Skip to content

Commit 993a81f

Browse files
committed
chore: change Serializer$ API
- only object configuration - use function for allowing scope injection - fix force()
1 parent 0c19276 commit 993a81f

File tree

9 files changed

+140
-101
lines changed

9 files changed

+140
-101
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2242,8 +2242,8 @@
22422242
}
22432243
],
22442244
"kind": "Variable",
2245-
"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\nuseSerializer$: typeof createSerializer$\n```\n\n\n\n```tsx\nclass MyCustomSerializable {\n constructor(public n: number) {}\n inc() {\n this.n++;\n }\n}\nconst Cmp = component$(() => {\n const custom = useSerializer$({\n deserialize: (data) => new MyCustomSerializable(data),\n serialize: (data) => data.n,\n initial: 2,\n });\n return <div onClick$={() => custom.value.inc()}>{custom.value.n}</div>;\n});\n```\n\n\nWhen using a Signal as the data to create the object, you may not need `serialize`<!-- -->. Furthermore, when the signal is updated, the serializer will be updated as well, and the current object will be passed as the second argument.\n\n```tsx\nconst Cmp = component$(() => {\n const n = useSignal(2);\n const custom = useSerializer$((_data, current) => {\n if (current) {\n current.n = n.value;\n return current;\n }\n return new MyCustomSerializable(n.value);\n});\n return <div onClick$={() => n.value++}>{custom.value.n}</div>;\n});\n```\n(note that in this example, the `{custom.value.n}` is not reactive, so the div text will not update)",
2246-
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-serialized.ts",
2245+
"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\nuseSerializer$: typeof createSerializer$\n```\n\n\n\n```tsx\nclass MyCustomSerializable {\n constructor(public n: number) {}\n inc() {\n this.n++;\n }\n}\nconst Cmp = component$(() => {\n const custom = useSerializer$({\n deserialize: (data) => new MyCustomSerializable(data),\n serialize: (data) => data.n,\n initial: 2,\n });\n return <div onClick$={() => custom.value.inc()}>{custom.value.n}</div>;\n});\n```\n\n\nWhen using a Signal as the data to create the object, you need to pass the configuration as a function, and you can then also provide the `update` function to update the object when the signal changes.\n\nBy returning an object from `update`<!-- -->, you signal that the listeners have to be notified. You can mutate the current object but you should return it so that it will trigger listeners.\n\n```tsx\nconst Cmp = component$(() => {\n const n = useSignal(2);\n const custom = useSerializer$(() =>\n ({\n deserialize: () => new MyCustomSerializable(n.value),\n update: (current) => {\n current.n = n.value;\n return current;\n }\n })\n );\n return <div onClick$={() => n.value++}>{custom.value.n}</div>;\n});\n```",
2246+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-serializer.ts",
22472247
"mdFile": "core.useserializer_.md"
22482248
},
22492249
{

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8936,25 +8936,25 @@ const Cmp = component$(() => {
89368936
});
89378937
```
89388938
8939-
When using a Signal as the data to create the object, you may not need `serialize`. Furthermore, when the signal is updated, the serializer will be updated as well, and the current object will be passed as the second argument.
8939+
When using a Signal as the data to create the object, you need to pass the configuration as a function, and you can then also provide the `update` function to update the object when the signal changes.
8940+
8941+
By returning an object from `update`, you signal that the listeners have to be notified. You can mutate the current object but you should return it so that it will trigger listeners.
89408942
89418943
```tsx
89428944
const Cmp = component$(() => {
89438945
const n = useSignal(2);
8944-
const custom = useSerializer$((_data, current) => {
8945-
if (current) {
8946+
const custom = useSerializer$(() => ({
8947+
deserialize: () => new MyCustomSerializable(n.value),
8948+
update: (current) => {
89468949
current.n = n.value;
89478950
return current;
8948-
}
8949-
return new MyCustomSerializable(n.value);
8950-
});
8951+
},
8952+
}));
89518953
return <div onClick$={() => n.value++}>{custom.value.n}</div>;
89528954
});
89538955
```
89548956
8955-
(note that in this example, the `{custom.value.n}` is not reactive, so the div text will not update)
8956-
8957-
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-serialized.ts)
8957+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-serializer.ts)
89588958
89598959
## useServerData
89608960

packages/qwik/src/core/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export type { ContextId } from './use/use-context';
111111
export type { UseStoreOptions } from './use/use-store.public';
112112
export type { ComputedFn } from './use/use-computed';
113113
export { useComputedQrl } from './use/use-computed';
114-
export { useSerializerQrl, useSerializer$ } from './use/use-serialized';
114+
export { useSerializerQrl, useSerializer$ } from './use/use-serializer';
115115
export type { OnVisibleTaskOptions, VisibleTaskStrategy } from './use/use-visible-task';
116116
export { useVisibleTaskQrl } from './use/use-visible-task';
117117
export type { TaskCtx, TaskFn, Tracker } from './use/use-task';

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

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -847,26 +847,25 @@ export const createSerializationContext = (
847847
});
848848
} else if (obj instanceof Signal) {
849849
/**
850-
* WrappedSignal might not be calculated yet so we need to use `untrackedValue` to get the
851-
* value. ComputedSignal can be left uncalculated.
850+
* ComputedSignal can be left uncalculated if invalid.
851+
*
852+
* SerializerSignal is always serialized if it was already calculated.
852853
*/
853-
const v =
854-
obj instanceof WrappedSignal
855-
? obj.untrackedValue
856-
: obj instanceof ComputedSignal &&
857-
!(obj instanceof SerializerSignal) &&
858-
(obj.$invalid$ || fastSkipSerialize(obj))
859-
? NEEDS_COMPUTATION
860-
: obj.$untrackedValue$;
861-
if (v !== NEEDS_COMPUTATION) {
854+
const toSerialize =
855+
obj instanceof ComputedSignal &&
856+
!(obj instanceof SerializerSignal) &&
857+
(obj.$invalid$ || fastSkipSerialize(obj))
858+
? NEEDS_COMPUTATION
859+
: obj.$untrackedValue$;
860+
if (toSerialize !== NEEDS_COMPUTATION) {
862861
if (obj instanceof SerializerSignal) {
863862
promises.push(
864863
(obj.$computeQrl$ as any as QRLInternal<SerializerArg<any, any>>)
865864
.resolve()
866865
.then((arg) => {
867866
let data;
868867
if ((arg as any).serialize) {
869-
data = (arg as any).serialize(v);
868+
data = (arg as any).serialize(toSerialize);
870869
}
871870
if (data === undefined) {
872871
data = NEEDS_COMPUTATION;
@@ -876,7 +875,7 @@ export const createSerializationContext = (
876875
})
877876
);
878877
} else {
879-
discoveredValues.push(v);
878+
discoveredValues.push(toSerialize);
880879
}
881880
}
882881
if (obj.$effects$) {

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ export interface ComputedSignal<T> extends ReadonlySignal<T> {
4949
*
5050
* @public
5151
*/
52-
export interface SerializerSignal<T> extends ComputedSignal<T> {}
52+
export interface SerializerSignal<T> extends ComputedSignal<T> {
53+
/** Fake property to make the serialization linter happy */
54+
__noSerialize__: true;
55+
}
5356

5457
/**
5558
* Creates a Signal with the given value. If no value is given, the signal is created with
@@ -88,7 +91,6 @@ export { createComputedQrl };
8891
* @public
8992
*/
9093
export const createSerializer$: <T, S>(
91-
// We want to also add T as a possible parameter type, but that breaks type inference
9294
arg: SerializerArg<T, S>
9395
) => T extends Promise<any> ? never : SerializerSignal<T> = implicit$FirstArg(
9496
createSerializerQrl as any

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

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -430,8 +430,6 @@ export class ComputedSignal<T> extends Signal<T> {
430430
$invalidate$() {
431431
this.$invalid$ = true;
432432
this.$forceRunEffects$ = false;
433-
// We should only call subscribers if the calculation actually changed.
434-
// Therefore, we need to calculate the value now.
435433
this.$container$?.$scheduler$(ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, null, this);
436434
}
437435

@@ -440,10 +438,8 @@ export class ComputedSignal<T> extends Signal<T> {
440438
* remained the same object
441439
*/
442440
force() {
443-
this.$invalid$ = true;
444-
// TODO shouldn't force be set to true, invalid left alone and the effects scheduled?
445-
this.$forceRunEffects$ = false;
446-
triggerEffects(this.$container$, this, this.$effects$);
441+
this.$forceRunEffects$ = true;
442+
this.$container$?.$scheduler$(ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, null, this);
447443
}
448444

449445
get untrackedValue() {
@@ -536,7 +532,7 @@ export class WrappedSignal<T> extends Signal<T> implements Subscriber {
536532

537533
/**
538534
* Use this to force running subscribers, for example when the calculated value has mutated but
539-
* remained the same object
535+
* remained the same object.
540536
*/
541537
force() {
542538
this.$invalid$ = true;
@@ -579,39 +575,51 @@ export class WrappedSignal<T> extends Signal<T> implements Subscriber {
579575
}
580576
}
581577

578+
/** @public */
579+
export type SerializerArgObject<T, S> = {
580+
/**
581+
* This will be called with initial or serialized data to reconstruct an object. If no
582+
* `initialData` is provided, it will be called with `undefined`.
583+
*
584+
* This must not return a Promise.
585+
*/
586+
deserialize: (data: Awaited<S>) => T;
587+
/** The initial value to use when deserializing. */
588+
initial?: S | undefined;
589+
/**
590+
* This will be called with the object to get the serialized data. You can return a Promise if you
591+
* need to do async work.
592+
*
593+
* The result may be anything that Qwik can serialize.
594+
*
595+
* If you do not provide it, the object will be serialized as `undefined`. However, if the object
596+
* has a `[SerializerSymbol]` property, that will be used as the serializer instead.
597+
*/
598+
serialize?: (obj: T) => S;
599+
};
600+
582601
/**
583602
* Serialize and deserialize custom objects.
584603
*
585-
* If you pass a function, it will be used as the `deserialize` function.
604+
* If you need to use scoped state, you can pass a function instead of an object. The function will
605+
* be called with the current value, and you can return a new value.
586606
*
587607
* @public
588608
*/
589609
export type SerializerArg<T, S> =
590-
| {
591-
/**
592-
* This function will be called with serialized data to reconstruct an object.
593-
*
594-
* If it is created for the first time, it will get the `initial` data or `undefined`.
595-
*
596-
* If it uses signals or stores, it will be called when these change, and then the second
597-
* argument will be the previously constructed object.
598-
*
599-
* This function must not return a promise.
600-
*/
601-
deserialize: (data: S | undefined, previous: T | undefined) => T;
610+
| SerializerArgObject<T, S>
611+
| (() => SerializerArgObject<T, S> & {
602612
/**
603-
* This function will be called with the custom object to get the serialized data. You can
604-
* return a promise if you need to do async work.
613+
* This gets called when reactive state used during `deserialize` changes. You may mutate the
614+
* current object, or return a new object.
605615
*
606-
* The result may be anything that Qwik can serialize.
616+
* If it returns a value, that will be used as the new value, and listeners will be triggered.
617+
* If no change happened, don't return anything.
607618
*
608-
* If you do not provide it, the object will be serialized as `undefined`.
619+
* If you mutate the current object, you must return it so that it will trigger listeners.
609620
*/
610-
serialize?: (customObject: T) => S | Promise<S>;
611-
/** The initial value to use when deserializing. */
612-
initial?: S;
613-
}
614-
| ((data: S | undefined, previous: T | undefined) => T);
621+
update?: (current: T) => T | void;
622+
});
615623

616624
/**
617625
* A signal which provides a non-serializable value. It works like a computed signal, but it is
@@ -630,30 +638,31 @@ export class SerializerSignal<T, S> extends ComputedSignal<T> {
630638
return false;
631639
}
632640
throwIfQRLNotResolved(this.$computeQrl$);
633-
const arg = (this.$computeQrl$ as any as QRLInternal<SerializerArg<T, S>>).resolved!;
634-
let deserialize, initial;
641+
let arg = (this.$computeQrl$ as any as QRLInternal<SerializerArg<T, S>>).resolved!;
635642
if (typeof arg === 'function') {
636-
deserialize = arg;
637-
} else {
638-
deserialize = arg.deserialize;
639-
initial = arg.initial;
643+
arg = arg();
640644
}
645+
const { deserialize, initial } = arg;
646+
const update = (arg as any).update as ((current: T) => T) | undefined;
641647
const currentValue =
642648
this.$untrackedValue$ === NEEDS_COMPUTATION ? initial : this.$untrackedValue$;
643649
const untrackedValue = trackSignal(
644650
() =>
645651
this.$didInitialize$
646-
? deserialize(undefined, currentValue as T)
647-
: deserialize(currentValue as S, undefined),
652+
? update?.(currentValue as T)
653+
: deserialize(currentValue as Awaited<S>),
648654
this,
649655
EffectProperty.VNODE,
650656
this.$container$!
651657
);
652658
DEBUG && log('SerializerSignal.$compute$', untrackedValue);
659+
const didChange =
660+
(this.$didInitialize$ && untrackedValue !== 'undefined') ||
661+
untrackedValue !== this.$untrackedValue$;
653662
this.$invalid$ = false;
654-
const didChange = untrackedValue !== this.$untrackedValue$;
663+
this.$didInitialize$ = true;
655664
if (didChange) {
656-
this.$untrackedValue$ = untrackedValue;
665+
this.$untrackedValue$ = untrackedValue as T;
657666
}
658667
return didChange;
659668
}

packages/qwik/src/core/signal/signal.unit.tsx

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { $, type ValueOrPromise } from '@qwik.dev/core';
1+
import { $, isBrowser, type ValueOrPromise } from '@qwik.dev/core';
22
import { createDocument, getTestPlatform } from '@qwik.dev/core/testing';
33
import { afterEach, beforeEach, describe, expect, expectTypeOf, it } from 'vitest';
44
import { getDomContainer } from '../client/dom-container';
@@ -28,7 +28,12 @@ import {
2828
type Signal,
2929
} from './signal.public';
3030

31-
class Foo {}
31+
class Foo {
32+
constructor(public val: number = 0) {}
33+
update(val: number) {
34+
this.val = val;
35+
}
36+
}
3237

3338
describe('signal types', () => {
3439
it('Signal<T>', () => () => {
@@ -41,7 +46,7 @@ describe('signal types', () => {
4146
const signal2 = createComputed$<number>(() => 1);
4247
expectTypeOf(signal2).toEqualTypeOf<ComputedSignal<number>>();
4348
});
44-
it('SerializerSignal<T>', () => () => {
49+
it('SerializerSignal<T, S>', () => () => {
4550
{
4651
const signal = createSerializer$({
4752
deserialize: () => new Foo(),
@@ -54,15 +59,25 @@ describe('signal types', () => {
5459
expectTypeOf(signal.value).toEqualTypeOf<Foo>();
5560
}
5661
{
57-
const signal = createSerializer$(() => new Foo());
58-
expectTypeOf(signal).toEqualTypeOf<SerializerSignal<Foo>>();
59-
expectTypeOf(signal.value).toEqualTypeOf<Foo>();
62+
const stuff = createSignal(1);
63+
const signal = createSerializer$(() => ({
64+
deserialize: () => (isBrowser ? new Foo(stuff.value) : undefined),
65+
update: (foo) => {
66+
if (foo!.val !== stuff.value) {
67+
return;
68+
}
69+
foo!.update(stuff.value);
70+
return foo;
71+
},
72+
}));
73+
expectTypeOf(signal).toEqualTypeOf<SerializerSignal<undefined> | SerializerSignal<Foo>>();
74+
expectTypeOf(signal.value).toEqualTypeOf<Foo | undefined>();
6075
}
6176
{
62-
const signal = createSerializer$<Foo, number>({
63-
deserialize: (data, prev) => {
77+
const signal = createSerializer$({
78+
// We have to specify the type here, sadly
79+
deserialize: (data?: number) => {
6480
expectTypeOf(data).toEqualTypeOf<number | undefined>();
65-
expectTypeOf(prev).toEqualTypeOf<Foo | undefined>();
6681
return new Foo();
6782
},
6883
serialize: (obj) => {
@@ -75,13 +90,12 @@ describe('signal types', () => {
7590
}
7691
{
7792
const signal = createSerializer$({
78-
deserialize: (data, prev) => {
79-
expectTypeOf(data).toEqualTypeOf<number | undefined>();
80-
expectTypeOf(prev).toEqualTypeOf<Foo | undefined>();
93+
deserialize: (data) => {
94+
expectTypeOf(data).toEqualTypeOf<number>();
8195
return new Foo();
8296
},
83-
// you only have to specify the type on the serialize function
84-
serialize: (obj: Foo) => {
97+
initial: 3,
98+
serialize: (obj) => {
8599
expect(obj).toBeInstanceOf(Foo);
86100
return 1;
87101
},

0 commit comments

Comments
 (0)