From 9009520c9059fa5a5eb20dcc3ddb511caebd3e6a Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Fri, 3 Jan 2025 08:27:01 +0100 Subject: [PATCH] feat(custom serdes): allow Promise serialization --- .changeset/nasty-planes-jam.md | 3 ++- packages/qwik/src/core/shared/error/error.ts | 2 ++ .../src/core/shared/shared-serialization.ts | 21 ++++++++++++------- .../core/shared/shared-serialization.unit.ts | 16 +++++++++++++- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/.changeset/nasty-planes-jam.md b/.changeset/nasty-planes-jam.md index cf1e161e9d02..2f467df3db7c 100644 --- a/.changeset/nasty-planes-jam.md +++ b/.changeset/nasty-planes-jam.md @@ -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. diff --git a/packages/qwik/src/core/shared/error/error.ts b/packages/qwik/src/core/shared/error/error.ts index 6816a81efbd9..683ceac0490a 100644 --- a/packages/qwik/src/core/shared/error/error.ts +++ b/packages/qwik/src/core/shared/error/error.ts @@ -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) { @@ -128,6 +129,7 @@ export const enum QError { wrappedReadOnly = 49, promisesNotExpected = 50, unsafeAttr = 51, + serializerSymbolRejectedPromise = 52, } export const qError = (code: number, errorMessageArgs: any[] = []): Error => { diff --git a/packages/qwik/src/core/shared/shared-serialization.ts b/packages/qwik/src/core/shared/shared-serialization.ts index b97580d4bac6..62e9edd5d75c 100644 --- a/packages/qwik/src/core/shared/shared-serialization.ts +++ b/packages/qwik/src/core/shared/shared-serialization.ts @@ -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') { @@ -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++; diff --git a/packages/qwik/src/core/shared/shared-serialization.unit.ts b/packages/qwik/src/core/shared/shared-serialization.unit.ts index d6368a38f057..640a1ada3d74 100644 --- a/packages/qwik/src/core/shared/shared-serialization.unit.ts +++ b/packages/qwik/src/core/shared/shared-serialization.unit.ts @@ -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(` @@ -875,6 +875,20 @@ 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)" + `); + }); }); });