Skip to content

Commit

Permalink
feat(custom serdes): allow Promise serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
wmertens committed Jan 4, 2025
1 parent 18169a7 commit 103e3c1
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 10 deletions.
3 changes: 2 additions & 1 deletion .changeset/nasty-planes-jam.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
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.
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.
2 changes: 2 additions & 0 deletions packages/qwik/src/core/shared/error/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const codeToText = (code: number, ...parts: any[]): string => {
'WrappedSignal is read-only', // 49
'SsrError: Promises not expected here.', // 50
'Attribute value is unsafe for SSR', // 51
'SerializerSymbol function returned rejected promise', // 52
];
let text = MAP[code] ?? '';
if (parts.length) {
Expand Down Expand Up @@ -128,6 +129,7 @@ export const enum QError {
wrappedReadOnly = 49,
promisesNotExpected = 50,
unsafeAttr = 51,
serializerSymbolRejectedPromise = 52,
}

export const qError = (code: number, errorMessageArgs: any[] = []): Error => {
Expand Down
21 changes: 14 additions & 7 deletions packages/qwik/src/core/shared/shared-serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1053,13 +1053,11 @@ function serialize(serializationContext: SerializationContext): void {
output(TypeIds.Constant, Constants.EMPTY_ARRAY);
} else if (value === EMPTY_OBJ) {
output(TypeIds.Constant, Constants.EMPTY_OBJ);
} else if (value === null) {
output(TypeIds.Constant, Constants.Null);
} else {
depth++;
if (value === null) {
output(TypeIds.Constant, Constants.Null);
} else {
writeObjectValue(value, idx);
}
writeObjectValue(value, idx);
depth--;
}
} else if (typeof value === 'string') {
Expand Down Expand Up @@ -1148,8 +1146,17 @@ function serialize(serializationContext: SerializationContext): void {
}
output(Array.isArray(storeTarget) ? TypeIds.StoreArray : TypeIds.Store, out);
}
} else if (SerializerSymbol in value && typeof value[SerializerSymbol] === 'function') {
const result = serializationResults.get(value);
} else if (isSerializerObj(value)) {
let result = serializationResults.get(value);
// special case: we unwrap Promises
if (isPromise(result)) {
const promiseResult = promiseResults.get(result)!;
if (!promiseResult[0]) {
console.error(promiseResult[1]);
throw qError(QError.serializerSymbolRejectedPromise);
}
result = promiseResult[1];
}
depth--;
writeValue(result, idx);
depth++;
Expand Down
31 changes: 29 additions & 2 deletions packages/qwik/src/core/shared/shared-serialization.unit.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { $, component$, noSerialize } from '@qwik.dev/core';
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { _fnSignal, _wrapProp } from '../internal';
import { EffectPropData, type Signal } from '../signal/signal';
import {
Expand Down Expand Up @@ -863,7 +863,7 @@ describe('shared-serialization', () => {
(27 chars)"
`);
});
it('should not use SerializeSymbol if not function', async () => {
it('should not use SerializerSymbol if not function', async () => {
const obj = { hi: 'orig', [SerializerSymbol]: 'hey' };
const state = await serialize(obj);
expect(dumpState(state)).toMatchInlineSnapshot(`
Expand All @@ -875,6 +875,33 @@ describe('shared-serialization', () => {
(22 chars)"
`);
});
it('should unwrap promises from SerializerSymbol', async () => {
class Foo {
hi = 'promise';
async [SerializerSymbol]() {
return Promise.resolve(this.hi);
}
}
const state = await serialize(new Foo());
expect(dumpState(state)).toMatchInlineSnapshot(`
"
0 String "promise"
(13 chars)"
`);
});
});
it('should throw rejected promises from SerializerSymbol', async () => {
const consoleSpy = vi.spyOn(console, 'error');

class Foo {
hi = 'promise';
async [SerializerSymbol]() {
throw 'oh no';
}
}
await expect(serialize(new Foo())).rejects.toThrow('Q52');
expect(consoleSpy).toHaveBeenCalledWith('oh no');
consoleSpy.mockRestore();
});
});

Expand Down

0 comments on commit 103e3c1

Please sign in to comment.