From 8ced083267c5d3caed0840d51dd39ebb47755fc3 Mon Sep 17 00:00:00 2001 From: Luca Casonato Date: Thu, 31 Oct 2024 11:22:03 +0100 Subject: [PATCH] fix: serialization of signals containing null and undefined values (#2744) --- src/jsonify/custom_test.ts | 45 +++++++++++++++++++++++++++-- src/jsonify/parse.ts | 28 ++++++++++++++++-- src/jsonify/stringify.ts | 11 ++++--- src/runtime/server/preact_hooks.tsx | 8 +++-- 4 files changed, 80 insertions(+), 12 deletions(-) diff --git a/src/jsonify/custom_test.ts b/src/jsonify/custom_test.ts index 0aa5ffef32d..aa2127025f3 100644 --- a/src/jsonify/custom_test.ts +++ b/src/jsonify/custom_test.ts @@ -12,7 +12,8 @@ Deno.test("custom parse - Point", () => { } const str = stringify(new Point(30, 40), { - Point: (value) => value instanceof Point ? [value.x, value.y] : undefined, + Point: (value) => + value instanceof Point ? { value: [value.x, value.y] } : undefined, }); expect(str).toEqual('[["Point",1],[2,3],30,40]'); @@ -35,18 +36,56 @@ Deno.test("custom stringify - Signals", () => { const s = signal(2); expect(stringify(s, { Signal: (s2: unknown) => { - return s2 instanceof Signal ? s2.peek() : undefined; + return s2 instanceof Signal ? { value: s2.peek() } : undefined; }, })).toEqual( '[["Signal",1],2]', ); }); +Deno.test("custom parse - Signals with null value", () => { + const res = parse('[["Signal",-2]]', { + Signal: (value) => signal(value), + }); + expect(res).toBeInstanceOf(Signal); + expect(res.peek()).toEqual(null); +}); + +Deno.test("custom stringify - Signals with null value", () => { + const s = signal(null); + expect(stringify(s, { + Signal: (s2: unknown) => { + return s2 instanceof Signal ? { value: s2.peek() } : undefined; + }, + })).toEqual( + '[["Signal",-2]]', + ); +}); + +Deno.test("custom parse - Signals with undefined value", () => { + const res = parse('[["Signal",-1]]', { + Signal: (value) => signal(value), + }); + expect(res).toBeInstanceOf(Signal); + expect(res.peek()).toEqual(undefined); +}); + +Deno.test("custom stringify - Signals with undefined value", () => { + const s = signal(undefined); + expect(stringify(s, { + Signal: (s2: unknown) => { + return s2 instanceof Signal ? { value: s2.peek() } : undefined; + }, + })).toEqual( + '[["Signal",-1]]', + ); +}); + Deno.test("custom stringify - referenced Signals", () => { const s = signal(2); expect(stringify([s, s], { Signal: (s2: unknown) => { - return s2 instanceof Signal ? s2.peek() : undefined; + return s2 instanceof Signal ? { value: s2.peek() } : undefined; }, })).toEqual( '[[1,1],["Signal",2],2]', diff --git a/src/jsonify/parse.ts b/src/jsonify/parse.ts index 66ed83ca89c..91c0fe0e527 100644 --- a/src/jsonify/parse.ts +++ b/src/jsonify/parse.ts @@ -68,8 +68,32 @@ function unpack( if (custom !== undefined && name in custom) { const fn = custom[name]; const ref = current[1]; - unpack(arr, hydrated, ref, custom); - const value = hydrated[ref]; + let value; + if (ref < 0) { + switch (ref) { + case UNDEFINED: + value = undefined; + break; + case NULL: + value = null; + break; + case NAN: + value = NaN; + break; + case INFINITY_POS: + value = Infinity; + break; + case INFINITY_NEG: + value = -Infinity; + break; + case ZERO_NEG: + value = -0; + break; + } + } else { + unpack(arr, hydrated, ref, custom); + value = hydrated[ref]; + } hydrated[idx] = fn(value); return; } diff --git a/src/jsonify/stringify.ts b/src/jsonify/stringify.ts index 5511e8d713f..eb55ca41a6c 100644 --- a/src/jsonify/stringify.ts +++ b/src/jsonify/stringify.ts @@ -8,8 +8,11 @@ import { } from "./constants.ts"; import { HOLE } from "./constants.ts"; -// deno-lint-ignore no-explicit-any -export type Stringifiers = Record any>; +export type Stringifiers = Record< + string, + // deno-lint-ignore no-explicit-any + (value: any) => { value: any } | undefined +>; /** * Serializes the following: @@ -94,8 +97,8 @@ function serializeInner( const res = fn(value); if (res === undefined) continue; - serializeInner(out, indexes, res, custom); - str = `["${k}",${idx + 1}]`; + const innerIdx = serializeInner(out, indexes, res.value, custom); + str = `["${k}",${innerIdx}]`; out[idx] = str; return idx; } diff --git a/src/runtime/server/preact_hooks.tsx b/src/runtime/server/preact_hooks.tsx index fc4bd2a4230..ec6dbb65738 100644 --- a/src/runtime/server/preact_hooks.tsx +++ b/src/runtime/server/preact_hooks.tsx @@ -369,14 +369,16 @@ function isVNode(x: any): x is VNode { const stringifiers: Stringifiers = { Signal: (value: unknown) => { - return isSignal(value) ? value.peek() : undefined; + return isSignal(value) ? { value: value.peek() } : undefined; }, Slot: (value: unknown) => { if (isVNode(value) && value.type === Slot) { const props = value.props as SlotProps; return { - name: props.name, - id: props.id, + value: { + name: props.name, + id: props.id, + }, }; } },