Skip to content

Commit

Permalink
feat(core): use/createSerialized$
Browse files Browse the repository at this point in the history
  • Loading branch information
wmertens committed Jan 3, 2025
1 parent 059a688 commit aca1de8
Show file tree
Hide file tree
Showing 13 changed files with 409 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/nasty-planes-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik.dev/core': minor
---

FEAT: `useSerialized$` and `createSerialized$`
28 changes: 28 additions & 0 deletions packages/docs/src/routes/api/qwik/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,20 @@
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-context.ts",
"mdFile": "core.createcontextid.md"
},
{
"name": "createSerialized$",
"id": "createserialized_",
"hierarchy": [
{
"name": "createSerialized$",
"id": "createserialized_"
}
],
"kind": "Function",
"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;",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/signal/signal.public.ts",
"mdFile": "core.createserialized_.md"
},
{
"name": "createSignal",
"id": "createsignal",
Expand Down Expand Up @@ -2031,6 +2045,20 @@
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource-dollar.ts",
"mdFile": "core.useresource_.md"
},
{
"name": "useSerialized$",
"id": "useserialized_",
"hierarchy": [
{
"name": "useSerialized$",
"id": "useserialized_"
}
],
"kind": "Variable",
"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```",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-serializer.ts",
"mdFile": "core.useserialized_.md"
},
{
"name": "useServerData",
"id": "useserverdata",
Expand Down
84 changes: 84 additions & 0 deletions packages/docs/src/routes/api/qwik/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,51 @@ The name of the context.

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

## createSerialized$

Create a signal that holds a custom serializable value. See `useSerialized$` for more details.

```typescript
createSerialized$: <
T extends CustomSerializable<T, S>,
S,
F extends ConstructorFn<T, S> = ConstructorFn<T, S>,
>(
qrl: F | QRL<F>,
) => SerializedSignal<T, S, F>;
```

<table><thead><tr><th>

Parameter

</th><th>

Type

</th><th>

Description

</th></tr></thead>
<tbody><tr><td>

qrl

</td><td>

F \| [QRL](#qrl)&lt;F&gt;

</td><td>

</td></tr>
</tbody></table>
**Returns:**

SerializedSignal&lt;T, S, F&gt;

[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/signal/signal.public.ts)

## createSignal

Creates a Signal with the given value. If no value is given, the signal is created with `undefined`.
Expand Down Expand Up @@ -4905,6 +4950,45 @@ _(Optional)_
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource-dollar.ts)
## useSerialized$
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.
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.
This is useful when using third party libraries that use custom objects that are not serializable.
Note that the `fn` is called lazily, so it won't impact container resume.
```typescript
useSerialized$: {
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>;
}['fn']
```
```tsx
class MyCustomSerializable {
constructor(public n: number) {}
inc() {
this.n++;
}
[SerializeSymbol]() {
return this.n;
}
}
const Cmp = component$(() => {
const custom = useSerialized$<MyCustomSerializable, number>(
(prev) =>
new MyCustomSerializable(
prev instanceof MyCustomSerializable ? prev : (prev ?? 3),
),
);
return <div onClick$={() => custom.value.inc()}>{custom.value.n}</div>;
});
```
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-serializer.ts)
## useServerData
```typescript
Expand Down
2 changes: 2 additions & 0 deletions packages/qwik/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export {
ComputedSignal,
ContextId,
createComputed$,
createSerialized$,
createContextId,
createSignal,
CSSProperties,
Expand Down Expand Up @@ -60,6 +61,7 @@ export {
useOnDocument,
useOnWindow,
useResource$,
useSerialized$,
useServerData,
useSignal,
useStore,
Expand Down
22 changes: 22 additions & 0 deletions packages/qwik/src/core/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,18 @@ export const createComputedQrl: <T>(qrl: QRL<() => T>) => T extends Promise<any>
// @public
export const createContextId: <STATE = unknown>(name: string) => ContextId<STATE>;

// Warning: (ae-forgotten-export) The symbol "CustomSerializable" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "ConstructorFn" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "SerializedSignal" needs to be exported by the entry point index.d.ts
//
// @public
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>;

// Warning: (ae-internal-missing-underscore) The name "createSerializedQrl" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal (undocumented)
export const createSerializedQrl: <T extends CustomSerializable<T, S>, S, F extends ConstructorFn<T, S> = ConstructorFn<T, S>>(qrl: QRL<F>) => SerializedSignal<T, S, F>;

// @public
export const createSignal: {
<T>(): Signal<T | undefined>;
Expand Down Expand Up @@ -1092,6 +1104,16 @@ export const useResource$: <T>(generatorFn: ResourceFn<T>, opts?: ResourceOption
// @internal (undocumented)
export const useResourceQrl: <T>(qrl: QRL<ResourceFn<T>>, opts?: ResourceOptions) => ResourceReturn<T>;

// @public
export const useSerialized$: {
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>;
}['fn'];

// Warning: (ae-internal-missing-underscore) The name "useSerializerQrl" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal (undocumented)
export const useSerializerQrl: <T extends CustomSerializable<T, S>, S, F extends ConstructorFn<T, S>>(qrl: QRL<F>) => T extends Promise<any> ? never : ReadonlySignal<T>;

// @public (undocumented)
export function useServerData<T>(key: string): T | undefined;

Expand Down
10 changes: 9 additions & 1 deletion packages/qwik/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export type { ContextId } from './use/use-context';
export type { UseStoreOptions } from './use/use-store.public';
export type { ComputedFn } from './use/use-computed';
export { useComputedQrl } from './use/use-computed';
export { useSerializerQrl, useSerialized$ } from './use/use-serializer';
export type { OnVisibleTaskOptions, VisibleTaskStrategy } from './use/use-visible-task';
export { useVisibleTaskQrl } from './use/use-visible-task';
export type { EagernessOptions, TaskCtx, TaskFn, Tracker, UseTaskOptions } from './use/use-task';
Expand All @@ -132,7 +133,14 @@ export { useComputed$ } from './use/use-computed-dollar';
export { useErrorBoundary } from './use/use-error-boundary';
export type { ErrorBoundaryStore } from './shared/error/error-handling';
export { type ReadonlySignal, type Signal, type ComputedSignal } from './signal/signal.public';
export { isSignal, createSignal, createComputedQrl, createComputed$ } from './signal/signal.public';
export {
isSignal,
createSignal,
createComputedQrl,
createComputed$,
createSerializedQrl,
createSerialized$,
} from './signal/signal.public';
export { EffectData as _EffectData } from './signal/signal';

//////////////////////////////////////////////////////////////////////////////////////////
Expand Down
33 changes: 25 additions & 8 deletions packages/qwik/src/core/shared/shared-serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ import { type DomContainer } from '../client/dom-container';
import type { VNode } from '../client/types';
import { vnode_getNode, vnode_isVNode, vnode_locate, vnode_toString } from '../client/vnode';
import { NEEDS_COMPUTATION } from '../signal/flags';
import { ComputedSignal, EffectData, Signal, WrappedSignal } from '../signal/signal';
import {
ComputedSignal,
EffectData,
SerializedSignal,
Signal,
WrappedSignal,
isSerializerObj,
type EffectSubscriptions,
} from '../signal/signal';
import type { Subscriber } from '../signal/signal-subscriber';
import {
STORE_ARRAY_PROP,
Expand Down Expand Up @@ -39,7 +47,7 @@ import {
type QRLInternal,
type SyncQRLInternal,
} from './qrl/qrl-class';
import type { QRL } from './qrl/qrl.public';
import { type QRL } from './qrl/qrl.public';
import { ChoreType } from './scheduler';
import type { DeserializeContainer, HostElement, ObjToProxyMap } from './types';
import { _CONST_PROPS, _VAR_PROPS } from './utils/constants';
Expand Down Expand Up @@ -279,13 +287,15 @@ const inflate = (container: DeserializeContainer, target: any, typeId: TypeIds,
signal.$effects$ = d.slice(4);
break;
}
case TypeIds.SerializedSignal:
case TypeIds.ComputedSignal: {
const computed = target as ComputedSignal<unknown>;
const d = data as [QRLInternal<() => {}>, any, unknown?];
const d = data as [QRLInternal<() => {}>, EffectSubscriptions[] | null, unknown?];
computed.$computeQrl$ = d[0];
computed.$effects$ = d[1];
if (d.length === 3) {
if (d.length >= 3) {
computed.$untrackedValue$ = d[2];
computed.$invalid$ = typeId === TypeIds.SerializedSignal;
} else {
computed.$invalid$ = true;
/**
Expand Down Expand Up @@ -472,6 +482,8 @@ const allocate = (container: DeserializeContainer, typeId: number, value: unknow
return new WrappedSignal(container as any, null!, null!, null!);
case TypeIds.ComputedSignal:
return new ComputedSignal(container as any, null!);
case TypeIds.SerializedSignal:
return new SerializedSignal(container as any, null!);
case TypeIds.Store:
return createStore(container as any, {}, 0);
case TypeIds.StoreArray:
Expand Down Expand Up @@ -869,7 +881,7 @@ export const createSerializationContext = (
discoveredValues.push(obj.data);
} else if (Array.isArray(obj)) {
discoveredValues.push(...obj);
} else if (SerializerSymbol in obj && typeof obj[SerializerSymbol] === 'function') {
} else if (isSerializerObj(obj)) {
const result = obj[SerializerSymbol](obj);
serializationResults.set(obj, result);
discoveredValues.push(result);
Expand Down Expand Up @@ -1136,7 +1148,7 @@ function serialize(serializationContext: SerializationContext): void {
* Special case: when a Signal value is an SSRNode, it always needs to be a DOM ref instead.
* It can never be meant to become a vNode, because vNodes are internal only.
*/
const v =
const v: unknown =
value instanceof ComputedSignal &&
(value.$invalid$ || fastSkipSerialize(value.$untrackedValue$))
? NEEDS_COMPUTATION
Expand All @@ -1150,15 +1162,18 @@ function serialize(serializationContext: SerializationContext): void {
...(value.$effects$ || []),
]);
} else if (value instanceof ComputedSignal) {
const out = [
const out: [QRLInternal, EffectSubscriptions[] | null, unknown?] = [
value.$computeQrl$,
// TODO check if we can use domVRef for effects
value.$effects$,
];
if (v !== NEEDS_COMPUTATION) {
out.push(v);
}
output(TypeIds.ComputedSignal, out);
output(
value instanceof SerializedSignal ? TypeIds.SerializedSignal : TypeIds.ComputedSignal,
out
);
} else {
output(TypeIds.Signal, [v, ...(value.$effects$ || [])]);
}
Expand Down Expand Up @@ -1596,6 +1611,7 @@ export const enum TypeIds {
Signal,
WrappedSignal,
ComputedSignal,
SerializedSignal,
Store,
StoreArray,
FormData,
Expand Down Expand Up @@ -1629,6 +1645,7 @@ export const _typeIdNames = [
'Signal',
'WrappedSignal',
'ComputedSignal',
'SerializedSignal',
'Store',
'StoreArray',
'FormData',
Expand Down
30 changes: 29 additions & 1 deletion packages/qwik/src/core/shared/shared-serialization.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { $, component$, noSerialize } from '@qwik.dev/core';
import { describe, expect, it } from 'vitest';
import { _fnSignal, _wrapProp } from '../internal';
import { EffectData, type Signal } from '../signal/signal';
import { createComputed$, createSignal, isSignal } from '../signal/signal.public';
import {
createComputed$,
createSerialized$,
createSignal,
isSignal,
} from '../signal/signal.public';
import { StoreFlags, createStore } from '../signal/store';
import { createResourceReturn } from '../use/use-resource';
import { Task } from '../use/use-task';
Expand Down Expand Up @@ -403,6 +408,21 @@ describe('shared-serialization', () => {
(186 chars)"
`);
});
it(title(TypeIds.SerializedSignal), async () => {
const custom = createSerialized$<MyCustomSerializable, number>(
(prev) => new MyCustomSerializable(prev as number)
);
const objs = await serialize(custom);
expect(dumpState(objs)).toMatchInlineSnapshot(`
"
0 SerializedSignal [
QRL 1
Constant null
]
1 String "mock-chunk#describe_describe_it_custom_createSerialized_RQFR5EU0bpE"
(87 chars)"
`);
});
it(title(TypeIds.Store), async () => {
expect(await dump(createStore(null, { a: { b: true } }, StoreFlags.RECURSIVE)))
.toMatchInlineSnapshot(`
Expand Down Expand Up @@ -608,6 +628,7 @@ describe('shared-serialization', () => {
});
it.todo(title(TypeIds.WrappedSignal));
it.todo(title(TypeIds.ComputedSignal));
it.todo(title(TypeIds.SerializedSignal));
// this requires a domcontainer
it.skip(title(TypeIds.Store), async () => {
const objs = await serialize(createStore(null, { a: { b: true } }, StoreFlags.RECURSIVE));
Expand Down Expand Up @@ -851,3 +872,10 @@ async function serialize(...roots: any[]): Promise<any[]> {
DEBUG && console.log(objs);
return objs;
}

class MyCustomSerializable {
constructor(public value: number) {}
[SerializerSymbol]() {
return this.value;
}
}
Loading

0 comments on commit aca1de8

Please sign in to comment.