Skip to content

Commit 165ac6d

Browse files
committed
wip
works but needs cleanup and rebasing on the _hW branch once that's stable
1 parent fd6d03d commit 165ac6d

File tree

11 files changed

+237
-91
lines changed

11 files changed

+237
-91
lines changed

packages/qwik/src/core/shared/qrl/qrl-class.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -140,14 +140,19 @@ export const createQRL = <TYPE>(
140140
return fn;
141141
}
142142
return function (this: unknown, ...args: QrlArgs<TYPE>) {
143-
let context = tryGetInvokeContext();
144-
if (context) {
145-
return fn.apply(this, args);
146-
}
147-
context = newInvokeContext();
143+
const context = tryGetInvokeContext() || newInvokeContext();
144+
const prevQrl = context.$qrl$;
145+
const prevEvent = context.$event$;
146+
// used by useLexicalScope
148147
context.$qrl$ = qrl;
149-
context.$event$ = this as Event;
150-
return invoke.call(this, context, fn as any, ...args);
148+
// TODO possibly remove this, are we using it?
149+
context.$event$ ||= this as Event;
150+
try {
151+
return invoke.call(this, context, fn as any, ...args);
152+
} finally {
153+
context.$qrl$ = prevQrl;
154+
context.$event$ = prevEvent;
155+
}
151156
} as TYPE;
152157
};
153158

packages/qwik/src/core/shared/serialization.md

+14
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,17 @@ To avoid blocking the main thread on wake, we lazily restore the roots, with cac
3737
The serialized text is first parsed to get an array of encoded root data.
3838

3939
Then, a proxy gets the raw data and returns an array that deserializes properties on demand and caches them. Objects are also lazily restored.
40+
41+
## Out-of-order streaming
42+
43+
when we allow out-of-order streaming but we want interactivity while the page is still streaming, we can't scan the state once for cycles/Promises/references, because later state might refer to earlier state.
44+
45+
Therefore we must make the serializer single-pass. To do this, we could store each object as a root, but that will make references take more bytes. Instead, we could implement sub-paths for references, and when we encounter a reference, we output that instead.
46+
47+
For later references it would be best to have a root reference, so we'd probably add a root for the sub-reference and later we can refer to that.
48+
49+
We'll still need to keep track of Promises. We can do this by waiting, but that halts streaming. Instead, we could write a forward reference id, and when the promise is resolved, we store the result as the next root item. At the end of the stream we can emit a mapping from forward references to root index.
50+
51+
Then later, when we send out-of-order state, it will append to the existing state on the client.
52+
53+
This single-pass approach might just be better in general, because several bugs were found due to differences between the first and second pass code.

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

+32-7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
WrappedSignal,
1717
isSerializerObj,
1818
type EffectSubscriptions,
19+
type SerializedArg,
1920
} from '../signal/signal';
2021
import type { Subscriber } from '../signal/signal-subscriber';
2122
import {
@@ -291,6 +292,7 @@ const inflate = (
291292
signal.$effects$ = d.slice(5);
292293
break;
293294
}
295+
// Inflating a SerializedSignal is the same as inflating a ComputedSignal
294296
case TypeIds.SerializedSignal:
295297
case TypeIds.ComputedSignal: {
296298
const computed = target as ComputedSignal<unknown>;
@@ -840,11 +842,31 @@ export const createSerializationContext = (
840842
const v =
841843
obj instanceof WrappedSignal
842844
? obj.untrackedValue
843-
: obj instanceof ComputedSignal && (obj.$invalid$ || fastSkipSerialize(obj))
845+
: obj instanceof ComputedSignal &&
846+
!(obj instanceof SerializedSignal) &&
847+
(obj.$invalid$ || fastSkipSerialize(obj))
844848
? NEEDS_COMPUTATION
845849
: obj.$untrackedValue$;
846850
if (v !== NEEDS_COMPUTATION) {
847-
discoveredValues.push(v);
851+
if (obj instanceof SerializedSignal) {
852+
promises.push(
853+
(obj.$computeQrl$ as any as QRLInternal<SerializedArg<any, any>>)
854+
.resolve()
855+
.then((arg) => {
856+
let data;
857+
if (arg.serialize) {
858+
data = arg.serialize(v);
859+
}
860+
if (data === undefined) {
861+
data = NEEDS_COMPUTATION;
862+
}
863+
serializationResults.set(obj, data);
864+
discoveredValues.push(data);
865+
})
866+
);
867+
} else {
868+
discoveredValues.push(v);
869+
}
848870
}
849871
if (obj.$effects$) {
850872
discoveredValues.push(...obj.$effects$);
@@ -1188,7 +1210,9 @@ function serialize(serializationContext: SerializationContext): void {
11881210
* Special case: when a Signal value is an SSRNode, it always needs to be a DOM ref instead.
11891211
* It can never be meant to become a vNode, because vNodes are internal only.
11901212
*/
1213+
const isSerialized = value instanceof SerializedSignal;
11911214
const v: unknown =
1215+
!isSerialized &&
11921216
value instanceof ComputedSignal &&
11931217
(value.$invalid$ || fastSkipSerialize(value.$untrackedValue$))
11941218
? NEEDS_COMPUTATION
@@ -1209,12 +1233,13 @@ function serialize(serializationContext: SerializationContext): void {
12091233
value.$effects$,
12101234
];
12111235
if (v !== NEEDS_COMPUTATION) {
1212-
out.push(v);
1236+
if (isSerialized) {
1237+
out.push(serializationResults.get(value));
1238+
} else {
1239+
out.push(v);
1240+
}
12131241
}
1214-
output(
1215-
value instanceof SerializedSignal ? TypeIds.SerializedSignal : TypeIds.ComputedSignal,
1216-
out
1217-
);
1242+
output(isSerialized ? TypeIds.SerializedSignal : TypeIds.ComputedSignal, out);
12181243
} else {
12191244
output(TypeIds.Signal, [v, ...(value.$effects$ || [])]);
12201245
}

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -411,9 +411,10 @@ describe('shared-serialization', () => {
411411
`);
412412
});
413413
it(title(TypeIds.SerializedSignal), async () => {
414-
const custom = createSerialized$<MyCustomSerializable, number>(
415-
(prev) => new MyCustomSerializable((prev as number) || 3)
416-
);
414+
const custom = createSerialized$({
415+
deserialize: (n?: number) => new MyCustomSerializable(n || 3),
416+
serialize: (obj) => obj.n,
417+
});
417418
// Force the value to be created
418419
custom.value.inc();
419420
const objs = await serialize(custom);

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

+9-4
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import {
55
SerializedSignal,
66
Signal as SignalImpl,
77
throwIfQRLNotResolved,
8-
type ConstructorFn,
98
type CustomSerializable,
9+
type SerializedArg,
1010
} from './signal';
1111
import type { Signal } from './signal.public';
1212

@@ -23,8 +23,13 @@ export const createComputedSignal = <T>(qrl: QRL<() => T>): ComputedSignal<T> =>
2323

2424
/** @internal */
2525
export const createSerializedSignal = <T extends CustomSerializable<T, S>, S>(
26-
qrl: QRL<ConstructorFn<T, S>>
26+
// We want to also add T as a possible parameter type, but that breaks type inference
27+
arg: QRL<{
28+
serialize: (data: S | undefined) => T;
29+
deserialize: (data: T) => S;
30+
initial?: S;
31+
}>
2732
) => {
28-
throwIfQRLNotResolved(qrl);
29-
return new SerializedSignal<T>(null, qrl);
33+
throwIfQRLNotResolved(arg);
34+
return new SerializedSignal<T, S>(null, arg as any as QRLInternal<SerializedArg<T, S>>);
3035
};

packages/qwik/src/core/signal/signal.public.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { implicit$FirstArg } from '../shared/qrl/implicit_dollar';
22
import { SerializerSymbol } from '../shared/utils/serialize-utils';
3-
import type { CustomSerializable } from './signal';
3+
import type { CustomSerializable, SerializedArg } from './signal';
44
import {
55
createSignal as _createSignal,
66
createComputedSignal as createComputedQrl,
@@ -88,13 +88,9 @@ export { createComputedQrl };
8888
*
8989
* @public
9090
*/
91-
export const createSerialized$: <
92-
T extends CustomSerializable<any, S>,
93-
S = T extends { [SerializerSymbol]: (obj: any) => infer U } ? U : unknown,
94-
>(
91+
export const createSerialized$: <T, S>(
9592
// We want to also add T as a possible parameter type, but that breaks type inference
96-
// The
97-
qrl: (data: S | undefined) => T
93+
arg: SerializedArg<T, S>
9894
) => T extends Promise<any> ? never : SerializedSignal<T> = implicit$FirstArg(
9995
createSerializedQrl as any
10096
);

packages/qwik/src/core/signal/signal.ts

+84-32
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,28 @@
1111
* - It needs to store a function which needs to re-run.
1212
* - It is `Readonly` because it is computed.
1313
*/
14+
import type { VNode } from '../client/types';
15+
import { vnode_getProp, vnode_isTextVNode, vnode_isVNode, vnode_setProp } from '../client/vnode';
1416
import { pad, qwikDebugToString } from '../debug';
17+
import type { OnRenderFn } from '../shared/component.public';
1518
import { assertDefined, assertFalse, assertTrue } from '../shared/error/assert';
19+
import { QError, qError } from '../shared/error/error';
20+
import type { Props } from '../shared/jsx/jsx-runtime';
1621
import { type QRLInternal } from '../shared/qrl/qrl-class';
1722
import type { QRL } from '../shared/qrl/qrl.public';
18-
import { trackSignal, tryGetInvokeContext } from '../use/use-core';
19-
import { Task, TaskFlags, isTask } from '../use/use-task';
23+
import { ChoreType, type NodePropData, type NodePropPayload } from '../shared/scheduler';
24+
import type { Container, HostElement } from '../shared/types';
2025
import { ELEMENT_PROPS, OnRenderProp, QSubscribers } from '../shared/utils/markers';
2126
import { isPromise } from '../shared/utils/promises';
2227
import { qDev } from '../shared/utils/qdev';
23-
import type { VNode } from '../client/types';
24-
import { vnode_getProp, vnode_isTextVNode, vnode_isVNode, vnode_setProp } from '../client/vnode';
25-
import { ChoreType, type NodePropData, type NodePropPayload } from '../shared/scheduler';
26-
import type { Container, HostElement } from '../shared/types';
28+
import { SerializerSymbol } from '../shared/utils/serialize-utils';
2729
import type { ISsrNode } from '../ssr/ssr-types';
30+
import { trackSignal, tryGetInvokeContext } from '../use/use-core';
31+
import { Task, TaskFlags, isTask } from '../use/use-task';
32+
import { NEEDS_COMPUTATION } from './flags';
33+
import { Subscriber, isSubscriber } from './signal-subscriber';
2834
import type { Signal as ISignal, ReadonlySignal } from './signal.public';
2935
import type { TargetType } from './store';
30-
import { isSubscriber, Subscriber } from './signal-subscriber';
31-
import type { Props } from '../shared/jsx/jsx-runtime';
32-
import type { OnRenderFn } from '../shared/component.public';
33-
import { NEEDS_COMPUTATION } from './flags';
34-
import { QError, qError } from '../shared/error/error';
35-
import { SerializerSymbol } from '../shared/utils/serialize-utils';
3636

3737
const DEBUG = false;
3838

@@ -374,7 +374,7 @@ export const triggerEffects = (
374374
DEBUG && log('done scheduling');
375375
};
376376

377-
type ComputeQRL<T> = QRLInternal<(prev: T | undefined) => T>;
377+
type ComputeQRL<T> = QRLInternal<() => T>;
378378

379379
/**
380380
* A signal which is computed from other signals.
@@ -415,6 +415,7 @@ export class ComputedSignal<T> extends Signal<T> {
415415
*/
416416
force() {
417417
this.$invalid$ = true;
418+
// TODO shouldn't force be set to true, invalid left alone and the effects scheduled?
418419
this.$forceRunEffects$ = false;
419420
triggerEffects(this.$container$, this, this.$effects$);
420421
}
@@ -439,9 +440,7 @@ export class ComputedSignal<T> extends Signal<T> {
439440
const previousEffectSubscription = ctx?.$effectSubscriber$;
440441
ctx && (ctx.$effectSubscriber$ = [this, EffectProperty.VNODE]);
441442
try {
442-
const untrackedValue = computeQrl.getFn(ctx)(
443-
this.$untrackedValue$ === NEEDS_COMPUTATION ? undefined : this.$untrackedValue$
444-
) as T;
443+
const untrackedValue = computeQrl.getFn(ctx)() as T;
445444
if (isPromise(untrackedValue)) {
446445
throw qError(QError.computedNotSync, [
447446
computeQrl.dev ? computeQrl.dev.file : '',
@@ -554,37 +553,90 @@ export class WrappedSignal<T> extends Signal<T> implements Subscriber {
554553
}
555554
}
556555

557-
export type CustomSerializable<T extends { [SerializerSymbol]: (obj: any) => any }, S> = {
558-
[SerializerSymbol]: (obj: T) => S;
559-
};
560556
/**
561-
* Called with serialized data to reconstruct an object. If it uses signals or stores, it will be
562-
* called when these change, and then the argument will be the previously constructed object.
563-
*
564-
* The constructed object should provide a `[SerializerSymbol]` method which provides the serialized
565-
* data.
557+
* Serialize and deserialize custom objects.
566558
*
567-
* This function may not return a promise.
559+
* If you pass a function, it will be used as the `deserialize` function.
568560
*
569561
* @public
570562
*/
571-
export type ConstructorFn<
572-
T extends CustomSerializable<T, any>,
573-
S = ReturnType<T[typeof SerializerSymbol]>,
574-
> = ((data: S | undefined) => T) | ((data: S | undefined | T) => T);
563+
export type SerializedArg<T, S> =
564+
| {
565+
/**
566+
* This function will be called with serialized data to reconstruct an object.
567+
*
568+
* If it is created for the first time, it will get the `initial` data or `undefined`.
569+
*
570+
* If it uses signals or stores, it will be called when these change, and then the second
571+
* argument will be the previously constructed object.
572+
*
573+
* This function must not return a promise.
574+
*/
575+
deserialize: (data: S | undefined, previous: T | undefined) => T;
576+
/**
577+
* This function will be called with the custom object to get the serialized data. You can
578+
* return a promise if you need to do async work.
579+
*
580+
* The result may be anything that Qwik can serialize.
581+
*
582+
* If you do not provide it, the object will be serialized as `undefined`.
583+
*/
584+
serialize?: (customObject: T) => S | Promise<S>;
585+
/** The initial value to use when deserializing. */
586+
initial?: S;
587+
}
588+
| ((data: S | undefined, previous: T | undefined) => T);
575589

576590
/**
577591
* A signal which provides a non-serializable value. It works like a computed signal, but it is
578592
* handled slightly differently during serdes.
579593
*
580594
* @public
581595
*/
582-
export class SerializedSignal<T extends CustomSerializable<T, any>> extends ComputedSignal<T> {
583-
constructor(container: Container | null, fn: QRL<ConstructorFn<T>>) {
584-
super(container, fn as unknown as ComputeQRL<T>);
596+
export class SerializedSignal<T, S> extends ComputedSignal<T> {
597+
constructor(container: Container | null, argQrl: QRLInternal<SerializedArg<T, S>>) {
598+
super(container, argQrl as unknown as ComputeQRL<T>);
599+
}
600+
$didInitialize$: boolean = false;
601+
602+
$computeIfNeeded$(): boolean {
603+
if (!this.$invalid$) {
604+
return false;
605+
}
606+
throwIfQRLNotResolved(this.$computeQrl$);
607+
const arg = (this.$computeQrl$ as any as QRLInternal<SerializedArg<T, S>>).resolved!;
608+
let deserialize, initial;
609+
if (typeof arg === 'function') {
610+
deserialize = arg;
611+
} else {
612+
deserialize = arg.deserialize;
613+
initial = arg.initial;
614+
}
615+
const currentValue =
616+
this.$untrackedValue$ === NEEDS_COMPUTATION ? initial : this.$untrackedValue$;
617+
const untrackedValue = trackSignal(
618+
() =>
619+
this.$didInitialize$
620+
? deserialize(undefined, currentValue as T)
621+
: deserialize(currentValue as S, undefined),
622+
this,
623+
EffectProperty.VNODE,
624+
this.$container$!
625+
);
626+
DEBUG && log('SerializedSignal.$compute$', untrackedValue);
627+
this.$invalid$ = false;
628+
const didChange = untrackedValue !== this.$untrackedValue$;
629+
if (didChange) {
630+
this.$untrackedValue$ = untrackedValue;
631+
}
632+
return didChange;
585633
}
586634
}
587635

636+
// TODO move to serializer
637+
export type CustomSerializable<T extends { [SerializerSymbol]: (obj: any) => any }, S> = {
638+
[SerializerSymbol]: (obj: T) => S;
639+
};
588640
/** @internal */
589641
export const isSerializerObj = <T extends { [SerializerSymbol]: (obj: any) => any }, S>(
590642
obj: unknown

0 commit comments

Comments
 (0)