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
12 changes: 7 additions & 5 deletions src/accessDeep.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { setDeep } from './accessDeep.js';
import { setDeep, type AccessDeepContext } from './accessDeep.js';

import { describe, it, expect } from 'vitest';

Expand All @@ -7,10 +7,11 @@ describe('setDeep', () => {
const obj = {
a: new Map([[new Set(['NaN']), [[1, 'undefined']]]]),
};
const context: AccessDeepContext = new WeakMap();

setDeep(obj, ['a', 0, 0, 0], Number);
setDeep(obj, ['a', 0, 1], entries => new Map(entries));
setDeep(obj, ['a', 0, 1, 0, 1], () => undefined);
setDeep(obj, ['a', 0, 0, 0], Number, context);
setDeep(obj, ['a', 0, 1], entries => new Map(entries), context);
setDeep(obj, ['a', 0, 1, 0, 1], () => undefined, context);

expect(obj).toEqual({
a: new Map([[new Set([NaN]), new Map([[1, undefined]])]]),
Expand All @@ -21,8 +22,9 @@ describe('setDeep', () => {
const obj = {
a: new Set([10, new Set(['NaN'])]),
};
const context: AccessDeepContext = new WeakMap();

setDeep(obj, ['a', 1, 0], Number);
setDeep(obj, ['a', 1, 0], Number, context);

expect(obj).toEqual({
a: new Set([10, new Set([NaN])]),
Expand Down
56 changes: 41 additions & 15 deletions src/accessDeep.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { isMap, isArray, isPlainObject, isSet } from './is.js';
import { includes } from './util.js';

const getNthKey = (value: Map<any, any> | Set<any>, n: number): any => {
if (n > value.size) throw new Error('index out of bounds');
const keys = value.keys();
while (n > 0) {
keys.next();
n--;
export type AccessDeepContext = WeakMap<object, any[]>;

const getNthKey = (
value: Map<any, any> | Set<any>,
n: number,
context: AccessDeepContext
): any => {
let indexed = context.get(value);
if (!indexed) {
indexed = Array.from(value.keys());
context.set(value, indexed);
}

if (!Number.isInteger(n) || n < 0 || n >= indexed.length) {
throw new Error('index out of bounds');
}

return keys.next().value;
return indexed[n];
};

function validatePath(path: (string | number)[]) {
Expand All @@ -24,18 +33,22 @@ function validatePath(path: (string | number)[]) {
}
}

export const getDeep = (object: object, path: (string | number)[]): object => {
export const getDeep = (
object: object,
path: (string | number)[],
context: AccessDeepContext
): object => {
validatePath(path);

for (let i = 0; i < path.length; i++) {
const key = path[i];
if (isSet(object)) {
object = getNthKey(object, +key);
object = getNthKey(object, +key, context);
} else if (isMap(object)) {
const row = +key;
const type = +path[++i] === 0 ? 'key' : 'value';

const keyOfRow = getNthKey(object, row);
const keyOfRow = getNthKey(object, row, context);
switch (type) {
case 'key':
object = keyOfRow;
Expand All @@ -55,7 +68,8 @@ export const getDeep = (object: object, path: (string | number)[]): object => {
export const setDeep = (
object: any,
path: (string | number)[],
mapper: (v: any) => any
mapper: (v: any) => any,
context: AccessDeepContext
): any => {
validatePath(path);

Expand All @@ -75,7 +89,7 @@ export const setDeep = (
parent = parent[key];
} else if (isSet(parent)) {
const row = +key;
parent = getNthKey(parent, row);
parent = getNthKey(parent, row, context);
} else if (isMap(parent)) {
const isEnd = i === path.length - 2;
if (isEnd) {
Expand All @@ -85,7 +99,7 @@ export const setDeep = (
const row = +key;
const type = +path[++i] === 0 ? 'key' : 'value';

const keyOfRow = getNthKey(parent, row);
const keyOfRow = getNthKey(parent, row, context);
switch (type) {
case 'key':
parent = keyOfRow;
Expand All @@ -106,17 +120,24 @@ export const setDeep = (
}

if (isSet(parent)) {
const oldValue = getNthKey(parent, +lastKey);
const row = +lastKey;
const oldValue = getNthKey(parent, row, context);
const newValue = mapper(oldValue);

if (oldValue !== newValue) {
parent.delete(oldValue);
parent.add(newValue);

const currentContext = context.get(parent);
if (currentContext) {
currentContext[row] = newValue;
}
}
}

if (isMap(parent)) {
const row = +path[path.length - 2];
const keyToRow = getNthKey(parent, row);
const keyToRow = getNthKey(parent, row, context);

const type = +lastKey === 0 ? 'key' : 'value';
switch (type) {
Expand All @@ -126,6 +147,11 @@ export const setDeep = (

if (newKey !== keyToRow) {
parent.delete(keyToRow);

const currentContext = context.get(parent);
if (currentContext) {
currentContext[row] = newKey;
}
}
break;
}
Expand Down
29 changes: 29 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,35 @@ describe('stringify & parse', () => {
},
},

'maintains referential equality between Set elements and master array': {
input: () => {
const objA = { name: 'A' };
const objB = { name: 'B' };
return {
master: [objA, objB],
testSet: new Set([objA, objB])
};
},
output: {
master: [{ name: 'A' }, { name: 'B' }],
testSet: [{ name: 'A' }, { name: 'B' }],
},
outputAnnotations: {
values: {
testSet: ['set'],
},
referentialEqualities: {
'master.0': ['testSet.0'],
'master.1': ['testSet.1'],
},
},
customExpectations: value => {
const setArr = Array.from(value.testSet);
expect(setArr[0]).toBe(value.master[0]);
expect(setArr[1]).toBe(value.master[1]);
},
},

'works for symbols': {
skipOnNode10: true,
input: () => {
Expand Down
8 changes: 6 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
generateReferentialEqualityAnnotations,
walker,
} from './plainer.js';
import { type AccessDeepContext } from './accessDeep.js';
import { copy } from 'copy-anything';

export default class SuperJSON {
Expand Down Expand Up @@ -65,15 +66,18 @@ export default class SuperJSON {

let result: T = options?.inPlace ? json : copy(json) as any;

const context: AccessDeepContext = new WeakMap();

if (meta?.values) {
result = applyValueAnnotations(result, meta.values, meta.v ?? 0, this);
result = applyValueAnnotations(result, meta.values, meta.v ?? 0, this, context);
}

if (meta?.referentialEqualities) {
result = applyReferentialEqualityAnnotations(
result,
meta.referentialEqualities,
meta.v ?? 0
meta.v ?? 0,
context
);
}

Expand Down
17 changes: 10 additions & 7 deletions src/plainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from './transformer.js';
import { includes, forEach } from './util.js';
import { parsePath } from './pathstringifier.js';
import { getDeep, setDeep } from './accessDeep.js';
import { getDeep, setDeep, type AccessDeepContext } from './accessDeep.js';
import SuperJSON from './index.js';

type Tree<T> = InnerNode<T> | Leaf<T>;
Expand Down Expand Up @@ -65,12 +65,13 @@ export function applyValueAnnotations(
plain: any,
annotations: MinimisedTree<TypeAnnotation>,
version: number,
superJson: SuperJSON
superJson: SuperJSON,
context: AccessDeepContext
) {
traverse(
annotations,
(type, path) => {
plain = setDeep(plain, path, v => untransformValue(v, type, superJson));
plain = setDeep(plain, path, v => untransformValue(v, type, superJson), context);
},
version
);
Expand All @@ -81,16 +82,17 @@ export function applyValueAnnotations(
export function applyReferentialEqualityAnnotations(
plain: any,
annotations: ReferentialEqualityAnnotations,
version: number
version: number,
context: AccessDeepContext
) {
const legacyPaths = enableLegacyPaths(version);
function apply(identicalPaths: string[], path: string) {
const object = getDeep(plain, parsePath(path, legacyPaths));
const object = getDeep(plain, parsePath(path, legacyPaths), context);

identicalPaths
.map(path => parsePath(path, legacyPaths))
.forEach(identicalObjectPath => {
plain = setDeep(plain, identicalObjectPath, () => object);
plain = setDeep(plain, identicalObjectPath, () => object, context);
});
}

Expand All @@ -100,7 +102,8 @@ export function applyReferentialEqualityAnnotations(
plain = setDeep(
plain,
parsePath(identicalPath, legacyPaths),
() => plain
() => plain,
context
);
});

Expand Down