Skip to content

Commit 01a6329

Browse files
authored
feat: implement useWatch() (#330)
1 parent f8e5f58 commit 01a6329

File tree

14 files changed

+171
-125
lines changed

14 files changed

+171
-125
lines changed

src/core/api.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,6 @@ export const onUnmount$: (first: () => void) => void;
174174
// @public
175175
export function onUnmountQrl(unmountFn: QRL<() => void>): void;
176176

177-
// @public
178-
export const onWatch$: (first: (obs: Observer) => unknown | (() => void)) => void;
179-
180-
// @public
181-
export function onWatchQrl(watchFn: QRL<(obs: Observer) => unknown | (() => void)>): void;
182-
183177
// @public
184178
export function onWindow(event: string, eventFn: QRL<() => void>): void;
185179

@@ -215,9 +209,9 @@ export interface QRL<TYPE = any> {
215209
// (undocumented)
216210
__brand__QRL__: TYPE;
217211
// (undocumented)
218-
invoke<ARGS extends any[]>(...args: ARGS): Promise<TYPE extends (...args: any) => any ? ReturnType<TYPE> : never>;
212+
invoke(...args: TYPE extends (...args: infer ARGS) => any ? ARGS : never): TYPE extends (...args: any[]) => infer RETURN ? ValueOrPromise<RETURN> : never;
219213
// (undocumented)
220-
invokeFn(el?: Element): (...args: any[]) => any;
214+
invokeFn(el?: Element): TYPE extends (...args: infer ARGS) => infer RETURN ? (...args: ARGS) => ValueOrPromise<RETURN> : never;
221215
// (undocumented)
222216
resolve(container?: Element): Promise<TYPE>;
223217
}
@@ -322,14 +316,22 @@ export function useStylesQrl(styles: QRL<string>): void;
322316
// @alpha (undocumented)
323317
export function useSubscriber<T extends {}>(obj: T): T;
324318

319+
// @public
320+
export const useWatch$: (first: (obs: Observer) => void | (() => void)) => void;
321+
322+
// @public
323+
export function useWatchQrl(watchQrl: QRL<(obs: Observer) => void | (() => void)>): void;
324+
325325
// @public
326326
export type ValueOrPromise<T> = T | Promise<T>;
327327

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

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

334336
// (No @packageDocumentation comment for this package)
335337

src/core/import/qrl-class.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { InvokeContext, newInvokeContext, tryGetInvokeContext, useInvoke } from '../use/use-core';
2+
import { then } from '../util/promises';
23
import type { ValueOrPromise } from '../util/types';
34
import { qrlImport, QRLSerializeOptions, stringifyQRL } from './qrl';
45
import type { QRL as IQRL } from './qrl.public';
@@ -33,24 +34,27 @@ class QRL<TYPE = any> implements IQRL<TYPE> {
3334
if (el) {
3435
this.setContainer(el);
3536
}
36-
return qrlImport(this.el, this);
37+
return qrlImport(this.el, this as any);
3738
}
3839

39-
invokeFn(): (...args: any[]) => any {
40-
return async (...args: any[]) => {
40+
invokeFn(el?: Element): any {
41+
return ((...args: any[]): any => {
4142
const currentCtx = tryGetInvokeContext();
42-
const fn = typeof this.symbolRef === 'function' ? this.symbolRef : await this.resolve();
43+
const fn = (typeof this.symbolRef === 'function' ? this.symbolRef : this.resolve(el)) as TYPE;
4344

44-
if (typeof fn === 'function') {
45-
const context: InvokeContext = {
46-
...newInvokeContext(),
47-
...currentCtx,
48-
qrl: this,
49-
};
50-
return useInvoke(context, fn as any, ...args);
51-
}
52-
throw new Error('QRL is not a function');
53-
};
45+
return then(fn, (fn) => {
46+
if (typeof fn === 'function') {
47+
const context: InvokeContext = {
48+
...newInvokeContext(),
49+
...currentCtx,
50+
qrl: this,
51+
waitOn: undefined,
52+
};
53+
return useInvoke(context, fn as any, ...args);
54+
}
55+
throw new Error('QRL is not a function');
56+
});
57+
}) as any;
5458
}
5559

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

67-
async invoke<ARGS extends any[]>(
68-
...args: ARGS
69-
): Promise<TYPE extends (...args: any) => any ? ReturnType<TYPE> : never> {
71+
invoke(...args: TYPE extends (...args: infer ARGS) => any ? ARGS : never) {
7072
const fn = this.invokeFn();
71-
return fn(...args);
73+
return fn(...args) as any;
7274
}
7375

7476
serialize(options?: QRLSerializeOptions) {

src/core/import/qrl.public.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ValueOrPromise } from '..';
12
import { runtimeQrl } from './qrl';
23

34
// <docs markdown="https://hackmd.io/m5DzCi5MTa26LuUj5t3HpQ#QRL">
@@ -128,10 +129,15 @@ import { runtimeQrl } from './qrl';
128129
export interface QRL<TYPE = any> {
129130
__brand__QRL__: TYPE;
130131
resolve(container?: Element): Promise<TYPE>;
131-
invoke<ARGS extends any[]>(
132-
...args: ARGS
133-
): Promise<TYPE extends (...args: any) => any ? ReturnType<TYPE> : never>;
134-
invokeFn(el?: Element): (...args: any[]) => any;
132+
invoke(
133+
...args: TYPE extends (...args: infer ARGS) => any ? ARGS : never
134+
): TYPE extends (...args: any[]) => infer RETURN ? ValueOrPromise<RETURN> : never;
135+
136+
invokeFn(
137+
el?: Element
138+
): TYPE extends (...args: infer ARGS) => infer RETURN
139+
? (...args: ARGS) => ValueOrPromise<RETURN>
140+
: never;
135141
}
136142

137143
/**

src/core/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export type { CorePlatform } from './platform/types';
5151
//////////////////////////////////////////////////////////////////////////////////////////
5252
// Watch
5353
//////////////////////////////////////////////////////////////////////////////////////////
54-
export { onWatch$, onWatchQrl } from './watch/watch.public';
54+
export { useWatch$, useWatchQrl } from './watch/watch.public';
5555
export type { Observer } from './watch/watch.public';
5656

5757
//////////////////////////////////////////////////////////////////////////////////////////

src/core/object/q-object.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { QError, qError } from '../error/error';
33
import { isQrl } from '../import/qrl-class';
44
import { notifyRender } from '../render/notify-render';
55
import { tryGetInvokeContext } from '../use/use-core';
6+
import { isElement } from '../util/element';
67
import { logWarn } from '../util/log';
78
import { qDev, qTest } from '../util/qdev';
89
import { debugStringify } from '../util/stringify';
10+
import { runWatch, WatchDescriptor } from '../watch/watch.public';
911

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

9597
class ReadWriteProxyHandler implements ProxyHandler<TargetType> {
9698
private subscriber?: Element;
97-
constructor(private proxyMap: ObjToProxyMap, private subs = new Map<Element, Set<string>>()) {}
99+
constructor(
100+
private proxyMap: ObjToProxyMap,
101+
private subs = new Map<Element | WatchDescriptor, Set<string>>()
102+
) {}
98103

99104
getSub(el: Element) {
100105
let sub = this.subs.get(el);
@@ -147,15 +152,15 @@ class ReadWriteProxyHandler implements ProxyHandler<TargetType> {
147152
const isArray = Array.isArray(target);
148153
if (isArray) {
149154
target[prop as any] = unwrappedNewValue;
150-
this.subs.forEach((_, el) => notifyRender(el));
155+
this.subs.forEach((_, sub) => notifyChange(sub));
151156
return true;
152157
}
153158
const oldValue = target[prop];
154159
if (oldValue !== unwrappedNewValue) {
155160
target[prop] = unwrappedNewValue;
156-
this.subs.forEach((propSets, el) => {
161+
this.subs.forEach((propSets, sub) => {
157162
if (propSets.has(prop)) {
158-
notifyRender(el);
163+
notifyChange(sub);
159164
}
160165
});
161166
}
@@ -174,6 +179,14 @@ class ReadWriteProxyHandler implements ProxyHandler<TargetType> {
174179
}
175180
}
176181

182+
export function notifyChange(subscriber: Element | WatchDescriptor) {
183+
if (isElement(subscriber)) {
184+
notifyRender(subscriber);
185+
} else {
186+
runWatch(subscriber as WatchDescriptor);
187+
}
188+
}
189+
177190
function verifySerializable<T>(value: T) {
178191
if (shouldSerialize(value) && typeof value == 'object' && value !== null) {
179192
if (Array.isArray(value)) return;

src/core/object/store.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export function resume(containerEl: Element) {
6464
};
6565

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

6969
// Rebuild target objects
7070
for (const obj of meta.objs) {
@@ -171,8 +171,8 @@ export function snapshotState(containerEl: Element) {
171171
const subs = proxyMap.get(obj)?.[QOjectSubsSymbol] as Map<Element, Set<string>>;
172172
if (subs) {
173173
return Object.fromEntries(
174-
Array.from(subs.entries()).map(([el, set]) => {
175-
const id = getElementID(el);
174+
Array.from(subs.entries()).map(([sub, set]) => {
175+
const id = getObjId(sub);
176176
if (id !== null) {
177177
return [id, Array.from(set)];
178178
} else {
@@ -284,7 +284,7 @@ export function walkNodes(nodes: Element[], parent: Element, predicate: (el: Ele
284284
function reviveValues(
285285
objs: any[],
286286
subs: any[],
287-
elementMap: Map<string, Element>,
287+
getObject: GetObject,
288288
map: ObjToProxyMap,
289289
containerEl: Element
290290
) {
@@ -301,7 +301,7 @@ function reviveValues(
301301
if (sub) {
302302
const converted = new Map();
303303
Object.entries(sub).forEach((entry) => {
304-
const el = elementMap.get(entry[0]);
304+
const el = getObject(entry[0]);
305305
if (!el) {
306306
logWarn(
307307
'QWIK can not revive subscriptions because of missing element ID',

src/core/use/use-core.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export interface InvokeContext {
2626
url: URL | null;
2727
qrl?: QRLInternal;
2828
subscriptions: boolean;
29-
waitOn?: Promise<any>[];
29+
waitOn?: ValueOrPromise<any>[];
3030
props?: Props;
3131
}
3232

@@ -104,7 +104,7 @@ export function newInvokeContext(
104104
/**
105105
* @private
106106
*/
107-
export function useWaitOn(promise: Promise<any>): void {
107+
export function useWaitOn(promise: ValueOrPromise<any>): void {
108108
const ctx = getInvokeContext();
109109
(ctx.waitOn || (ctx.waitOn = [])).push(promise);
110110
}

src/core/use/use-subscriber.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useHostElement } from './use-host-element.public';
22
import { QOjectOriginalProxy, QOjectTargetSymbol, SetSubscriber } from '../object/q-object';
3+
import type { WatchDescriptor } from '../watch/watch.public';
34

45
/**
56
* @alpha
@@ -11,7 +12,7 @@ export function useSubscriber<T extends {}>(obj: T): T {
1112
/**
1213
* @alpha
1314
*/
14-
export function wrapSubscriber<T extends {}>(obj: T, subscriber: Element) {
15+
export function wrapSubscriber<T extends {}>(obj: T, subscriber: Element | WatchDescriptor) {
1516
if (obj && typeof obj === 'object') {
1617
const target = (obj as any)[QOjectTargetSymbol];
1718
if (!target) {

src/core/watch/watch.examples.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
// it to the desired comment location
66
//
77

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

10-
// <docs anchor="onWatch">
10+
// <docs anchor="useWatch">
1111
export const MyComp = component$(() => {
1212
const store = useStore({ count: 0, doubleCount: 0 });
13-
onWatch$((obs) => {
13+
useWatch$((obs) => {
1414
store.doubleCount = 2 * obs(store).count;
1515
});
1616
return $(() => (

0 commit comments

Comments
 (0)