Skip to content

Commit aca1de8

Browse files
committed
feat(core): use/createSerialized$
1 parent 059a688 commit aca1de8

File tree

13 files changed

+409
-12
lines changed

13 files changed

+409
-12
lines changed

.changeset/nasty-planes-jam.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': minor
3+
---
4+
5+
FEAT: `useSerialized$` and `createSerialized$`

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,20 @@
221221
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-context.ts",
222222
"mdFile": "core.createcontextid.md"
223223
},
224+
{
225+
"name": "createSerialized$",
226+
"id": "createserialized_",
227+
"hierarchy": [
228+
{
229+
"name": "createSerialized$",
230+
"id": "createserialized_"
231+
}
232+
],
233+
"kind": "Function",
234+
"content": "Create a signal that holds a custom serializable value. See `useSerialized$` for more details.\n\n\n```typescript\ncreateSerialized$: <T extends CustomSerializable<T, S>, S, F extends ConstructorFn<T, S> = ConstructorFn<T, S>>(qrl: F | QRL<F>) => SerializedSignal<T, S, F>\n```\n\n\n<table><thead><tr><th>\n\nParameter\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\nqrl\n\n\n</td><td>\n\nF \\| [QRL](#qrl)<!-- -->&lt;F&gt;\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nSerializedSignal&lt;T, S, F&gt;",
235+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/signal/signal.public.ts",
236+
"mdFile": "core.createserialized_.md"
237+
},
224238
{
225239
"name": "createSignal",
226240
"id": "createsignal",
@@ -2031,6 +2045,20 @@
20312045
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource-dollar.ts",
20322046
"mdFile": "core.useresource_.md"
20332047
},
2048+
{
2049+
"name": "useSerialized$",
2050+
"id": "useserialized_",
2051+
"hierarchy": [
2052+
{
2053+
"name": "useSerialized$",
2054+
"id": "useserialized_"
2055+
}
2056+
],
2057+
"kind": "Variable",
2058+
"content": "Creates a signal which holds a custom serializable value. It requires that the value implements the `CustomSerializable` type, which means having a function under the `[SerializeSymbol]` property that returns a serializable value when called.\n\nThe `fn` you pass is called with the result of the serialization (in the browser, only when the value is needed), or `undefined` when not yet initialized. If you refer to other signals, `fn` will be called when those change just like computed signals, and then the argument will be the previous output, not the serialized result.\n\nThis is useful when using third party libraries that use custom objects that are not serializable.\n\nNote that the `fn` is called lazily, so it won't impact container resume.\n\n\n```typescript\nuseSerialized$: {\n fn: <T extends CustomSerializable<T, S>, S, F extends ConstructorFn<T, S> = ConstructorFn<T, S>>(fn: F | QRL<F>) => T extends Promise<any> ? never : ReadonlySignal<T>;\n}['fn']\n```\n\n\n\n```tsx\nclass MyCustomSerializable {\n constructor(public n: number) {}\n inc() {\n this.n++;\n }\n [SerializeSymbol]() {\n return this.n;\n }\n}\nconst Cmp = component$(() => {\n const custom = useSerialized$<MyCustomSerializable, number>(\n (prev) =>\n new MyCustomSerializable(prev instanceof MyCustomSerializable ? prev : (prev ?? 3))\n );\n return <div onClick$={() => custom.value.inc()}>{custom.value.n}</div>;\n});\n```",
2059+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-serializer.ts",
2060+
"mdFile": "core.useserialized_.md"
2061+
},
20342062
{
20352063
"name": "useServerData",
20362064
"id": "useserverdata",

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,51 @@ The name of the context.
723723

724724
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-context.ts)
725725

726+
## createSerialized$
727+
728+
Create a signal that holds a custom serializable value. See `useSerialized$` for more details.
729+
730+
```typescript
731+
createSerialized$: <
732+
T extends CustomSerializable<T, S>,
733+
S,
734+
F extends ConstructorFn<T, S> = ConstructorFn<T, S>,
735+
>(
736+
qrl: F | QRL<F>,
737+
) => SerializedSignal<T, S, F>;
738+
```
739+
740+
<table><thead><tr><th>
741+
742+
Parameter
743+
744+
</th><th>
745+
746+
Type
747+
748+
</th><th>
749+
750+
Description
751+
752+
</th></tr></thead>
753+
<tbody><tr><td>
754+
755+
qrl
756+
757+
</td><td>
758+
759+
F \| [QRL](#qrl)&lt;F&gt;
760+
761+
</td><td>
762+
763+
</td></tr>
764+
</tbody></table>
765+
**Returns:**
766+
767+
SerializedSignal&lt;T, S, F&gt;
768+
769+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/signal/signal.public.ts)
770+
726771
## createSignal
727772

728773
Creates a Signal with the given value. If no value is given, the signal is created with `undefined`.
@@ -4905,6 +4950,45 @@ _(Optional)_
49054950
49064951
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource-dollar.ts)
49074952
4953+
## useSerialized$
4954+
4955+
Creates a signal which holds a custom serializable value. It requires that the value implements the `CustomSerializable` type, which means having a function under the `[SerializeSymbol]` property that returns a serializable value when called.
4956+
4957+
The `fn` you pass is called with the result of the serialization (in the browser, only when the value is needed), or `undefined` when not yet initialized. If you refer to other signals, `fn` will be called when those change just like computed signals, and then the argument will be the previous output, not the serialized result.
4958+
4959+
This is useful when using third party libraries that use custom objects that are not serializable.
4960+
4961+
Note that the `fn` is called lazily, so it won't impact container resume.
4962+
4963+
```typescript
4964+
useSerialized$: {
4965+
fn: <T extends CustomSerializable<T, S>, S, F extends ConstructorFn<T, S> = ConstructorFn<T, S>>(fn: F | QRL<F>) => T extends Promise<any> ? never : ReadonlySignal<T>;
4966+
}['fn']
4967+
```
4968+
4969+
```tsx
4970+
class MyCustomSerializable {
4971+
constructor(public n: number) {}
4972+
inc() {
4973+
this.n++;
4974+
}
4975+
[SerializeSymbol]() {
4976+
return this.n;
4977+
}
4978+
}
4979+
const Cmp = component$(() => {
4980+
const custom = useSerialized$<MyCustomSerializable, number>(
4981+
(prev) =>
4982+
new MyCustomSerializable(
4983+
prev instanceof MyCustomSerializable ? prev : (prev ?? 3),
4984+
),
4985+
);
4986+
return <div onClick$={() => custom.value.inc()}>{custom.value.n}</div>;
4987+
});
4988+
```
4989+
4990+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-serializer.ts)
4991+
49084992
## useServerData
49094993
49104994
```typescript

packages/qwik/public.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export {
66
ComputedSignal,
77
ContextId,
88
createComputed$,
9+
createSerialized$,
910
createContextId,
1011
createSignal,
1112
CSSProperties,
@@ -60,6 +61,7 @@ export {
6061
useOnDocument,
6162
useOnWindow,
6263
useResource$,
64+
useSerialized$,
6365
useServerData,
6466
useSignal,
6567
useStore,

packages/qwik/src/core/api.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,18 @@ export const createComputedQrl: <T>(qrl: QRL<() => T>) => T extends Promise<any>
119119
// @public
120120
export const createContextId: <STATE = unknown>(name: string) => ContextId<STATE>;
121121

122+
// Warning: (ae-forgotten-export) The symbol "CustomSerializable" needs to be exported by the entry point index.d.ts
123+
// Warning: (ae-forgotten-export) The symbol "ConstructorFn" needs to be exported by the entry point index.d.ts
124+
// Warning: (ae-forgotten-export) The symbol "SerializedSignal" needs to be exported by the entry point index.d.ts
125+
//
126+
// @public
127+
export const createSerialized$: <T extends CustomSerializable<T, S>, S, F extends ConstructorFn<T, S> = ConstructorFn<T, S>>(qrl: F | QRL<F>) => SerializedSignal<T, S, F>;
128+
129+
// Warning: (ae-internal-missing-underscore) The name "createSerializedQrl" should be prefixed with an underscore because the declaration is marked as @internal
130+
//
131+
// @internal (undocumented)
132+
export const createSerializedQrl: <T extends CustomSerializable<T, S>, S, F extends ConstructorFn<T, S> = ConstructorFn<T, S>>(qrl: QRL<F>) => SerializedSignal<T, S, F>;
133+
122134
// @public
123135
export const createSignal: {
124136
<T>(): Signal<T | undefined>;
@@ -1092,6 +1104,16 @@ export const useResource$: <T>(generatorFn: ResourceFn<T>, opts?: ResourceOption
10921104
// @internal (undocumented)
10931105
export const useResourceQrl: <T>(qrl: QRL<ResourceFn<T>>, opts?: ResourceOptions) => ResourceReturn<T>;
10941106

1107+
// @public
1108+
export const useSerialized$: {
1109+
fn: <T extends CustomSerializable<T, S>, S, F extends ConstructorFn<T, S> = ConstructorFn<T, S>>(fn: F | QRL<F>) => T extends Promise<any> ? never : ReadonlySignal<T>;
1110+
}['fn'];
1111+
1112+
// Warning: (ae-internal-missing-underscore) The name "useSerializerQrl" should be prefixed with an underscore because the declaration is marked as @internal
1113+
//
1114+
// @internal (undocumented)
1115+
export const useSerializerQrl: <T extends CustomSerializable<T, S>, S, F extends ConstructorFn<T, S>>(qrl: QRL<F>) => T extends Promise<any> ? never : ReadonlySignal<T>;
1116+
10951117
// @public (undocumented)
10961118
export function useServerData<T>(key: string): T | undefined;
10971119

packages/qwik/src/core/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export type { ContextId } from './use/use-context';
110110
export type { UseStoreOptions } from './use/use-store.public';
111111
export type { ComputedFn } from './use/use-computed';
112112
export { useComputedQrl } from './use/use-computed';
113+
export { useSerializerQrl, useSerialized$ } from './use/use-serializer';
113114
export type { OnVisibleTaskOptions, VisibleTaskStrategy } from './use/use-visible-task';
114115
export { useVisibleTaskQrl } from './use/use-visible-task';
115116
export type { EagernessOptions, TaskCtx, TaskFn, Tracker, UseTaskOptions } from './use/use-task';
@@ -132,7 +133,14 @@ export { useComputed$ } from './use/use-computed-dollar';
132133
export { useErrorBoundary } from './use/use-error-boundary';
133134
export type { ErrorBoundaryStore } from './shared/error/error-handling';
134135
export { type ReadonlySignal, type Signal, type ComputedSignal } from './signal/signal.public';
135-
export { isSignal, createSignal, createComputedQrl, createComputed$ } from './signal/signal.public';
136+
export {
137+
isSignal,
138+
createSignal,
139+
createComputedQrl,
140+
createComputed$,
141+
createSerializedQrl,
142+
createSerialized$,
143+
} from './signal/signal.public';
136144
export { EffectData as _EffectData } from './signal/signal';
137145

138146
//////////////////////////////////////////////////////////////////////////////////////////

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

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@ import { type DomContainer } from '../client/dom-container';
88
import type { VNode } from '../client/types';
99
import { vnode_getNode, vnode_isVNode, vnode_locate, vnode_toString } from '../client/vnode';
1010
import { NEEDS_COMPUTATION } from '../signal/flags';
11-
import { ComputedSignal, EffectData, Signal, WrappedSignal } from '../signal/signal';
11+
import {
12+
ComputedSignal,
13+
EffectData,
14+
SerializedSignal,
15+
Signal,
16+
WrappedSignal,
17+
isSerializerObj,
18+
type EffectSubscriptions,
19+
} from '../signal/signal';
1220
import type { Subscriber } from '../signal/signal-subscriber';
1321
import {
1422
STORE_ARRAY_PROP,
@@ -39,7 +47,7 @@ import {
3947
type QRLInternal,
4048
type SyncQRLInternal,
4149
} from './qrl/qrl-class';
42-
import type { QRL } from './qrl/qrl.public';
50+
import { type QRL } from './qrl/qrl.public';
4351
import { ChoreType } from './scheduler';
4452
import type { DeserializeContainer, HostElement, ObjToProxyMap } from './types';
4553
import { _CONST_PROPS, _VAR_PROPS } from './utils/constants';
@@ -279,13 +287,15 @@ const inflate = (container: DeserializeContainer, target: any, typeId: TypeIds,
279287
signal.$effects$ = d.slice(4);
280288
break;
281289
}
290+
case TypeIds.SerializedSignal:
282291
case TypeIds.ComputedSignal: {
283292
const computed = target as ComputedSignal<unknown>;
284-
const d = data as [QRLInternal<() => {}>, any, unknown?];
293+
const d = data as [QRLInternal<() => {}>, EffectSubscriptions[] | null, unknown?];
285294
computed.$computeQrl$ = d[0];
286295
computed.$effects$ = d[1];
287-
if (d.length === 3) {
296+
if (d.length >= 3) {
288297
computed.$untrackedValue$ = d[2];
298+
computed.$invalid$ = typeId === TypeIds.SerializedSignal;
289299
} else {
290300
computed.$invalid$ = true;
291301
/**
@@ -472,6 +482,8 @@ const allocate = (container: DeserializeContainer, typeId: number, value: unknow
472482
return new WrappedSignal(container as any, null!, null!, null!);
473483
case TypeIds.ComputedSignal:
474484
return new ComputedSignal(container as any, null!);
485+
case TypeIds.SerializedSignal:
486+
return new SerializedSignal(container as any, null!);
475487
case TypeIds.Store:
476488
return createStore(container as any, {}, 0);
477489
case TypeIds.StoreArray:
@@ -869,7 +881,7 @@ export const createSerializationContext = (
869881
discoveredValues.push(obj.data);
870882
} else if (Array.isArray(obj)) {
871883
discoveredValues.push(...obj);
872-
} else if (SerializerSymbol in obj && typeof obj[SerializerSymbol] === 'function') {
884+
} else if (isSerializerObj(obj)) {
873885
const result = obj[SerializerSymbol](obj);
874886
serializationResults.set(obj, result);
875887
discoveredValues.push(result);
@@ -1136,7 +1148,7 @@ function serialize(serializationContext: SerializationContext): void {
11361148
* Special case: when a Signal value is an SSRNode, it always needs to be a DOM ref instead.
11371149
* It can never be meant to become a vNode, because vNodes are internal only.
11381150
*/
1139-
const v =
1151+
const v: unknown =
11401152
value instanceof ComputedSignal &&
11411153
(value.$invalid$ || fastSkipSerialize(value.$untrackedValue$))
11421154
? NEEDS_COMPUTATION
@@ -1150,15 +1162,18 @@ function serialize(serializationContext: SerializationContext): void {
11501162
...(value.$effects$ || []),
11511163
]);
11521164
} else if (value instanceof ComputedSignal) {
1153-
const out = [
1165+
const out: [QRLInternal, EffectSubscriptions[] | null, unknown?] = [
11541166
value.$computeQrl$,
11551167
// TODO check if we can use domVRef for effects
11561168
value.$effects$,
11571169
];
11581170
if (v !== NEEDS_COMPUTATION) {
11591171
out.push(v);
11601172
}
1161-
output(TypeIds.ComputedSignal, out);
1173+
output(
1174+
value instanceof SerializedSignal ? TypeIds.SerializedSignal : TypeIds.ComputedSignal,
1175+
out
1176+
);
11621177
} else {
11631178
output(TypeIds.Signal, [v, ...(value.$effects$ || [])]);
11641179
}
@@ -1596,6 +1611,7 @@ export const enum TypeIds {
15961611
Signal,
15971612
WrappedSignal,
15981613
ComputedSignal,
1614+
SerializedSignal,
15991615
Store,
16001616
StoreArray,
16011617
FormData,
@@ -1629,6 +1645,7 @@ export const _typeIdNames = [
16291645
'Signal',
16301646
'WrappedSignal',
16311647
'ComputedSignal',
1648+
'SerializedSignal',
16321649
'Store',
16331650
'StoreArray',
16341651
'FormData',

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import { $, component$, noSerialize } from '@qwik.dev/core';
22
import { describe, expect, it } from 'vitest';
33
import { _fnSignal, _wrapProp } from '../internal';
44
import { EffectData, type Signal } from '../signal/signal';
5-
import { createComputed$, createSignal, isSignal } from '../signal/signal.public';
5+
import {
6+
createComputed$,
7+
createSerialized$,
8+
createSignal,
9+
isSignal,
10+
} from '../signal/signal.public';
611
import { StoreFlags, createStore } from '../signal/store';
712
import { createResourceReturn } from '../use/use-resource';
813
import { Task } from '../use/use-task';
@@ -403,6 +408,21 @@ describe('shared-serialization', () => {
403408
(186 chars)"
404409
`);
405410
});
411+
it(title(TypeIds.SerializedSignal), async () => {
412+
const custom = createSerialized$<MyCustomSerializable, number>(
413+
(prev) => new MyCustomSerializable(prev as number)
414+
);
415+
const objs = await serialize(custom);
416+
expect(dumpState(objs)).toMatchInlineSnapshot(`
417+
"
418+
0 SerializedSignal [
419+
QRL 1
420+
Constant null
421+
]
422+
1 String "mock-chunk#describe_describe_it_custom_createSerialized_RQFR5EU0bpE"
423+
(87 chars)"
424+
`);
425+
});
406426
it(title(TypeIds.Store), async () => {
407427
expect(await dump(createStore(null, { a: { b: true } }, StoreFlags.RECURSIVE)))
408428
.toMatchInlineSnapshot(`
@@ -608,6 +628,7 @@ describe('shared-serialization', () => {
608628
});
609629
it.todo(title(TypeIds.WrappedSignal));
610630
it.todo(title(TypeIds.ComputedSignal));
631+
it.todo(title(TypeIds.SerializedSignal));
611632
// this requires a domcontainer
612633
it.skip(title(TypeIds.Store), async () => {
613634
const objs = await serialize(createStore(null, { a: { b: true } }, StoreFlags.RECURSIVE));
@@ -851,3 +872,10 @@ async function serialize(...roots: any[]): Promise<any[]> {
851872
DEBUG && console.log(objs);
852873
return objs;
853874
}
875+
876+
class MyCustomSerializable {
877+
constructor(public value: number) {}
878+
[SerializerSymbol]() {
879+
return this.value;
880+
}
881+
}

0 commit comments

Comments
 (0)