Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions cjs/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
13 changes: 11 additions & 2 deletions esm/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
13 changes: 13 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');