diff --git a/cjs/serialize.js b/cjs/serialize.js index ac6cec1..342ad94 100644 --- a/cjs/serialize.js +++ b/cjs/serialize.js @@ -47,14 +47,23 @@ const shouldSkip = ([TYPE, type]) => ( const serializer = (strict, json, $, _) => { + // `$` memoizes seen values to preserve shared references and handle cycles, + // but it is a Map and Map keys use SameValueZero, which treats -0 and +0 as + // the same key. Negative zero must skip the memo so it is never conflated + // with +0, which native structuredClone (and the default clone) keep apart. + // Gate the Object.is check behind a number/zero test so it only runs for + // zero-valued numbers; every other value short-circuits before it. + const memoizable = value => !(typeof value === "number" && !value && Object.is(value, -0)); + const as = (out, value) => { const index = _.push(out) - 1; - $.set(value, index); + if (memoizable(value)) + $.set(value, index); return index; }; const pair = value => { - if ($.has(value)) + if (memoizable(value) && $.has(value)) return $.get(value); let [TYPE, type] = typeOf(value); diff --git a/esm/serialize.js b/esm/serialize.js index 24cfe2c..034d44b 100644 --- a/esm/serialize.js +++ b/esm/serialize.js @@ -49,14 +49,23 @@ const shouldSkip = ([TYPE, type]) => ( const serializer = (strict, json, $, _) => { + // `$` memoizes seen values to preserve shared references and handle cycles, + // but it is a Map and Map keys use SameValueZero, which treats -0 and +0 as + // the same key. Negative zero must skip the memo so it is never conflated + // with +0, which native structuredClone (and the default clone) keep apart. + // Gate the Object.is check behind a number/zero test so it only runs for + // zero-valued numbers; every other value short-circuits before it. + const memoizable = value => !(typeof value === "number" && !value && Object.is(value, -0)); + const as = (out, value) => { const index = _.push(out) - 1; - $.set(value, index); + if (memoizable(value)) + $.set(value, index); return index; }; const pair = value => { - if ($.has(value)) + if (memoizable(value) && $.has(value)) return $.get(value); let [TYPE, type] = typeOf(value); diff --git a/test/index.js b/test/index.js index 0277401..296761f 100644 --- a/test/index.js +++ b/test/index.js @@ -168,4 +168,17 @@ const invalidViaJSON = parse(stringify(invalidDate)); assert(invalidViaJSON instanceof Date, true); assert(Number.isNaN(invalidViaJSON.getTime()), true); +// -0 and +0 must round-trip distinctly like native structuredClone, even when +// both appear in the same value. The serialize memo is a Map, whose keys use +// SameValueZero, so without care +0 and -0 collapse onto a single record. +const zeros = deserialize(serialize([-0, 0])); +assert(zeros[0], -0, `expected -0 at [0], got ${zeros[0]}`); +assert(zeros[1], 0, `expected +0 at [1], got ${zeros[1]}`); +const zerosRev = deserialize(serialize([0, -0])); +assert(zerosRev[0], 0, `expected +0 at [0], got ${zerosRev[0]}`); +assert(zerosRev[1], -0, `expected -0 at [1], got ${zerosRev[1]}`); +const zerosObj = deserialize(serialize({neg: -0, pos: 0})); +assert(zerosObj.neg, -0, `expected -0 for neg, got ${zerosObj.neg}`); +assert(zerosObj.pos, 0, `expected +0 for pos, got ${zerosObj.pos}`); + require('./eval.js');