Skip to content

Commit 1badea0

Browse files
committed
feat(custom serdes): allow Promise serialization
1 parent dee9c04 commit 1badea0

File tree

4 files changed

+47
-10
lines changed

4 files changed

+47
-10
lines changed

.changeset/nasty-planes-jam.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
FEAT: `useSerialized$(fn)` and `createSerialized$(fn)` allow serializing custom objects. You must provide a
66
function that converts the custom object to a serializable one via the `[SerializerSymbol]`
77
property, and then provide `use|createSerialized$(fn)` with the function that creates the custom object
8-
from the serialized data. This will lazily create the value when needed.
8+
from the serialized data. This will lazily create the value when needed. Note that the serializer
9+
function may return a Promise, which will be awaited. The deserializer must not return a Promise.

packages/qwik/src/core/shared/error/error.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const codeToText = (code: number, ...parts: any[]): string => {
5555
'ComputedSignal is read-only', // 47
5656
'WrappedSignal is read-only', // 48
5757
'Attribute value is unsafe for SSR', // 49
58+
'SerializerSymbol function returned rejected promise', // 50
5859
];
5960
let text = MAP[code] ?? '';
6061
if (parts.length) {
@@ -124,6 +125,7 @@ export const enum QError {
124125
computedReadOnly = 47,
125126
wrappedReadOnly = 48,
126127
unsafeAttr = 49,
128+
serializerSymbolRejectedPromise = 50,
127129
}
128130

129131
export const qError = (code: number, errorMessageArgs: any[] = []): Error => {

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,13 +1061,11 @@ function serialize(serializationContext: SerializationContext): void {
10611061
output(TypeIds.Constant, Constants.EMPTY_ARRAY);
10621062
} else if (value === EMPTY_OBJ) {
10631063
output(TypeIds.Constant, Constants.EMPTY_OBJ);
1064+
} else if (value === null) {
1065+
output(TypeIds.Constant, Constants.Null);
10641066
} else {
10651067
depth++;
1066-
if (value === null) {
1067-
output(TypeIds.Constant, Constants.Null);
1068-
} else {
1069-
writeObjectValue(value, idx);
1070-
}
1068+
writeObjectValue(value, idx);
10711069
depth--;
10721070
}
10731071
} else if (typeof value === 'string') {
@@ -1157,8 +1155,17 @@ function serialize(serializationContext: SerializationContext): void {
11571155
}
11581156
output(Array.isArray(storeTarget) ? TypeIds.StoreArray : TypeIds.Store, out);
11591157
}
1160-
} else if (SerializerSymbol in value && typeof value[SerializerSymbol] === 'function') {
1161-
const result = serializationResults.get(value);
1158+
} else if (isSerializerObj(value)) {
1159+
let result = serializationResults.get(value);
1160+
// special case: we unwrap Promises
1161+
if (isPromise(result)) {
1162+
const promiseResult = promiseResults.get(result)!;
1163+
if (!promiseResult[0]) {
1164+
console.error(promiseResult[1]);
1165+
throw qError(QError.serializerSymbolRejectedPromise);
1166+
}
1167+
result = promiseResult[1];
1168+
}
11621169
depth--;
11631170
writeValue(result, idx);
11641171
depth++;

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

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { $, component$, noSerialize } from '@qwik.dev/core';
2-
import { describe, expect, it } from 'vitest';
2+
import { describe, expect, it, vi } from 'vitest';
33
import { _fnSignal, _wrapProp } from '../internal';
44
import { EffectPropData, type Signal } from '../signal/signal';
55
import {
@@ -860,7 +860,7 @@ describe('shared-serialization', () => {
860860
(27 chars)"
861861
`);
862862
});
863-
it('should not use SerializeSymbol if not function', async () => {
863+
it('should not use SerializerSymbol if not function', async () => {
864864
const obj = { hi: 'orig', [SerializerSymbol]: 'hey' };
865865
const state = await serialize(obj);
866866
expect(dumpState(state)).toMatchInlineSnapshot(`
@@ -872,6 +872,33 @@ describe('shared-serialization', () => {
872872
(22 chars)"
873873
`);
874874
});
875+
it('should unwrap promises from SerializerSymbol', async () => {
876+
class Foo {
877+
hi = 'promise';
878+
async [SerializerSymbol]() {
879+
return Promise.resolve(this.hi);
880+
}
881+
}
882+
const state = await serialize(new Foo());
883+
expect(dumpState(state)).toMatchInlineSnapshot(`
884+
"
885+
0 String "promise"
886+
(13 chars)"
887+
`);
888+
});
889+
});
890+
it('should throw rejected promises from SerializerSymbol', async () => {
891+
const consoleSpy = vi.spyOn(console, 'error');
892+
893+
class Foo {
894+
hi = 'promise';
895+
async [SerializerSymbol]() {
896+
throw 'oh no';
897+
}
898+
}
899+
await expect(serialize(new Foo())).rejects.toThrow('Q50');
900+
expect(consoleSpy).toHaveBeenCalledWith('oh no');
901+
consoleSpy.mockRestore();
875902
});
876903
});
877904

0 commit comments

Comments
 (0)