Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: custom serialization #7223

Draft
wants to merge 5 commits into
base: build/v2
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/nasty-planes-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@qwik.dev/core': minor
---

FEAT: `useSerialized$(fn)` and `createSerialized$(fn)` allow serializing custom objects. You must provide a
function that converts the custom object to a serializable one via the `[SerializerSymbol]`
property, and then provide `use|createSerialized$(fn)` with the function that creates the custom object
from the serialized data. This will lazily create the value when needed. Note that the serializer
function may return a Promise, which will be awaited. The deserializer must not return a Promise.
8 changes: 8 additions & 0 deletions .changeset/tricky-peaches-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@qwik.dev/core': minor
---

FEAT: `NoSerializeSymbol`: objects that have this defined will not be serialized

FEAT: `SerializerSymbol`: objects that have this defined as a function will get it called with the object as a parameter during serialization. The function should return the data that should be serialized.
Use this to remove cached data, consolidate things etc.
62 changes: 59 additions & 3 deletions packages/docs/src/routes/api/qwik/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@
}
],
"kind": "Function",
"content": "Create a computed signal which is calculated from the given QRL. A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated.\n\nThe QRL must be a function which returns the value of the signal. The function must not have side effects, and it mus be synchronous.\n\nIf you need the function to be async, use `useSignal` and `useTask$` instead.\n\n\n```typescript\ncreateComputed$: <T>(qrl: () => T) => T extends Promise<any> ? never : ComputedSignal<T>\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\n() =&gt; T\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nT extends Promise&lt;any&gt; ? never : [ComputedSignal](#computedsignal)<!-- -->&lt;T&gt;",
"content": "Create a computed signal which is calculated from the given QRL. A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated.\n\nThe QRL must be a function which returns the value of the signal. The function must not have side effects, and it must be synchronous.\n\nIf you need the function to be async, use `useSignal` and `useTask$` instead.\n\n\n```typescript\ncreateComputed$: <T>(qrl: () => T) => T extends Promise<any> ? never : ComputedSignal<T>\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\n() =&gt; T\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nT extends Promise&lt;any&gt; ? never : [ComputedSignal](#computedsignal)<!-- -->&lt;T&gt;",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/signal/signal.public.ts",
"mdFile": "core.createcomputed_.md"
},
Expand All @@ -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$](#useserialized_) for more details.\n\n\n```typescript\ncreateSerialized$: <T extends CustomSerializable<any, S>, S = T extends {\n [SerializerSymbol]: (obj: any) => infer U;\n} ? U : unknown>(qrl: (data: S | undefined) => T) => T extends Promise<any> ? never : SerializedSignal<T>\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\n(data: S \\| undefined) =&gt; T\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nT extends Promise&lt;any&gt; ? never : SerializedSignal&lt;T&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 @@ -841,6 +855,20 @@
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/utils/serialize-utils.ts",
"mdFile": "core.noserialize.md"
},
{
"name": "NoSerializeSymbol",
"id": "noserializesymbol",
"hierarchy": [
{
"name": "NoSerializeSymbol",
"id": "noserializesymbol"
}
],
"kind": "Variable",
"content": "If an object has this property, it will not be serialized\n\n\n```typescript\nNoSerializeSymbol: unique symbol\n```",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/utils/serialize-utils.ts",
"mdFile": "core.noserializesymbol.md"
},
{
"name": "OnRenderFn",
"id": "onrenderfn",
Expand Down Expand Up @@ -1541,6 +1569,20 @@
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource.ts",
"mdFile": "core.resourcereturn.md"
},
{
"name": "SerializerSymbol",
"id": "serializersymbol",
"hierarchy": [
{
"name": "SerializerSymbol",
"id": "serializersymbol"
}
],
"kind": "Variable",
"content": "If an object has this property as a function, it will be called with the object and should return a serializable value.\n\nThis can be used to clean up etc.\n\n\n```typescript\nSerializerSymbol: unique symbol\n```",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/utils/serialize-utils.ts",
"mdFile": "core.serializersymbol.md"
},
{
"name": "setPlatform",
"id": "setplatform",
Expand Down Expand Up @@ -1873,8 +1915,8 @@
}
],
"kind": "Function",
"content": "Creates a computed signal which is calculated from the given function. A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated, and if the result changed, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered.\n\nThe function must be synchronous and must not have any side effects.\n\n\n```typescript\nuseComputed$: <T>(qrl: import(\"./use-computed\").ComputedFn<T>) => T extends Promise<any> ? never : import(\"..\").ReadonlySignal<T>\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\nimport(\"./use-computed\").[ComputedFn](#computedfn)<!-- -->&lt;T&gt;\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nT extends Promise&lt;any&gt; ? never : import(\"..\").[ReadonlySignal](#readonlysignal)<!-- -->&lt;T&gt;",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-computed-dollar.ts",
"content": "Creates a computed signal which is calculated from the given function. A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated, and if the result changed, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered.\n\nThe function must be synchronous and must not have any side effects.\n\n\n```typescript\nuseComputed$: <T>(qrl: ComputedFn<T>) => T extends Promise<any> ? never : ReadonlySignal<T>\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\n[ComputedFn](#computedfn)<!-- -->&lt;T&gt;\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nT extends Promise&lt;any&gt; ? never : [ReadonlySignal](#readonlysignal)<!-- -->&lt;T&gt;",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-computed.ts",
"mdFile": "core.usecomputed_.md"
},
{
Expand Down Expand Up @@ -2003,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$: typeof createSerialized$\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-serialized.ts",
"mdFile": "core.useserialized_.md"
},
{
"name": "useServerData",
"id": "useserverdata",
Expand Down
110 changes: 105 additions & 5 deletions packages/docs/src/routes/api/qwik/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ Description

Create a computed signal which is calculated from the given QRL. A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated.

The QRL must be a function which returns the value of the signal. The function must not have side effects, and it mus be synchronous.
The QRL must be a function which returns the value of the signal. The function must not have side effects, and it must be synchronous.

If you need the function to be async, use `useSignal` and `useTask$` instead.

Expand Down Expand Up @@ -723,6 +723,47 @@ 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$](#useserialized_) for more details.

```typescript
createSerialized$: <T extends CustomSerializable<any, S>, S = T extends {
[SerializerSymbol]: (obj: any) => infer U;
} ? U : unknown>(qrl: (data: S | undefined) => T) => T extends Promise<any> ? never : SerializedSignal<T>
```

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

Parameter

</th><th>

Type

</th><th>

Description

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

qrl

</td><td>

(data: S \| undefined) =&gt; T

</td><td>

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

T extends Promise&lt;any&gt; ? never : SerializedSignal&lt;T&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 @@ -1855,6 +1896,16 @@ export type NoSerialize<T> =

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

## NoSerializeSymbol

If an object has this property, it will not be serialized

```typescript
NoSerializeSymbol: unique symbol
```

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

## OnRenderFn

```typescript
Expand Down Expand Up @@ -3549,6 +3600,18 @@ export type ResourceReturn<T> =

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

## SerializerSymbol

If an object has this property as a function, it will be called with the object and should return a serializable value.

This can be used to clean up etc.

```typescript
SerializerSymbol: unique symbol
```

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

## setPlatform

Sets the `CorePlatform`.
Expand Down Expand Up @@ -4394,7 +4457,7 @@ Creates a computed signal which is calculated from the given function. A compute
The function must be synchronous and must not have any side effects.

```typescript
useComputed$: <T>(qrl: import("./use-computed").ComputedFn<T>) => T extends Promise<any> ? never : import("..").ReadonlySignal<T>
useComputed$: <T>(qrl: ComputedFn<T>) => T extends Promise<any> ? never : ReadonlySignal<T>
```

<table><thead><tr><th>
Expand All @@ -4416,17 +4479,17 @@ qrl

</td><td>

import("./use-computed").[ComputedFn](#computedfn)&lt;T&gt;
[ComputedFn](#computedfn)&lt;T&gt;

</td><td>

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

T extends Promise&lt;any&gt; ? never : import("..").[ReadonlySignal](#readonlysignal)&lt;T&gt;
T extends Promise&lt;any&gt; ? never : [ReadonlySignal](#readonlysignal)&lt;T&gt;

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

## useConstant

Expand Down Expand Up @@ -4883,6 +4946,43 @@ _(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$: typeof createSerialized$;
```

```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-serialized.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
32 changes: 31 additions & 1 deletion packages/qwik/src/core/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,30 @@ export interface CorrectedToggleEvent extends Event {
// @public
export const createComputed$: <T>(qrl: () => T) => T extends Promise<any> ? never : ComputedSignal<T>;

// Warning: (ae-forgotten-export) The symbol "ComputedSignal_2" needs to be exported by the entry point index.d.ts
// Warning: (ae-internal-missing-underscore) The name "createComputedQrl" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal (undocumented)
export const createComputedQrl: <T>(qrl: QRL<() => T>) => T extends Promise<any> ? never : ComputedSignal<T>;
export const createComputedQrl: <T>(qrl: QRL<() => T>) => ComputedSignal_2<T>;

// @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 "SerializedSignal" needs to be exported by the entry point index.d.ts
//
// @public
export const createSerialized$: <T extends CustomSerializable<any, S>, S = T extends {
[SerializerSymbol]: (obj: any) => infer U;
} ? U : unknown>(qrl: (data: S | undefined) => T) => T extends Promise<any> ? never : SerializedSignal<T>;

// 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_2" needs to be exported by the entry point index.d.ts
// 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>(qrl: QRL<ConstructorFn<T, S>>) => SerializedSignal_2<T>;

// @public
export const createSignal: {
<T>(): Signal<T | undefined>;
Expand Down Expand Up @@ -498,6 +514,9 @@ export type NoSerialize<T> = (T & {
// @public
export const noSerialize: <T extends object | undefined>(input: T) => NoSerialize<T>;

// @public
export const NoSerializeSymbol: unique symbol;

// @public (undocumented)
export type OnRenderFn<PROPS> = (props: PROPS) => JSXOutput;

Expand Down Expand Up @@ -811,6 +830,9 @@ export const _restProps: (props: Record<string, any>, omit: string[], target?: {
// @internal
export function _serialize(data: unknown[]): Promise<string>;

// @public
export const SerializerSymbol: unique symbol;

// @public
export const setPlatform: (plt: CorePlatform) => CorePlatform;

Expand Down Expand Up @@ -1086,6 +1108,14 @@ 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$: typeof createSerialized$;

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

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

Expand Down
Loading
Loading