From 03b4b8407d47dc9799e16e926e3486853ed77201 Mon Sep 17 00:00:00 2001 From: JeremyMoeglich Date: Wed, 17 Dec 2025 18:50:38 +0100 Subject: [PATCH 1/2] Improve performance for index lookups on Set, Map --- src/accessDeep.test.ts | 59 +++++++++++-- src/accessDeep.ts | 72 ++++++++++++---- src/avlTree.ts | 185 +++++++++++++++++++++++++++++++++++++++++ src/index.ts | 8 +- src/plainer.ts | 17 ++-- 5 files changed, 312 insertions(+), 29 deletions(-) create mode 100644 src/avlTree.ts diff --git a/src/accessDeep.test.ts b/src/accessDeep.test.ts index 5e64fad..83ab7c3 100644 --- a/src/accessDeep.test.ts +++ b/src/accessDeep.test.ts @@ -1,4 +1,4 @@ -import { setDeep } from './accessDeep.js'; +import { setDeep, type AccessDeepContext } from './accessDeep.js'; import { describe, it, expect } from 'vitest'; @@ -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]])]]), @@ -21,11 +22,59 @@ 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])]), }); }); + + it('keeps set index cache in sync when values change', () => { + const obj = { s: new Set([1, 2, 3]) }; + const context: AccessDeepContext = new WeakMap(); + + setDeep(obj, ['s', 1], v => v * 10, context); // 2 -> 20 (appended) + setDeep(obj, ['s', 1], v => v * 100, context); // 3 -> 300 (appended) + + expect(Array.from(obj.s)).toEqual([1, 20, 300]); + + const indexed = context.get(obj.s); + expect(indexed).toBeDefined(); + // Verify cache is in sync by checking we can access all indices + expect(() => indexed!.get(obj.s.size - 1)).not.toThrow(); + expect(() => indexed!.get(obj.s.size)).toThrow(); + }); + + it('keeps set index cache in sync when mapper creates duplicates', () => { + const obj = { s: new Set([1, 2, 3]) }; + const context: AccessDeepContext = new WeakMap(); + + setDeep(obj, ['s', 0], () => 2, context); // 1 -> 2 (dedup, no append) + setDeep(obj, ['s', 0], v => v * 10, context); // 2 -> 20 (appended) + + expect(Array.from(obj.s)).toEqual([3, 20]); + + const indexed = context.get(obj.s); + expect(indexed).toBeDefined(); + // Verify cache is in sync by checking we can access all indices + expect(() => indexed!.get(obj.s.size - 1)).not.toThrow(); + expect(() => indexed!.get(obj.s.size)).toThrow(); + }); + + it('keeps map key index cache in sync when keys change', () => { + const obj = { m: new Map([['a', 1], ['b', 2], ['c', 3]]) }; + const context: AccessDeepContext = new WeakMap(); + + setDeep(obj, ['m', 1, 0], k => String(k).toUpperCase(), context); // b -> B (appended) + setDeep(obj, ['m', 1, 1], v => v * 10, context); // row 1 is now c + + expect(Array.from(obj.m.entries())).toEqual([['a', 1], ['c', 30], ['B', 2]]); + + const indexed = context.get(obj.m); + expect(indexed).toBeDefined(); + expect(() => indexed!.get(obj.m.size - 1)).not.toThrow(); + expect(() => indexed!.get(obj.m.size)).toThrow(); + }); }); diff --git a/src/accessDeep.ts b/src/accessDeep.ts index ea98613..70ab462 100644 --- a/src/accessDeep.ts +++ b/src/accessDeep.ts @@ -1,15 +1,33 @@ +import { IndexedListAVL } from './avlTree.js'; import { isMap, isArray, isPlainObject, isSet } from './is.js'; import { includes } from './util.js'; -const getNthKey = (value: Map | Set, 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>; + +function getIndexed( + value: Map | Set, + context: AccessDeepContext +) { + const cached = context.get(value); + if (cached) return cached; + + const created: IndexedListAVL = new IndexedListAVL(value.keys()); + + context.set(value, created); + return created; +} + +const getNthKey = ( + value: Map | Set, + n: number, + context: AccessDeepContext +): any => { + if (!Number.isInteger(n) || n < 0 || n >= value.size) { + throw new Error('index out of bounds'); } - return keys.next().value; + const indexed = getIndexed(value, context); + return indexed.get(n); }; function validatePath(path: (string | number)[]) { @@ -24,18 +42,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; @@ -55,7 +77,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); @@ -75,7 +98,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) { @@ -85,7 +108,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; @@ -106,26 +129,45 @@ export const setDeep = ( } if (isSet(parent)) { - const oldValue = getNthKey(parent, +lastKey); + const row = +lastKey; + const oldValue = getNthKey(parent, row, context); const newValue = mapper(oldValue); + const hadNewValue = parent.has(newValue); if (oldValue !== newValue) { parent.delete(oldValue); parent.add(newValue); + + const currentContext = context.get(parent); + if (currentContext) { + currentContext.delete(row); + if (!hadNewValue) { + currentContext.insertAtEnd(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) { case 'key': { const newKey = mapper(keyToRow); + const hadNewKey = parent.has(newKey); parent.set(newKey, parent.get(keyToRow)); if (newKey !== keyToRow) { parent.delete(keyToRow); + + const currentContext = context.get(parent); + if (currentContext) { + currentContext.delete(row); + if (!hadNewKey) { + currentContext.insertAtEnd(newKey); + } + } } break; } diff --git a/src/avlTree.ts b/src/avlTree.ts new file mode 100644 index 0000000..293625f --- /dev/null +++ b/src/avlTree.ts @@ -0,0 +1,185 @@ +type MaybeNode = AvlNode | null; + +class AvlNode { + value: T; + left: MaybeNode = null; + right: MaybeNode = null; + height: number = 1; + size: number = 1; + + constructor(value: T) { + this.value = value; + } +} + +function getHeight(n: MaybeNode): number { + return n ? n.height : 0; +} + +function getSize(n: MaybeNode): number { + return n ? n.size : 0; +} + +function updateNode(n: AvlNode): void { + const leftHeight = getHeight(n.left); + const rightHeight = getHeight(n.right); + n.height = (leftHeight > rightHeight ? leftHeight : rightHeight) + 1; + n.size = 1 + getSize(n.left) + getSize(n.right); +} + +function rotateRight(y: AvlNode): AvlNode { + const x = y.left!; + const t2 = x.right; + + x.right = y; + y.left = t2; + + updateNode(y); + updateNode(x); + return x; +} + +function rotateLeft(x: AvlNode): AvlNode { + const y = x.right!; + const t2 = y.left; + + y.left = x; + x.right = t2; + + updateNode(x); + updateNode(y); + return y; +} + +function rebalance(n: AvlNode): AvlNode { + updateNode(n); + const balanceFactor = getHeight(n.left) - getHeight(n.right); + + if (balanceFactor > 1) { + const l = n.left!; + if (getHeight(l.left) < getHeight(l.right)) n.left = rotateLeft(l); + return rotateRight(n); + } + + if (balanceFactor < -1) { + const r = n.right!; + if (getHeight(r.right) < getHeight(r.left)) n.right = rotateRight(r); + return rotateLeft(n); + } + + return n; +} + +function extractMin(n: AvlNode): [MaybeNode, AvlNode] { + if (!n.left) { + const right = n.right; + n.right = null; + updateNode(n); + return [right, n]; + } + const [newLeft, minNode] = extractMin(n.left); + n.left = newLeft; + return [rebalance(n), minNode]; +} + +function insertAt(n: MaybeNode, index: number, value: T): AvlNode { + if (!n) return new AvlNode(value); + + const leftSize = getSize(n.left); + if (index <= leftSize) { + n.left = insertAt(n.left, index, value); + } else { + n.right = insertAt(n.right, index - leftSize - 1, value); + } + return rebalance(n); +} + +function deleteAt(n: AvlNode, index: number): [MaybeNode, T] { + const leftSize = getSize(n.left); + + if (index < leftSize) { + const [newLeft, deleted] = deleteAt(n.left!, index); + n.left = newLeft; + return [rebalance(n), deleted]; + } + + if (index > leftSize) { + const [newRight, deleted] = deleteAt(n.right!, index - leftSize - 1); + n.right = newRight; + return [rebalance(n), deleted]; + } + + // delete this node + const deletedValue = n.value; + + if (!n.left) return [n.right, deletedValue]; + if (!n.right) return [n.left, deletedValue]; + + const [newRight, successor] = extractMin(n.right); + successor.left = n.left; + successor.right = newRight; + return [rebalance(successor), deletedValue]; +} + +function nodeAt(root: MaybeNode, index: number): AvlNode { + let cur = root; + let i = index; + while (cur) { + const leftSize = getSize(cur.left); + if (i < leftSize) { + cur = cur.left; + } else if (i === leftSize) { + return cur; + } else { + i -= leftSize + 1; + cur = cur.right; + } + } + throw new RangeError(`index ${index} out of range`); +} + +function buildBalancedFromArray(arr: readonly T[], lo: number, hi: number): MaybeNode { + if (lo >= hi) return null; + const mid = lo + ((hi - lo) >>> 1); + const n = new AvlNode(arr[mid]); + n.left = buildBalancedFromArray(arr, lo, mid); + n.right = buildBalancedFromArray(arr, mid + 1, hi); + updateNode(n); + return n; +} + +function assertIndex(index: number, size: number): void { + if (!Number.isSafeInteger(index)) { + throw new TypeError(`index must be a safe integer, got ${index}`); + } + if (index < 0 || index >= size) { + throw new RangeError(`index ${index} out of range`); + } +} + +export class IndexedListAVL { + private root: MaybeNode = null; + + constructor(init: Iterable | null = null) { + if (init) { + const arr = Array.from(init); + this.root = buildBalancedFromArray(arr, 0, arr.length); + } + } + + get(index: number): T { + assertIndex(index, getSize(this.root)); + return nodeAt(this.root, index).value; + } + + delete(index: number): T { + assertIndex(index, getSize(this.root)); + const [newRoot, deleted] = deleteAt(this.root!, index); + this.root = newRoot; + return deleted; + } + + insertAtEnd(value: T): void { + this.root = insertAt(this.root, getSize(this.root), value); + } +} diff --git a/src/index.ts b/src/index.ts index 9a11ad7..7e9fb01 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 { @@ -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 ); } diff --git a/src/plainer.ts b/src/plainer.ts index 8e9059f..5020ad5 100644 --- a/src/plainer.ts +++ b/src/plainer.ts @@ -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 = InnerNode | Leaf; @@ -65,12 +65,13 @@ export function applyValueAnnotations( plain: any, annotations: MinimisedTree, 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 ); @@ -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); }); } @@ -100,7 +102,8 @@ export function applyReferentialEqualityAnnotations( plain = setDeep( plain, parsePath(identicalPath, legacyPaths), - () => plain + () => plain, + context ); }); From 1d4c9efbbc77ea29ea0f3c1efd467ccdd6f70ced Mon Sep 17 00:00:00 2001 From: JeremyMoeglich Date: Thu, 18 Dec 2025 06:13:07 +0100 Subject: [PATCH 2/2] replace tree with array --- src/accessDeep.test.ts | 47 ----------- src/accessDeep.ts | 40 +++------ src/avlTree.ts | 185 ----------------------------------------- src/index.test.ts | 29 +++++++ 4 files changed, 41 insertions(+), 260 deletions(-) delete mode 100644 src/avlTree.ts diff --git a/src/accessDeep.test.ts b/src/accessDeep.test.ts index 83ab7c3..a5ffedc 100644 --- a/src/accessDeep.test.ts +++ b/src/accessDeep.test.ts @@ -30,51 +30,4 @@ describe('setDeep', () => { a: new Set([10, new Set([NaN])]), }); }); - - it('keeps set index cache in sync when values change', () => { - const obj = { s: new Set([1, 2, 3]) }; - const context: AccessDeepContext = new WeakMap(); - - setDeep(obj, ['s', 1], v => v * 10, context); // 2 -> 20 (appended) - setDeep(obj, ['s', 1], v => v * 100, context); // 3 -> 300 (appended) - - expect(Array.from(obj.s)).toEqual([1, 20, 300]); - - const indexed = context.get(obj.s); - expect(indexed).toBeDefined(); - // Verify cache is in sync by checking we can access all indices - expect(() => indexed!.get(obj.s.size - 1)).not.toThrow(); - expect(() => indexed!.get(obj.s.size)).toThrow(); - }); - - it('keeps set index cache in sync when mapper creates duplicates', () => { - const obj = { s: new Set([1, 2, 3]) }; - const context: AccessDeepContext = new WeakMap(); - - setDeep(obj, ['s', 0], () => 2, context); // 1 -> 2 (dedup, no append) - setDeep(obj, ['s', 0], v => v * 10, context); // 2 -> 20 (appended) - - expect(Array.from(obj.s)).toEqual([3, 20]); - - const indexed = context.get(obj.s); - expect(indexed).toBeDefined(); - // Verify cache is in sync by checking we can access all indices - expect(() => indexed!.get(obj.s.size - 1)).not.toThrow(); - expect(() => indexed!.get(obj.s.size)).toThrow(); - }); - - it('keeps map key index cache in sync when keys change', () => { - const obj = { m: new Map([['a', 1], ['b', 2], ['c', 3]]) }; - const context: AccessDeepContext = new WeakMap(); - - setDeep(obj, ['m', 1, 0], k => String(k).toUpperCase(), context); // b -> B (appended) - setDeep(obj, ['m', 1, 1], v => v * 10, context); // row 1 is now c - - expect(Array.from(obj.m.entries())).toEqual([['a', 1], ['c', 30], ['B', 2]]); - - const indexed = context.get(obj.m); - expect(indexed).toBeDefined(); - expect(() => indexed!.get(obj.m.size - 1)).not.toThrow(); - expect(() => indexed!.get(obj.m.size)).toThrow(); - }); }); diff --git a/src/accessDeep.ts b/src/accessDeep.ts index 70ab462..c42432f 100644 --- a/src/accessDeep.ts +++ b/src/accessDeep.ts @@ -1,33 +1,24 @@ -import { IndexedListAVL } from './avlTree.js'; import { isMap, isArray, isPlainObject, isSet } from './is.js'; import { includes } from './util.js'; -export type AccessDeepContext = WeakMap>; - -function getIndexed( - value: Map | Set, - context: AccessDeepContext -) { - const cached = context.get(value); - if (cached) return cached; - - const created: IndexedListAVL = new IndexedListAVL(value.keys()); - - context.set(value, created); - return created; -} +export type AccessDeepContext = WeakMap; const getNthKey = ( value: Map | Set, n: number, context: AccessDeepContext ): any => { - if (!Number.isInteger(n) || n < 0 || n >= value.size) { + 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'); } - const indexed = getIndexed(value, context); - return indexed.get(n); + return indexed[n]; }; function validatePath(path: (string | number)[]) { @@ -132,17 +123,14 @@ export const setDeep = ( const row = +lastKey; const oldValue = getNthKey(parent, row, context); const newValue = mapper(oldValue); - const hadNewValue = parent.has(newValue); + if (oldValue !== newValue) { parent.delete(oldValue); parent.add(newValue); const currentContext = context.get(parent); if (currentContext) { - currentContext.delete(row); - if (!hadNewValue) { - currentContext.insertAtEnd(newValue); - } + currentContext[row] = newValue; } } } @@ -155,7 +143,6 @@ export const setDeep = ( switch (type) { case 'key': { const newKey = mapper(keyToRow); - const hadNewKey = parent.has(newKey); parent.set(newKey, parent.get(keyToRow)); if (newKey !== keyToRow) { @@ -163,10 +150,7 @@ export const setDeep = ( const currentContext = context.get(parent); if (currentContext) { - currentContext.delete(row); - if (!hadNewKey) { - currentContext.insertAtEnd(newKey); - } + currentContext[row] = newKey; } } break; diff --git a/src/avlTree.ts b/src/avlTree.ts deleted file mode 100644 index 293625f..0000000 --- a/src/avlTree.ts +++ /dev/null @@ -1,185 +0,0 @@ -type MaybeNode = AvlNode | null; - -class AvlNode { - value: T; - left: MaybeNode = null; - right: MaybeNode = null; - height: number = 1; - size: number = 1; - - constructor(value: T) { - this.value = value; - } -} - -function getHeight(n: MaybeNode): number { - return n ? n.height : 0; -} - -function getSize(n: MaybeNode): number { - return n ? n.size : 0; -} - -function updateNode(n: AvlNode): void { - const leftHeight = getHeight(n.left); - const rightHeight = getHeight(n.right); - n.height = (leftHeight > rightHeight ? leftHeight : rightHeight) + 1; - n.size = 1 + getSize(n.left) + getSize(n.right); -} - -function rotateRight(y: AvlNode): AvlNode { - const x = y.left!; - const t2 = x.right; - - x.right = y; - y.left = t2; - - updateNode(y); - updateNode(x); - return x; -} - -function rotateLeft(x: AvlNode): AvlNode { - const y = x.right!; - const t2 = y.left; - - y.left = x; - x.right = t2; - - updateNode(x); - updateNode(y); - return y; -} - -function rebalance(n: AvlNode): AvlNode { - updateNode(n); - const balanceFactor = getHeight(n.left) - getHeight(n.right); - - if (balanceFactor > 1) { - const l = n.left!; - if (getHeight(l.left) < getHeight(l.right)) n.left = rotateLeft(l); - return rotateRight(n); - } - - if (balanceFactor < -1) { - const r = n.right!; - if (getHeight(r.right) < getHeight(r.left)) n.right = rotateRight(r); - return rotateLeft(n); - } - - return n; -} - -function extractMin(n: AvlNode): [MaybeNode, AvlNode] { - if (!n.left) { - const right = n.right; - n.right = null; - updateNode(n); - return [right, n]; - } - const [newLeft, minNode] = extractMin(n.left); - n.left = newLeft; - return [rebalance(n), minNode]; -} - -function insertAt(n: MaybeNode, index: number, value: T): AvlNode { - if (!n) return new AvlNode(value); - - const leftSize = getSize(n.left); - if (index <= leftSize) { - n.left = insertAt(n.left, index, value); - } else { - n.right = insertAt(n.right, index - leftSize - 1, value); - } - return rebalance(n); -} - -function deleteAt(n: AvlNode, index: number): [MaybeNode, T] { - const leftSize = getSize(n.left); - - if (index < leftSize) { - const [newLeft, deleted] = deleteAt(n.left!, index); - n.left = newLeft; - return [rebalance(n), deleted]; - } - - if (index > leftSize) { - const [newRight, deleted] = deleteAt(n.right!, index - leftSize - 1); - n.right = newRight; - return [rebalance(n), deleted]; - } - - // delete this node - const deletedValue = n.value; - - if (!n.left) return [n.right, deletedValue]; - if (!n.right) return [n.left, deletedValue]; - - const [newRight, successor] = extractMin(n.right); - successor.left = n.left; - successor.right = newRight; - return [rebalance(successor), deletedValue]; -} - -function nodeAt(root: MaybeNode, index: number): AvlNode { - let cur = root; - let i = index; - while (cur) { - const leftSize = getSize(cur.left); - if (i < leftSize) { - cur = cur.left; - } else if (i === leftSize) { - return cur; - } else { - i -= leftSize + 1; - cur = cur.right; - } - } - throw new RangeError(`index ${index} out of range`); -} - -function buildBalancedFromArray(arr: readonly T[], lo: number, hi: number): MaybeNode { - if (lo >= hi) return null; - const mid = lo + ((hi - lo) >>> 1); - const n = new AvlNode(arr[mid]); - n.left = buildBalancedFromArray(arr, lo, mid); - n.right = buildBalancedFromArray(arr, mid + 1, hi); - updateNode(n); - return n; -} - -function assertIndex(index: number, size: number): void { - if (!Number.isSafeInteger(index)) { - throw new TypeError(`index must be a safe integer, got ${index}`); - } - if (index < 0 || index >= size) { - throw new RangeError(`index ${index} out of range`); - } -} - -export class IndexedListAVL { - private root: MaybeNode = null; - - constructor(init: Iterable | null = null) { - if (init) { - const arr = Array.from(init); - this.root = buildBalancedFromArray(arr, 0, arr.length); - } - } - - get(index: number): T { - assertIndex(index, getSize(this.root)); - return nodeAt(this.root, index).value; - } - - delete(index: number): T { - assertIndex(index, getSize(this.root)); - const [newRoot, deleted] = deleteAt(this.root!, index); - this.root = newRoot; - return deleted; - } - - insertAtEnd(value: T): void { - this.root = insertAt(this.root, getSize(this.root), value); - } -} diff --git a/src/index.test.ts b/src/index.test.ts index daea019..6f5b957 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -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: () => {