Skip to content

Commit be77ee7

Browse files
committed
feat: serialization for async computed signal
1 parent a095d32 commit be77ee7

File tree

7 files changed

+124
-21
lines changed

7 files changed

+124
-21
lines changed

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,14 @@ const addEffect = (
107107

108108
export const setupSignalValueAccess = <T, S>(
109109
target: SignalImpl<T>,
110-
effectsGetter: () => Set<EffectSubscription>,
111-
valueGetter: () => S
110+
effectsFn: () => Set<EffectSubscription>,
111+
returnValueFn: () => S
112112
) => {
113113
const ctx = tryGetInvokeContext();
114114
if (ctx) {
115115
if (target.$container$ === null) {
116116
if (!ctx.$container$) {
117-
return valueGetter();
117+
return returnValueFn();
118118
}
119119
// Grab the container now we have access to it
120120
target.$container$ = ctx.$container$;
@@ -126,9 +126,9 @@ export const setupSignalValueAccess = <T, S>(
126126
}
127127
const effectSubscriber = ctx.$effectSubscriber$;
128128
if (effectSubscriber) {
129-
addEffect(target, effectSubscriber, effectsGetter());
129+
addEffect(target, effectSubscriber, effectsFn());
130130
DEBUG && log('read->sub', pad('\n' + target.toString(), ' '));
131131
}
132132
}
133-
return valueGetter();
133+
return returnValueFn();
134134
};

packages/qwik/src/core/reactive-primitives/signal-api.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { SignalImpl } from './impl/signal-impl';
44
import { ComputedSignalImpl } from './impl/computed-signal-impl';
55
import { throwIfQRLNotResolved } from './utils';
66
import type { Signal } from './signal.public';
7-
import type { SerializerArg } from './types';
7+
import type { AsyncComputedCtx, AsyncComputeQRL, ComputeQRL, SerializerArg } from './types';
88
import { SerializerSignalImpl } from './impl/serializer-signal-impl';
9+
import { AsyncComputedSignalImpl } from './impl/async-computed-signal-impl';
910

1011
/** @internal */
1112
export const createSignal = <T>(value?: T): Signal<T> => {
@@ -15,7 +16,15 @@ export const createSignal = <T>(value?: T): Signal<T> => {
1516
/** @internal */
1617
export const createComputedSignal = <T>(qrl: QRL<() => T>): ComputedSignalImpl<T> => {
1718
throwIfQRLNotResolved(qrl);
18-
return new ComputedSignalImpl<T>(null, qrl as QRLInternal<() => T>);
19+
return new ComputedSignalImpl<T>(null, qrl as ComputeQRL<T>);
20+
};
21+
22+
/** @internal */
23+
export const createAsyncComputedSignal = <T>(
24+
qrl: QRL<(ctx: AsyncComputedCtx) => Promise<T>>
25+
): AsyncComputedSignalImpl<T> => {
26+
throwIfQRLNotResolved(qrl);
27+
return new AsyncComputedSignalImpl<T>(null, qrl as AsyncComputeQRL<T>);
1928
};
2029

2130
/** @internal */

packages/qwik/src/core/shared/scheduler.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,9 @@ export const createScheduler = (
237237
const isClientOnly =
238238
type === ChoreType.JOURNAL_FLUSH ||
239239
type === ChoreType.NODE_DIFF ||
240-
type === ChoreType.NODE_PROP;
240+
type === ChoreType.NODE_PROP ||
241+
type === ChoreType.QRL_RESOLVE ||
242+
type === ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS;
241243
if (isServer && isClientOnly) {
242244
DEBUG &&
243245
debugTrace(

packages/qwik/src/core/shared/shared-serialization.ts

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { type DomContainer } from '../client/dom-container';
77
import type { VNode } from '../client/types';
88
import { vnode_getNode, vnode_isVNode, vnode_locate, vnode_toString } from '../client/vnode';
99
import { isSerializerObj } from '../reactive-primitives/utils';
10-
import type { SerializerArg } from '../reactive-primitives/types';
10+
import type { AsyncComputeQRL, SerializerArg } from '../reactive-primitives/types';
1111
import {
1212
getOrCreateStore,
1313
getStoreHandler,
@@ -289,10 +289,34 @@ const inflate = (
289289
signal.$effects$ = new Set(d.slice(5) as EffectSubscription[]);
290290
break;
291291
}
292+
case TypeIds.AsyncComputedSignal: {
293+
const asyncComputed = target as AsyncComputedSignalImpl<unknown>;
294+
const d = data as [
295+
AsyncComputeQRL<unknown>,
296+
Array<EffectSubscription> | null,
297+
Array<EffectSubscription> | null,
298+
Array<EffectSubscription> | null,
299+
boolean,
300+
boolean,
301+
unknown?,
302+
];
303+
asyncComputed.$computeQrl$ = d[0];
304+
asyncComputed.$effects$ = new Set(d[1]);
305+
asyncComputed.$pendingEffects$ = new Set(d[2]);
306+
asyncComputed.$failedEffects$ = new Set(d[3]);
307+
asyncComputed.$untrackedPending$ = d[4];
308+
asyncComputed.$untrackedFailed$ = d[5];
309+
const hasValue = d.length > 6;
310+
if (hasValue) {
311+
asyncComputed.$untrackedValue$ = d[6];
312+
} else {
313+
asyncComputed.$flags$ |= SignalFlags.INVALID;
314+
}
315+
break;
316+
}
292317
// Inflating a SerializerSignal is the same as inflating a ComputedSignal
293318
case TypeIds.SerializerSignal:
294-
case TypeIds.ComputedSignal:
295-
case TypeIds.AsyncComputedSignal: {
319+
case TypeIds.ComputedSignal: {
296320
const computed = target as ComputedSignalImpl<unknown>;
297321
const d = data as [QRLInternal<() => {}>, EffectSubscription[] | null, unknown?];
298322
computed.$computeQrl$ = d[0];
@@ -1160,6 +1184,28 @@ async function serialize(serializationContext: SerializationContext): Promise<vo
11601184
value.$hostElement$,
11611185
...(value.$effects$ || []),
11621186
]);
1187+
} else if (value instanceof AsyncComputedSignalImpl) {
1188+
addPreloadQrl(value.$computeQrl$);
1189+
const out: [
1190+
QRLInternal,
1191+
Set<EffectSubscription> | null,
1192+
Set<EffectSubscription> | null,
1193+
Set<EffectSubscription> | null,
1194+
boolean,
1195+
boolean,
1196+
unknown?,
1197+
] = [
1198+
value.$computeQrl$,
1199+
value.$effects$,
1200+
value.$pendingEffects$,
1201+
value.$failedEffects$,
1202+
value.$untrackedPending$,
1203+
value.$untrackedFailed$,
1204+
];
1205+
if (v !== NEEDS_COMPUTATION) {
1206+
out.push(v);
1207+
}
1208+
output(TypeIds.AsyncComputedSignal, out);
11631209
} else if (value instanceof ComputedSignalImpl) {
11641210
addPreloadQrl(value.$computeQrl$);
11651211
const out: [QRLInternal, Set<EffectSubscription> | null, unknown?] = [
@@ -1170,12 +1216,7 @@ async function serialize(serializationContext: SerializationContext): Promise<vo
11701216
if (v !== NEEDS_COMPUTATION) {
11711217
out.push(v);
11721218
}
1173-
output(
1174-
value instanceof AsyncComputedSignalImpl
1175-
? TypeIds.AsyncComputedSignal
1176-
: TypeIds.ComputedSignal,
1177-
out
1178-
);
1219+
output(TypeIds.ComputedSignal, out);
11791220
} else {
11801221
output(TypeIds.Signal, [v, ...(value.$effects$ || [])]);
11811222
}

packages/qwik/src/core/shared/shared-serialization.unit.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import { isQrl } from './qrl/qrl-utils';
2626
import { NoSerializeSymbol, SerializerSymbol } from './utils/serialize-utils';
2727
import { SubscriptionData } from '../reactive-primitives/subscription-data';
2828
import { StoreFlags } from '../reactive-primitives/types';
29+
import { createAsyncComputedSignal } from '../reactive-primitives/signal-api';
30+
import { retryOnPromise } from './utils/promises';
2931
import { QError } from './error/error';
3032

3133
const DEBUG = false;
@@ -508,6 +510,55 @@ describe('shared-serialization', () => {
508510
(72 chars)"
509511
`);
510512
});
513+
it(title(TypeIds.AsyncComputedSignal), async () => {
514+
const foo = createSignal(1);
515+
const dirty = createAsyncComputedSignal(
516+
inlinedQrl(
517+
({ track }) => Promise.resolve(track(() => (foo as SignalImpl).value) + 1),
518+
'dirty',
519+
[foo]
520+
)
521+
);
522+
const clean = createAsyncComputedSignal(
523+
inlinedQrl(
524+
({ track }) => Promise.resolve(track(() => (foo as SignalImpl).value) + 1),
525+
'clean',
526+
[foo]
527+
)
528+
);
529+
await retryOnPromise(() => {
530+
// note that this won't subscribe because we're not setting up the context
531+
expect(clean.value).toBe(2);
532+
});
533+
534+
const objs = await serialize(dirty, clean);
535+
expect(dumpState(objs)).toMatchInlineSnapshot(`
536+
"
537+
0 AsyncComputedSignal [
538+
RootRef 2
539+
Constant null
540+
Constant null
541+
Constant null
542+
Constant false
543+
Constant false
544+
]
545+
1 AsyncComputedSignal [
546+
RootRef 3
547+
Constant null
548+
Constant null
549+
Constant null
550+
Constant false
551+
Constant false
552+
Number 2
553+
]
554+
2 PreloadQRL "mock-chunk#dirty[4]"
555+
3 PreloadQRL "mock-chunk#clean[4]"
556+
4 Signal [
557+
Number 1
558+
]
559+
(122 chars)"
560+
`);
561+
});
511562
it(title(TypeIds.Store), async () => {
512563
expect(await dump(createStore(null, { a: { b: true } }, StoreFlags.RECURSIVE)))
513564
.toMatchInlineSnapshot(`

packages/qwik/src/core/use/utils/tracker.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { isSignal } from '../../reactive-primitives/utils';
1111
import { qError, QError } from '../../shared/error/error';
1212
import type { Container } from '../../shared/types';
1313
import { noSerialize } from '../../shared/utils/serialize-utils';
14-
import { isFunction } from '../../shared/utils/types';
14+
import { isFunction, isObject } from '../../shared/utils/types';
1515
import { invoke, newInvokeContext } from '../use-core';
1616
import type { Task, Tracker } from '../use-task';
1717

@@ -29,7 +29,7 @@ export const trackFn =
2929
return (obj as Record<string, unknown>)[prop];
3030
} else if (isSignal(obj)) {
3131
return obj.value;
32-
} else if (isStore(obj)) {
32+
} else if (isObject(obj) && isStore(obj)) {
3333
// track whole store
3434
addStoreEffect(
3535
getStoreTarget(obj)!,

starters/apps/e2e/src/components/async-computed/async-computed.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,13 @@ export const PendingComponent = component$(() => {
5151
new Promise<number>((resolve) => {
5252
setTimeout(() => {
5353
resolve(track(count) * 2);
54-
}, 5000);
54+
}, 1000);
5555
}),
5656
);
5757

5858
return (
5959
<div>
60-
{/* {double.pending ? "pending" : "not pending"} */}
60+
{(double as any).pending ? "pending" : "not pending"}
6161
<div class="result">double: {double.value}</div>
6262
<button id="increment" onClick$={() => count.value++}>
6363
Increment

0 commit comments

Comments
 (0)