Skip to content

Commit 33ba20f

Browse files
committed
feat(custom serdes): allow Promise serialization
1 parent dcf7878 commit 33ba20f

File tree

4 files changed

+47
-10
lines changed

4 files changed

+47
-10
lines changed

.changeset/nasty-planes-jam.md

+2-1
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

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export const codeToText = (code: number, ...parts: any[]): string => {
5656
'WrappedSignal is read-only', // 49
5757
'SsrError: Promises not expected here.', // 50
5858
'Attribute value is unsafe for SSR', // 51
59+
'SerializerSymbol function returned rejected promise', // 52
5960
];
6061
let text = MAP[code] ?? '';
6162
if (parts.length) {
@@ -126,6 +127,7 @@ export const enum QError {
126127
wrappedReadOnly = 48,
127128
promisesNotExpected = 49,
128129
unsafeAttr = 50,
130+
serializerSymbolRejectedPromise = 52,
129131
}
130132

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

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

+14-7
Original file line numberDiff line numberDiff line change
@@ -1058,13 +1058,11 @@ function serialize(serializationContext: SerializationContext): void {
10581058
output(TypeIds.Constant, Constants.EMPTY_ARRAY);
10591059
} else if (value === EMPTY_OBJ) {
10601060
output(TypeIds.Constant, Constants.EMPTY_OBJ);
1061+
} else if (value === null) {
1062+
output(TypeIds.Constant, Constants.Null);
10611063
} else {
10621064
depth++;
1063-
if (value === null) {
1064-
output(TypeIds.Constant, Constants.Null);
1065-
} else {
1066-
writeObjectValue(value, idx);
1067-
}
1065+
writeObjectValue(value, idx);
10681066
depth--;
10691067
}
10701068
} else if (typeof value === 'string') {
@@ -1153,8 +1151,17 @@ function serialize(serializationContext: SerializationContext): void {
11531151
}
11541152
output(Array.isArray(storeTarget) ? TypeIds.StoreArray : TypeIds.Store, out);
11551153
}
1156-
} else if (SerializerSymbol in value && typeof value[SerializerSymbol] === 'function') {
1157-
const result = serializationResults.get(value);
1154+
} else if (isSerializerObj(value)) {
1155+
let result = serializationResults.get(value);
1156+
// special case: we unwrap Promises
1157+
if (isPromise(result)) {
1158+
const promiseResult = promiseResults.get(result)!;
1159+
if (!promiseResult[0]) {
1160+
console.error(promiseResult[1]);
1161+
throw qError(QError.serializerSymbolRejectedPromise);
1162+
}
1163+
result = promiseResult[1];
1164+
}
11581165
depth--;
11591166
writeValue(result, idx);
11601167
depth++;

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

+29-2
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 {
@@ -863,7 +863,7 @@ describe('shared-serialization', () => {
863863
(27 chars)"
864864
`);
865865
});
866-
it('should not use SerializeSymbol if not function', async () => {
866+
it('should not use SerializerSymbol if not function', async () => {
867867
const obj = { hi: 'orig', [SerializerSymbol]: 'hey' };
868868
const state = await serialize(obj);
869869
expect(dumpState(state)).toMatchInlineSnapshot(`
@@ -875,6 +875,33 @@ describe('shared-serialization', () => {
875875
(22 chars)"
876876
`);
877877
});
878+
it('should unwrap promises from SerializerSymbol', async () => {
879+
class Foo {
880+
hi = 'promise';
881+
async [SerializerSymbol]() {
882+
return Promise.resolve(this.hi);
883+
}
884+
}
885+
const state = await serialize(new Foo());
886+
expect(dumpState(state)).toMatchInlineSnapshot(`
887+
"
888+
0 String "promise"
889+
(13 chars)"
890+
`);
891+
});
892+
});
893+
it('should throw rejected promises from SerializerSymbol', async () => {
894+
const consoleSpy = vi.spyOn(console, 'error');
895+
896+
class Foo {
897+
hi = 'promise';
898+
async [SerializerSymbol]() {
899+
throw 'oh no';
900+
}
901+
}
902+
await expect(serialize(new Foo())).rejects.toThrow('Q52');
903+
expect(consoleSpy).toHaveBeenCalledWith('oh no');
904+
consoleSpy.mockRestore();
878905
});
879906
});
880907

0 commit comments

Comments
 (0)