Skip to content

Commit 1163e3d

Browse files
committed
WIP: update to signal-based implementation
1 parent 9d1612d commit 1163e3d

24 files changed

+517
-232
lines changed

packages/docs/src/routes/api/qwik/api.json

+16-2
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,24 @@
230230
}
231231
],
232232
"kind": "TypeAlias",
233-
"content": "```typescript\nexport type AsyncComputedFn<T> = (ctx: TaskCtx) => Promise<T>;\n```\n**References:** [TaskCtx](#taskctx)",
233+
"content": "```typescript\nexport type AsyncComputedFn<T> = (ctx: AsyncComputedCtx) => Promise<T>;\n```",
234234
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-async-computed.ts",
235235
"mdFile": "core.asynccomputedfn.md"
236236
},
237+
{
238+
"name": "AsyncComputedReadonlySignal",
239+
"id": "asynccomputedreadonlysignal",
240+
"hierarchy": [
241+
{
242+
"name": "AsyncComputedReadonlySignal",
243+
"id": "asynccomputedreadonlysignal"
244+
}
245+
],
246+
"kind": "Interface",
247+
"content": "```typescript\nexport interface AsyncComputedReadonlySignal<T = unknown> extends ReadonlySignal<T> \n```\n**Extends:** [ReadonlySignal](#readonlysignal)<!-- -->&lt;T&gt;\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[error](#)\n\n\n</td><td>\n\n\n</td><td>\n\nError \\| null\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\n[pending](#)\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>",
248+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/reactive-primitives/signal.public.ts",
249+
"mdFile": "core.asynccomputedreadonlysignal.md"
250+
},
237251
{
238252
"name": "AsyncComputedReturnType",
239253
"id": "asynccomputedreturntype",
@@ -244,7 +258,7 @@
244258
}
245259
],
246260
"kind": "TypeAlias",
247-
"content": "```typescript\nexport type AsyncComputedReturnType<T> = T extends Promise<infer T> ? ReadonlySignal<T> : ReadonlySignal<T>;\n```\n**References:** [ReadonlySignal](#readonlysignal)",
261+
"content": "```typescript\nexport type AsyncComputedReturnType<T> = T extends Promise<infer T> ? AsyncComputedReadonlySignal<T> : AsyncComputedReadonlySignal<T>;\n```\n**References:** [AsyncComputedReadonlySignal](#asynccomputedreadonlysignal)",
248262
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-async-computed.ts",
249263
"mdFile": "core.asynccomputedreturntype.md"
250264
},

packages/docs/src/routes/api/qwik/index.mdx

+60-5
Original file line numberDiff line numberDiff line change
@@ -122,21 +122,76 @@ Expression which should be lazy loaded
122122
## AsyncComputedFn
123123

124124
```typescript
125-
export type AsyncComputedFn<T> = (ctx: TaskCtx) => Promise<T>;
125+
export type AsyncComputedFn<T> = (ctx: AsyncComputedCtx) => Promise<T>;
126126
```
127127

128-
**References:** [TaskCtx](#taskctx)
129-
130128
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-async-computed.ts)
131129

130+
## AsyncComputedReadonlySignal
131+
132+
```typescript
133+
export interface AsyncComputedReadonlySignal<T = unknown> extends ReadonlySignal<T>
134+
```
135+
136+
**Extends:** [ReadonlySignal](#readonlysignal)&lt;T&gt;
137+
138+
<table><thead><tr><th>
139+
140+
Property
141+
142+
</th><th>
143+
144+
Modifiers
145+
146+
</th><th>
147+
148+
Type
149+
150+
</th><th>
151+
152+
Description
153+
154+
</th></tr></thead>
155+
<tbody><tr><td>
156+
157+
[error](#)
158+
159+
</td><td>
160+
161+
</td><td>
162+
163+
Error \| null
164+
165+
</td><td>
166+
167+
</td></tr>
168+
<tr><td>
169+
170+
[pending](#)
171+
172+
</td><td>
173+
174+
</td><td>
175+
176+
boolean
177+
178+
</td><td>
179+
180+
</td></tr>
181+
</tbody></table>
182+
183+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/reactive-primitives/signal.public.ts)
184+
132185
## AsyncComputedReturnType
133186
134187
```typescript
135188
export type AsyncComputedReturnType<T> =
136-
T extends Promise<infer T> ? ReadonlySignal<T> : ReadonlySignal<T>;
189+
T extends Promise<infer T>
190+
? AsyncComputedReadonlySignal<T>
191+
: AsyncComputedReadonlySignal<T>;
137192
```
138193
139-
**References:** [ReadonlySignal](#readonlysignal)
194+
**References:** [AsyncComputedReadonlySignal](#asynccomputedreadonlysignal)
140195
141196
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-async-computed.ts)
142197

packages/qwik/src/core/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ export { useErrorBoundary } from './use/use-error-boundary';
139139
export type { ErrorBoundaryStore } from './shared/error/error-handling';
140140
export {
141141
type ReadonlySignal,
142+
type AsyncComputedReadonlySignal,
142143
type Signal,
143144
type ComputedSignal,
144145
} from './reactive-primitives/signal.public';

packages/qwik/src/core/qwik.core.api.md

+12-2
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,21 @@ import { ValueOrPromise as ValueOrPromise_2 } from '..';
1616
// @public
1717
export const $: <T>(expression: T) => QRL<T>;
1818

19+
// Warning: (ae-forgotten-export) The symbol "AsyncComputedCtx" needs to be exported by the entry point index.d.ts
20+
//
21+
// @public (undocumented)
22+
export type AsyncComputedFn<T> = (ctx: AsyncComputedCtx) => Promise<T>;
23+
1924
// @public (undocumented)
20-
export type AsyncComputedFn<T> = (ctx: TaskCtx) => Promise<T>;
25+
export interface AsyncComputedReadonlySignal<T = unknown> extends ReadonlySignal<T> {
26+
// (undocumented)
27+
error: Error | null;
28+
// (undocumented)
29+
pending: boolean;
30+
}
2131

2232
// @public (undocumented)
23-
export type AsyncComputedReturnType<T> = T extends Promise<infer T> ? ReadonlySignal<T> : ReadonlySignal<T>;
33+
export type AsyncComputedReturnType<T> = T extends Promise<infer T> ? AsyncComputedReadonlySignal<T> : AsyncComputedReadonlySignal<T>;
2434

2535
// @public
2636
export type ClassList = string | undefined | null | false | Record<string, boolean | string | number | null | undefined> | ClassList[];

packages/qwik/src/core/reactive-primitives/cleanup.ts

+17
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
type EffectProperty,
1111
type EffectSubscription,
1212
} from './types';
13+
import { AsyncComputedSignalImpl } from './impl/async-computed-signal-impl';
1314

1415
/** Class for back reference to the EffectSubscription */
1516
export abstract class BackRef {
@@ -32,6 +33,8 @@ export function clearAllEffects(container: Container, consumer: Consumer): void
3233
for (const producer of backRefs) {
3334
if (producer instanceof SignalImpl) {
3435
clearSignal(container, producer, effect);
36+
} else if (producer instanceof AsyncComputedSignalImpl) {
37+
clearAsyncComputedSignal(producer, effect);
3538
} else if (container.$storeProxyMap$.has(producer)) {
3639
const target = container.$storeProxyMap$.get(producer)!;
3740
const storeHandler = getStoreHandler(target)!;
@@ -53,6 +56,20 @@ function clearSignal(container: Container, producer: SignalImpl, effect: EffectS
5356
}
5457
}
5558

59+
function clearAsyncComputedSignal(
60+
producer: AsyncComputedSignalImpl<unknown>,
61+
effect: EffectSubscription
62+
) {
63+
const effects = producer.$effects$;
64+
if (effects) {
65+
effects.delete(effect);
66+
}
67+
const pendingEffects = producer.$pendingEffects$;
68+
if (pendingEffects) {
69+
pendingEffects.delete(effect);
70+
}
71+
}
72+
5673
function clearStore(producer: StoreHandler, effect: EffectSubscription) {
5774
const effects = producer?.$effects$;
5875
if (effects) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { qwikDebugToString } from '../../debug';
2+
import { QError, qError } from '../../shared/error/error';
3+
import type { Container } from '../../shared/types';
4+
import { ChoreType } from '../../shared/util-chore-type';
5+
import { isPromise } from '../../shared/utils/promises';
6+
import { invoke, newInvokeContext } from '../../use/use-core';
7+
import { isSignal, throwIfQRLNotResolved } from '../utils';
8+
import type { BackRef } from '../cleanup';
9+
import { getSubscriber } from '../subscriber';
10+
import type { AsyncComputeQRL, EffectSubscription } from '../types';
11+
import { _EFFECT_BACK_REF, EffectProperty, SignalFlags, STORE_ALL_PROPS } from '../types';
12+
import { addStoreEffect, getStoreHandler, getStoreTarget, isStore } from './store';
13+
import type { Signal } from '../signal.public';
14+
import { isFunction } from '../../shared/utils/types';
15+
import { ComputedSignalImpl } from './computed-signal-impl';
16+
import { setupSignalValueAccess } from './signal-impl';
17+
import { isDev } from '@qwik.dev/core/build';
18+
19+
const DEBUG = false;
20+
const log = (...args: any[]) =>
21+
// eslint-disable-next-line no-console
22+
console.log('ASYNC COMPUTED SIGNAL', ...args.map(qwikDebugToString));
23+
24+
export class AsyncComputedSignalImpl<T>
25+
extends ComputedSignalImpl<T, AsyncComputeQRL<T>>
26+
implements BackRef
27+
{
28+
$untrackedPending$: boolean = false;
29+
$untrackedError$: Error | null = null;
30+
31+
$pendingEffects$: null | Set<EffectSubscription> = null;
32+
$errorEffects$: null | Set<EffectSubscription> = null;
33+
private $promiseValue$: T | null = null;
34+
35+
[_EFFECT_BACK_REF]: Map<EffectProperty | string, EffectSubscription> | null = null;
36+
37+
constructor(container: Container | null, fn: AsyncComputeQRL<T>, flags = SignalFlags.INVALID) {
38+
super(container, fn, flags);
39+
}
40+
41+
get pending(): boolean {
42+
return setupSignalValueAccess(
43+
this,
44+
() => (this.$pendingEffects$ ||= new Set()),
45+
() => this.untrackedPending
46+
);
47+
}
48+
49+
set untrackedPending(value: boolean) {
50+
if (value !== this.$untrackedPending$) {
51+
this.$untrackedPending$ = value;
52+
this.$container$?.$scheduler$(
53+
ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS,
54+
null,
55+
this,
56+
this.$pendingEffects$
57+
);
58+
}
59+
}
60+
61+
get untrackedPending() {
62+
return this.$untrackedPending$;
63+
}
64+
65+
get error(): Error | null {
66+
return setupSignalValueAccess(
67+
this,
68+
() => (this.$errorEffects$ ||= new Set()),
69+
() => this.untrackedError
70+
);
71+
}
72+
73+
set untrackedError(value: Error | null) {
74+
if (value !== this.$untrackedError$) {
75+
this.$untrackedError$ = value;
76+
this.$container$?.$scheduler$(
77+
ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS,
78+
null,
79+
this,
80+
this.$errorEffects$
81+
);
82+
}
83+
}
84+
85+
get untrackedError() {
86+
return this.$untrackedError$;
87+
}
88+
89+
$computeIfNeeded$() {
90+
if (!(this.$flags$ & SignalFlags.INVALID)) {
91+
return false;
92+
}
93+
const computeQrl = this.$computeQrl$;
94+
throwIfQRLNotResolved(computeQrl);
95+
96+
const untrackedValue =
97+
this.$promiseValue$ ?? (computeQrl.getFn()({ track: this.$trackFn$.bind(this) }) as T);
98+
if (isPromise(untrackedValue)) {
99+
this.untrackedPending = true;
100+
this.untrackedError = null;
101+
throw untrackedValue
102+
.then((promiseValue) => {
103+
this.$promiseValue$ = promiseValue;
104+
this.untrackedPending = false;
105+
})
106+
.catch((err) => {
107+
if (isDev) {
108+
console.error(err);
109+
}
110+
this.untrackedError = err;
111+
});
112+
}
113+
this.$promiseValue$ = null;
114+
DEBUG && log('Signal.$asyncCompute$', untrackedValue);
115+
116+
this.$flags$ &= ~SignalFlags.INVALID;
117+
118+
const didChange = untrackedValue !== this.$untrackedValue$;
119+
if (didChange) {
120+
this.$untrackedValue$ = untrackedValue;
121+
}
122+
return didChange;
123+
}
124+
125+
private $trackFn$(obj: (() => unknown) | object | Signal<unknown>, prop?: string) {
126+
const ctx = newInvokeContext();
127+
ctx.$effectSubscriber$ = getSubscriber(this, EffectProperty.VNODE);
128+
ctx.$container$ = this.$container$ || undefined;
129+
return invoke(ctx, () => {
130+
if (isFunction(obj)) {
131+
return obj();
132+
}
133+
if (prop) {
134+
return (obj as Record<string, unknown>)[prop];
135+
} else if (isSignal(obj)) {
136+
return obj.value;
137+
} else if (isStore(obj)) {
138+
// track whole store
139+
addStoreEffect(
140+
getStoreTarget(obj)!,
141+
STORE_ALL_PROPS,
142+
getStoreHandler(obj)!,
143+
ctx.$effectSubscriber$!
144+
);
145+
return obj;
146+
} else {
147+
throw qError(QError.trackObjectWithoutProp);
148+
}
149+
});
150+
}
151+
}

0 commit comments

Comments
 (0)