Skip to content

Commit 2f580ab

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

24 files changed

+464
-232
lines changed

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@
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
},
@@ -244,7 +244,7 @@
244244
}
245245
],
246246
"kind": "TypeAlias",
247-
"content": "```typescript\nexport type AsyncComputedReturnType<T> = T extends Promise<infer T> ? ReadonlySignal<T> : ReadonlySignal<T>;\n```\n**References:** [ReadonlySignal](#readonlysignal)",
247+
"content": "```typescript\nexport type AsyncComputedReturnType<T> = T extends Promise<infer T> ? PendingSignal<T> : PendingSignal<T>;\n```\n**References:** [PendingSignal](#pendingsignal)",
248248
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-async-computed.ts",
249249
"mdFile": "core.asynccomputedreturntype.md"
250250
},
@@ -1126,6 +1126,20 @@
11261126
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-visible-task.ts",
11271127
"mdFile": "core.onvisibletaskoptions.md"
11281128
},
1129+
{
1130+
"name": "PendingSignal",
1131+
"id": "pendingsignal",
1132+
"hierarchy": [
1133+
{
1134+
"name": "PendingSignal",
1135+
"id": "pendingsignal"
1136+
}
1137+
],
1138+
"kind": "Interface",
1139+
"content": "```typescript\nexport interface PendingSignal<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[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>",
1140+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/reactive-primitives/signal.public.ts",
1141+
"mdFile": "core.pendingsignal.md"
1142+
},
11291143
{
11301144
"name": "PrefetchGraph",
11311145
"id": "prefetchgraph",

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

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,21 +122,19 @@ 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

132130
## AsyncComputedReturnType
133131

134132
```typescript
135133
export type AsyncComputedReturnType<T> =
136-
T extends Promise<infer T> ? ReadonlySignal<T> : ReadonlySignal<T>;
134+
T extends Promise<infer T> ? PendingSignal<T> : PendingSignal<T>;
137135
```
138136

139-
**References:** [ReadonlySignal](#readonlysignal)
137+
**References:** [PendingSignal](#pendingsignal)
140138

141139
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-async-computed.ts)
142140

@@ -2058,6 +2056,48 @@ _(Optional)_ The strategy to use to determine when the "VisibleTask" should firs
20582056
20592057
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-visible-task.ts)
20602058
2059+
## PendingSignal
2060+
2061+
```typescript
2062+
export interface PendingSignal<T = unknown> extends ReadonlySignal<T>
2063+
```
2064+
2065+
**Extends:** [ReadonlySignal](#readonlysignal)&lt;T&gt;
2066+
2067+
<table><thead><tr><th>
2068+
2069+
Property
2070+
2071+
</th><th>
2072+
2073+
Modifiers
2074+
2075+
</th><th>
2076+
2077+
Type
2078+
2079+
</th><th>
2080+
2081+
Description
2082+
2083+
</th></tr></thead>
2084+
<tbody><tr><td>
2085+
2086+
[pending](#)
2087+
2088+
</td><td>
2089+
2090+
</td><td>
2091+
2092+
boolean
2093+
2094+
</td><td>
2095+
2096+
</td></tr>
2097+
</tbody></table>
2098+
2099+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/reactive-primitives/signal.public.ts)
2100+
20612101
## PrefetchGraph
20622102
20632103
> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.

packages/qwik/src/core/index.ts

Lines changed: 1 addition & 0 deletions
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 PendingSignal,
142143
type Signal,
143144
type ComputedSignal,
144145
} from './reactive-primitives/signal.public';

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ 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+
//
1921
// @public (undocumented)
20-
export type AsyncComputedFn<T> = (ctx: TaskCtx) => Promise<T>;
22+
export type AsyncComputedFn<T> = (ctx: AsyncComputedCtx) => Promise<T>;
2123

2224
// @public (undocumented)
23-
export type AsyncComputedReturnType<T> = T extends Promise<infer T> ? ReadonlySignal<T> : ReadonlySignal<T>;
25+
export type AsyncComputedReturnType<T> = T extends Promise<infer T> ? PendingSignal<T> : PendingSignal<T>;
2426

2527
// @public
2628
export type ClassList = string | undefined | null | false | Record<string, boolean | string | number | null | undefined> | ClassList[];
@@ -539,6 +541,12 @@ export interface OnVisibleTaskOptions {
539541
strategy?: VisibleTaskStrategy;
540542
}
541543

544+
// @public (undocumented)
545+
export interface PendingSignal<T = unknown> extends ReadonlySignal<T> {
546+
// (undocumented)
547+
pending: boolean;
548+
}
549+
542550
// @alpha @deprecated (undocumented)
543551
export const PrefetchGraph: (_opts?: {
544552
base?: string;

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

Lines changed: 17 additions & 0 deletions
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) {
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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+
18+
const DEBUG = false;
19+
const log = (...args: any[]) =>
20+
// eslint-disable-next-line no-console
21+
console.log('ASYNC COMPUTED SIGNAL', ...args.map(qwikDebugToString));
22+
23+
export class AsyncComputedSignalImpl<T>
24+
extends ComputedSignalImpl<T, AsyncComputeQRL<T>>
25+
implements BackRef
26+
{
27+
$untrackedPending$ = false;
28+
29+
$pendingEffects$: null | Set<EffectSubscription> = null;
30+
private $promiseValue$: T | null = null;
31+
32+
[_EFFECT_BACK_REF]: Map<EffectProperty | string, EffectSubscription> | null = null;
33+
34+
constructor(container: Container | null, fn: AsyncComputeQRL<T>, flags = SignalFlags.INVALID) {
35+
super(container, fn, flags);
36+
}
37+
38+
get pending(): boolean {
39+
return setupSignalValueAccess(
40+
this,
41+
() => (this.$pendingEffects$ ||= new Set()),
42+
() => this.untrackedPending
43+
);
44+
}
45+
46+
set untrackedPending(value: boolean) {
47+
if (value !== this.$untrackedPending$) {
48+
this.$untrackedPending$ = value;
49+
this.$container$?.$scheduler$(
50+
ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS,
51+
null,
52+
this,
53+
this.$pendingEffects$
54+
);
55+
}
56+
}
57+
58+
get untrackedPending() {
59+
return this.$untrackedPending$;
60+
}
61+
62+
$computeIfNeeded$() {
63+
if (!(this.$flags$ & SignalFlags.INVALID)) {
64+
return false;
65+
}
66+
const computeQrl = this.$computeQrl$;
67+
throwIfQRLNotResolved(computeQrl);
68+
69+
const untrackedValue =
70+
this.$promiseValue$ ?? (computeQrl.getFn()({ track: this.$trackFn$.bind(this) }) as T);
71+
if (isPromise(untrackedValue)) {
72+
this.untrackedPending = true;
73+
throw untrackedValue.then((promiseValue) => {
74+
this.$promiseValue$ = promiseValue;
75+
this.untrackedPending = false;
76+
});
77+
}
78+
this.$promiseValue$ = null;
79+
DEBUG && log('Signal.$asyncCompute$', untrackedValue);
80+
81+
this.$flags$ &= ~SignalFlags.INVALID;
82+
83+
const didChange = untrackedValue !== this.$untrackedValue$;
84+
if (didChange) {
85+
this.$untrackedValue$ = untrackedValue;
86+
}
87+
return didChange;
88+
}
89+
90+
private $trackFn$(obj: (() => unknown) | object | Signal<unknown>, prop?: string) {
91+
const ctx = newInvokeContext();
92+
ctx.$effectSubscriber$ = getSubscriber(this, EffectProperty.VNODE);
93+
ctx.$container$ = this.$container$ || undefined;
94+
return invoke(ctx, () => {
95+
if (isFunction(obj)) {
96+
return obj();
97+
}
98+
if (prop) {
99+
return (obj as Record<string, unknown>)[prop];
100+
} else if (isSignal(obj)) {
101+
return obj.value;
102+
} else if (isStore(obj)) {
103+
// track whole store
104+
addStoreEffect(
105+
getStoreTarget(obj)!,
106+
STORE_ALL_PROPS,
107+
getStoreHandler(obj)!,
108+
ctx.$effectSubscriber$!
109+
);
110+
return obj;
111+
} else {
112+
throw qError(QError.trackObjectWithoutProp);
113+
}
114+
});
115+
}
116+
}

packages/qwik/src/core/reactive-primitives/impl/computed-signal-impl.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { getSubscriber } from '../subscriber';
1111
import type { ComputeQRL, EffectSubscription } from '../types';
1212
import { _EFFECT_BACK_REF, EffectProperty, NEEDS_COMPUTATION, SignalFlags } from '../types';
1313
import { SignalImpl } from './signal-impl';
14+
import type { QRLInternal } from '../../shared/qrl/qrl-class';
1415

1516
const DEBUG = false;
1617
// eslint-disable-next-line no-console
@@ -21,14 +22,17 @@ const log = (...args: any[]) => console.log('COMPUTED SIGNAL', ...args.map(qwikD
2122
*
2223
* The value is available synchronously, but the computation is done lazily.
2324
*/
24-
export class ComputedSignalImpl<T> extends SignalImpl<T> implements BackRef {
25+
export class ComputedSignalImpl<T, S extends QRLInternal = ComputeQRL<T>>
26+
extends SignalImpl<T>
27+
implements BackRef
28+
{
2529
/**
2630
* The compute function is stored here.
2731
*
2832
* The computed functions must be executed synchronously (because of this we need to eagerly
2933
* resolve the QRL during the mark dirty phase so that any call to it will be synchronous). )
3034
*/
31-
$computeQrl$: ComputeQRL<T>;
35+
$computeQrl$: S;
3236
$flags$: SignalFlags;
3337
$forceRunEffects$: boolean = false;
3438
private $resolvedPromiseValue$: T | null = null;
@@ -37,7 +41,7 @@ export class ComputedSignalImpl<T> extends SignalImpl<T> implements BackRef {
3741

3842
constructor(
3943
container: Container | null,
40-
fn: ComputeQRL<T>,
44+
fn: S,
4145
// We need a separate flag to know when the computation needs running because
4246
// we need the old value to know if effects need running after computation
4347
flags = SignalFlags.INVALID
@@ -52,7 +56,12 @@ export class ComputedSignalImpl<T> extends SignalImpl<T> implements BackRef {
5256
$invalidate$() {
5357
this.$flags$ |= SignalFlags.INVALID;
5458
this.$forceRunEffects$ = false;
55-
this.$container$?.$scheduler$(ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, null, this);
59+
this.$container$?.$scheduler$(
60+
ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS,
61+
null,
62+
this,
63+
this.$effects$
64+
);
5665
}
5766

5867
/**
@@ -61,7 +70,12 @@ export class ComputedSignalImpl<T> extends SignalImpl<T> implements BackRef {
6170
*/
6271
force() {
6372
this.$forceRunEffects$ = true;
64-
this.$container$?.$scheduler$(ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, null, this);
73+
this.$container$?.$scheduler$(
74+
ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS,
75+
null,
76+
this,
77+
this.$effects$
78+
);
6579
}
6680

6781
get untrackedValue() {
@@ -84,7 +98,7 @@ export class ComputedSignalImpl<T> extends SignalImpl<T> implements BackRef {
8498
const previousEffectSubscription = ctx?.$effectSubscriber$;
8599
ctx && (ctx.$effectSubscriber$ = getSubscriber(this, EffectProperty.VNODE));
86100
try {
87-
const untrackedValue = this.$resolvedPromiseValue$ || (computeQrl.getFn(ctx)() as T);
101+
const untrackedValue = this.$resolvedPromiseValue$ || ((computeQrl.getFn(ctx) as S)() as T);
88102
if (isPromise(untrackedValue)) {
89103
throw untrackedValue.then((promiseValue) => {
90104
this.$resolvedPromiseValue$ = promiseValue;

0 commit comments

Comments
 (0)