Skip to content

Commit

Permalink
feat: implement useWatch() (#330)
Browse files Browse the repository at this point in the history
  • Loading branch information
manucorporat authored Mar 28, 2022
1 parent f8e5f58 commit 01a6329
Show file tree
Hide file tree
Showing 14 changed files with 171 additions and 125 deletions.
20 changes: 11 additions & 9 deletions src/core/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,6 @@ export const onUnmount$: (first: () => void) => void;
// @public
export function onUnmountQrl(unmountFn: QRL<() => void>): void;

// @public
export const onWatch$: (first: (obs: Observer) => unknown | (() => void)) => void;

// @public
export function onWatchQrl(watchFn: QRL<(obs: Observer) => unknown | (() => void)>): void;

// @public
export function onWindow(event: string, eventFn: QRL<() => void>): void;

Expand Down Expand Up @@ -215,9 +209,9 @@ export interface QRL<TYPE = any> {
// (undocumented)
__brand__QRL__: TYPE;
// (undocumented)
invoke<ARGS extends any[]>(...args: ARGS): Promise<TYPE extends (...args: any) => any ? ReturnType<TYPE> : never>;
invoke(...args: TYPE extends (...args: infer ARGS) => any ? ARGS : never): TYPE extends (...args: any[]) => infer RETURN ? ValueOrPromise<RETURN> : never;
// (undocumented)
invokeFn(el?: Element): (...args: any[]) => any;
invokeFn(el?: Element): TYPE extends (...args: infer ARGS) => infer RETURN ? (...args: ARGS) => ValueOrPromise<RETURN> : never;
// (undocumented)
resolve(container?: Element): Promise<TYPE>;
}
Expand Down Expand Up @@ -322,14 +316,22 @@ export function useStylesQrl(styles: QRL<string>): void;
// @alpha (undocumented)
export function useSubscriber<T extends {}>(obj: T): T;

// @public
export const useWatch$: (first: (obs: Observer) => void | (() => void)) => void;

// @public
export function useWatchQrl(watchQrl: QRL<(obs: Observer) => void | (() => void)>): void;

// @public
export type ValueOrPromise<T> = T | Promise<T>;

// @alpha (undocumented)
export const version: string;

// Warning: (ae-forgotten-export) The symbol "WatchDescriptor" needs to be exported by the entry point index.d.ts
//
// @alpha (undocumented)
export function wrapSubscriber<T extends {}>(obj: T, subscriber: Element): any;
export function wrapSubscriber<T extends {}>(obj: T, subscriber: Element | WatchDescriptor): any;

// (No @packageDocumentation comment for this package)

Expand Down
38 changes: 20 additions & 18 deletions src/core/import/qrl-class.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { InvokeContext, newInvokeContext, tryGetInvokeContext, useInvoke } from '../use/use-core';
import { then } from '../util/promises';
import type { ValueOrPromise } from '../util/types';
import { qrlImport, QRLSerializeOptions, stringifyQRL } from './qrl';
import type { QRL as IQRL } from './qrl.public';
Expand Down Expand Up @@ -33,24 +34,27 @@ class QRL<TYPE = any> implements IQRL<TYPE> {
if (el) {
this.setContainer(el);
}
return qrlImport(this.el, this);
return qrlImport(this.el, this as any);
}

invokeFn(): (...args: any[]) => any {
return async (...args: any[]) => {
invokeFn(el?: Element): any {
return ((...args: any[]): any => {
const currentCtx = tryGetInvokeContext();
const fn = typeof this.symbolRef === 'function' ? this.symbolRef : await this.resolve();
const fn = (typeof this.symbolRef === 'function' ? this.symbolRef : this.resolve(el)) as TYPE;

if (typeof fn === 'function') {
const context: InvokeContext = {
...newInvokeContext(),
...currentCtx,
qrl: this,
};
return useInvoke(context, fn as any, ...args);
}
throw new Error('QRL is not a function');
};
return then(fn, (fn) => {
if (typeof fn === 'function') {
const context: InvokeContext = {
...newInvokeContext(),
...currentCtx,
qrl: this,
waitOn: undefined,
};
return useInvoke(context, fn as any, ...args);
}
throw new Error('QRL is not a function');
});
}) as any;
}

copy(): QRLInternal<TYPE> {
Expand All @@ -64,11 +68,9 @@ class QRL<TYPE = any> implements IQRL<TYPE> {
);
}

async invoke<ARGS extends any[]>(
...args: ARGS
): Promise<TYPE extends (...args: any) => any ? ReturnType<TYPE> : never> {
invoke(...args: TYPE extends (...args: infer ARGS) => any ? ARGS : never) {
const fn = this.invokeFn();
return fn(...args);
return fn(...args) as any;
}

serialize(options?: QRLSerializeOptions) {
Expand Down
14 changes: 10 additions & 4 deletions src/core/import/qrl.public.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ValueOrPromise } from '..';
import { runtimeQrl } from './qrl';

// <docs markdown="https://hackmd.io/m5DzCi5MTa26LuUj5t3HpQ#QRL">
Expand Down Expand Up @@ -128,10 +129,15 @@ import { runtimeQrl } from './qrl';
export interface QRL<TYPE = any> {
__brand__QRL__: TYPE;
resolve(container?: Element): Promise<TYPE>;
invoke<ARGS extends any[]>(
...args: ARGS
): Promise<TYPE extends (...args: any) => any ? ReturnType<TYPE> : never>;
invokeFn(el?: Element): (...args: any[]) => any;
invoke(
...args: TYPE extends (...args: infer ARGS) => any ? ARGS : never
): TYPE extends (...args: any[]) => infer RETURN ? ValueOrPromise<RETURN> : never;

invokeFn(
el?: Element
): TYPE extends (...args: infer ARGS) => infer RETURN
? (...args: ARGS) => ValueOrPromise<RETURN>
: never;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export type { CorePlatform } from './platform/types';
//////////////////////////////////////////////////////////////////////////////////////////
// Watch
//////////////////////////////////////////////////////////////////////////////////////////
export { onWatch$, onWatchQrl } from './watch/watch.public';
export { useWatch$, useWatchQrl } from './watch/watch.public';
export type { Observer } from './watch/watch.public';

//////////////////////////////////////////////////////////////////////////////////////////
Expand Down
21 changes: 17 additions & 4 deletions src/core/object/q-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { QError, qError } from '../error/error';
import { isQrl } from '../import/qrl-class';
import { notifyRender } from '../render/notify-render';
import { tryGetInvokeContext } from '../use/use-core';
import { isElement } from '../util/element';
import { logWarn } from '../util/log';
import { qDev, qTest } from '../util/qdev';
import { debugStringify } from '../util/stringify';
import { runWatch, WatchDescriptor } from '../watch/watch.public';

export type ObjToProxyMap = WeakMap<any, any>;
export type QObject<T extends {}> = T & { __brand__: 'QObject' };
Expand Down Expand Up @@ -94,7 +96,10 @@ type TargetType = Record<string | symbol, any>;

class ReadWriteProxyHandler implements ProxyHandler<TargetType> {
private subscriber?: Element;
constructor(private proxyMap: ObjToProxyMap, private subs = new Map<Element, Set<string>>()) {}
constructor(
private proxyMap: ObjToProxyMap,
private subs = new Map<Element | WatchDescriptor, Set<string>>()
) {}

getSub(el: Element) {
let sub = this.subs.get(el);
Expand Down Expand Up @@ -147,15 +152,15 @@ class ReadWriteProxyHandler implements ProxyHandler<TargetType> {
const isArray = Array.isArray(target);
if (isArray) {
target[prop as any] = unwrappedNewValue;
this.subs.forEach((_, el) => notifyRender(el));
this.subs.forEach((_, sub) => notifyChange(sub));
return true;
}
const oldValue = target[prop];
if (oldValue !== unwrappedNewValue) {
target[prop] = unwrappedNewValue;
this.subs.forEach((propSets, el) => {
this.subs.forEach((propSets, sub) => {
if (propSets.has(prop)) {
notifyRender(el);
notifyChange(sub);
}
});
}
Expand All @@ -174,6 +179,14 @@ class ReadWriteProxyHandler implements ProxyHandler<TargetType> {
}
}

export function notifyChange(subscriber: Element | WatchDescriptor) {
if (isElement(subscriber)) {
notifyRender(subscriber);
} else {
runWatch(subscriber as WatchDescriptor);
}
}

function verifySerializable<T>(value: T) {
if (shouldSerialize(value) && typeof value == 'object' && value !== null) {
if (Array.isArray(value)) return;
Expand Down
10 changes: 5 additions & 5 deletions src/core/object/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function resume(containerEl: Element) {
};

// Revive proxies with subscriptions into the proxymap
reviveValues(meta.objs, meta.subs, elements, map, parentJSON);
reviveValues(meta.objs, meta.subs, getObject, map, parentJSON);

// Rebuild target objects
for (const obj of meta.objs) {
Expand Down Expand Up @@ -171,8 +171,8 @@ export function snapshotState(containerEl: Element) {
const subs = proxyMap.get(obj)?.[QOjectSubsSymbol] as Map<Element, Set<string>>;
if (subs) {
return Object.fromEntries(
Array.from(subs.entries()).map(([el, set]) => {
const id = getElementID(el);
Array.from(subs.entries()).map(([sub, set]) => {
const id = getObjId(sub);
if (id !== null) {
return [id, Array.from(set)];
} else {
Expand Down Expand Up @@ -284,7 +284,7 @@ export function walkNodes(nodes: Element[], parent: Element, predicate: (el: Ele
function reviveValues(
objs: any[],
subs: any[],
elementMap: Map<string, Element>,
getObject: GetObject,
map: ObjToProxyMap,
containerEl: Element
) {
Expand All @@ -301,7 +301,7 @@ function reviveValues(
if (sub) {
const converted = new Map();
Object.entries(sub).forEach((entry) => {
const el = elementMap.get(entry[0]);
const el = getObject(entry[0]);
if (!el) {
logWarn(
'QWIK can not revive subscriptions because of missing element ID',
Expand Down
4 changes: 2 additions & 2 deletions src/core/use/use-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface InvokeContext {
url: URL | null;
qrl?: QRLInternal;
subscriptions: boolean;
waitOn?: Promise<any>[];
waitOn?: ValueOrPromise<any>[];
props?: Props;
}

Expand Down Expand Up @@ -104,7 +104,7 @@ export function newInvokeContext(
/**
* @private
*/
export function useWaitOn(promise: Promise<any>): void {
export function useWaitOn(promise: ValueOrPromise<any>): void {
const ctx = getInvokeContext();
(ctx.waitOn || (ctx.waitOn = [])).push(promise);
}
Expand Down
3 changes: 2 additions & 1 deletion src/core/use/use-subscriber.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useHostElement } from './use-host-element.public';
import { QOjectOriginalProxy, QOjectTargetSymbol, SetSubscriber } from '../object/q-object';
import type { WatchDescriptor } from '../watch/watch.public';

/**
* @alpha
Expand All @@ -11,7 +12,7 @@ export function useSubscriber<T extends {}>(obj: T): T {
/**
* @alpha
*/
export function wrapSubscriber<T extends {}>(obj: T, subscriber: Element) {
export function wrapSubscriber<T extends {}>(obj: T, subscriber: Element | WatchDescriptor) {
if (obj && typeof obj === 'object') {
const target = (obj as any)[QOjectTargetSymbol];
if (!target) {
Expand Down
6 changes: 3 additions & 3 deletions src/core/watch/watch.examples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
// it to the desired comment location
//

import { component$, useStore, $, onWatch$ } from '@builder.io/qwik';
import { component$, useStore, $, useWatch$ } from '@builder.io/qwik';

// <docs anchor="onWatch">
// <docs anchor="useWatch">
export const MyComp = component$(() => {
const store = useStore({ count: 0, doubleCount: 0 });
onWatch$((obs) => {
useWatch$((obs) => {
store.doubleCount = 2 * obs(store).count;
});
return $(() => (
Expand Down
Loading

0 comments on commit 01a6329

Please sign in to comment.