diff --git a/benchmark/circles-fixture.js b/benchmark/circles-fixture.js new file mode 100644 index 0000000..f4456db --- /dev/null +++ b/benchmark/circles-fixture.js @@ -0,0 +1,64 @@ +'use strict' + +function buildPeopleGraph () { + const people = [] + + function createPerson (name, age) { + return { + name, + age, + friends: [], + parents: [], + children: [] + } + } + + // Populate the array with people + people.push(createPerson('John', 50)) + people.push(createPerson('Jane', 48)) + people.push(createPerson('Mike', 28)) + people.push(createPerson('Sara', 26)) + people.push(createPerson('Tom', 30)) + people.push(createPerson('Lily', 27)) + people.push(createPerson('Emily', 5)) + people.push(createPerson('Jack', 3)) + people.push(createPerson('Alice', 65)) + people.push(createPerson('Bob', 66)) + + // Define relationships + + // John and Jane are parents of Mike and Sara + people[0].children.push(people[2], people[3]) // John's children + people[1].children.push(people[2], people[3]) // Jane's children + people[2].parents.push(people[0], people[1]) // Mike's parents + people[3].parents.push(people[0], people[1]) // Sara's parents + + // Tom and Lily are parents of Emily and Jack + people[4].children.push(people[6], people[7]) // Tom's children + people[5].children.push(people[6], people[7]) // Lily's children + people[6].parents.push(people[4], people[5]) // Emily's parents + people[7].parents.push(people[4], people[5]) // Jack's parents + + // Alice and Bob are John's and Jane's parents (grandparents) + people[0].parents.push(people[8], people[9]) // John's parents + people[1].parents.push(people[8], people[9]) // Jane's parents + people[8].children.push(people[0], people[1]) // Alice's children + people[9].children.push(people[0], people[1]) // Bob's children + + // Add friends relationships (mutual circular relationships) + people[2].friends.push(people[4]) // Mike and Tom are friends + people[4].friends.push(people[2]) + + people[3].friends.push(people[5]) // Sara and Lily are friends + people[5].friends.push(people[3]) + + people[0].friends.push(people[9]) // John and Bob (his father) are friends + people[9].friends.push(people[0]) + + people[1].friends.push(people[8]) // Jane and Alice (her mother) are friends + people[8].friends.push(people[1]) + + return people +} + +module.exports = buildPeopleGraph() diff --git a/benchmark/circles.js b/benchmark/circles.js new file mode 100644 index 0000000..1c9c353 --- /dev/null +++ b/benchmark/circles.js @@ -0,0 +1,61 @@ +'use strict' + +/* global structuredClone */ + +const bench = require('fastbench') +const lodashCloneDeep = require('lodash.clonedeep') +const fastCopy = require('fast-copy').default +const obj = require('./circles-fixture') +const clone = require('..') +const nanoCopy = require('nano-copy') +const ramdaClone = require('ramda').clone +const cloneCircles = clone({ circles: true }) +const cloneCirclesProto = clone({ circles: true, proto: true }) +const max = 1000 + +const run = bench([ + function benchLodashCloneDeep (cb) { + for (let i = 0; i < max; i++) { + lodashCloneDeep(obj) + } + setImmediate(cb) + }, + function benchFastCopy (cb) { + for (let i = 0; i < max; i++) { + fastCopy(obj) + } + setImmediate(cb) + }, + function benchNanoCopy (cb) { + for (let i = 0; i < max; i++) { + nanoCopy(obj) + } + setImmediate(cb) + }, + function benchRamdaClone (cb) { + for (let i = 0; i < max; i++) { + ramdaClone(obj) + } + setImmediate(cb) + }, + function benchRfdcCircles (cb) { + for (let i = 0; i < max; i++) { + cloneCircles(obj) + } + setImmediate(cb) + }, + function benchRfdcCirclesProto (cb) { + for (let i = 0; i < max; i++) { + cloneCirclesProto(obj) + } + setImmediate(cb) + }, + function benchStructuredClone (cb) { + for (let i = 0; i < max; i++) { + structuredClone(obj) + } + setImmediate(cb) + } +], 100) + +run(run) diff --git a/index.js b/index.js index a964561..2d28752 100644 --- a/index.js +++ b/index.js @@ -93,13 +93,10 @@ function rfdc (opts) { } function rfdcCircles (opts) { - const refs = [] - const refsNew = [] - const constructorHandlers = new Map() constructorHandlers.set(Date, (o) => new Date(o)) - constructorHandlers.set(Map, (o, fn) => new Map(cloneArray(Array.from(o), fn))) - constructorHandlers.set(Set, (o, fn) => new Set(cloneArray(Array.from(o), fn))) + constructorHandlers.set(Map, (o, refs, fn) => new Map(cloneArray(Array.from(o), refs, fn))) + constructorHandlers.set(Set, (o, refs, fn) => new Set(cloneArray(Array.from(o), refs, fn))) if (opts.constructorHandlers) { for (const handler of opts.constructorHandlers) { constructorHandlers.set(handler[0], handler[1]) @@ -109,7 +106,7 @@ function rfdcCircles (opts) { let handler = null return opts.proto ? cloneProto : clone - function cloneArray (a, fn) { + function cloneArray (a, refs, fn) { const keys = Object.keys(a) const a2 = new Array(keys.length) for (let i = 0; i < keys.length; i++) { @@ -117,82 +114,91 @@ function rfdcCircles (opts) { const cur = a[k] if (typeof cur !== 'object' || cur === null) { a2[k] = cur - } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { - a2[k] = handler(cur, fn) - } else if (ArrayBuffer.isView(cur)) { - a2[k] = copyBuffer(cur) } else { - const index = refs.indexOf(cur) - if (index !== -1) { - a2[k] = refsNew[index] + const ref = refs.get(cur) + if (ref !== undefined) { + a2[k] = ref } else { - a2[k] = fn(cur) + let cur2 + if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { + cur2 = handler(cur, refs, fn) + } else if (ArrayBuffer.isView(cur)) { + cur2 = copyBuffer(cur) + } else { + cur2 = fn(cur, refs) + } + refs.set(cur, cur2) + a2[k] = cur2 } } } return a2 } - function clone (o) { + function clone (o, refs = new Map()) { if (typeof o !== 'object' || o === null) return o - if (Array.isArray(o)) return cloneArray(o, clone) + if (Array.isArray(o)) return cloneArray(o, refs, clone) if (o.constructor !== Object && (handler = constructorHandlers.get(o.constructor))) { - return handler(o, clone) + return handler(o, refs, clone) } const o2 = {} - refs.push(o) - refsNew.push(o2) + refs.set(o, o2) for (const k in o) { if (Object.hasOwnProperty.call(o, k) === false) continue const cur = o[k] if (typeof cur !== 'object' || cur === null) { o2[k] = cur - } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { - o2[k] = handler(cur, clone) - } else if (ArrayBuffer.isView(cur)) { - o2[k] = copyBuffer(cur) } else { - const i = refs.indexOf(cur) - if (i !== -1) { - o2[k] = refsNew[i] + const ref = refs.get(cur) + if (ref !== undefined) { + o2[k] = ref } else { - o2[k] = clone(cur) + let cur2 + if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { + cur2 = handler(cur, refs, clone) + } else if (ArrayBuffer.isView(cur)) { + cur2 = copyBuffer(cur) + } else { + cur2 = clone(cur, refs) + } + refs.set(cur, cur2) + o2[k] = cur2 } } } - refs.pop() - refsNew.pop() return o2 } - function cloneProto (o) { + function cloneProto (o, refs = new Map()) { if (typeof o !== 'object' || o === null) return o - if (Array.isArray(o)) return cloneArray(o, cloneProto) + if (Array.isArray(o)) return cloneArray(o, refs, cloneProto) if (o.constructor !== Object && (handler = constructorHandlers.get(o.constructor))) { - return handler(o, cloneProto) + return handler(o, refs, cloneProto) } const o2 = {} - refs.push(o) - refsNew.push(o2) + refs.set(o, o2) for (const k in o) { const cur = o[k] if (typeof cur !== 'object' || cur === null) { o2[k] = cur - } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { - o2[k] = handler(cur, cloneProto) - } else if (ArrayBuffer.isView(cur)) { - o2[k] = copyBuffer(cur) } else { - const i = refs.indexOf(cur) - if (i !== -1) { - o2[k] = refsNew[i] + const ref = refs.get(cur) + if (ref !== undefined) { + o2[k] = ref } else { - o2[k] = cloneProto(cur) + let cur2 + if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { + cur2 = handler(cur, refs, cloneProto) + } else if (ArrayBuffer.isView(cur)) { + cur2 = copyBuffer(cur) + } else { + cur2 = cloneProto(cur, refs) + } + refs.set(cur, cur2) + o2[k] = cur2 } } } - refs.pop() - refsNew.pop() return o2 } } diff --git a/test/index.js b/test/index.js index 5bdd9ac..da27e6f 100644 --- a/test/index.js +++ b/test/index.js @@ -120,6 +120,27 @@ test('custom constructor handler', async ({ same, ok, isNot }) => { same(cloned.foo.s, data.foo.s, 'same values') isNot(cloned.foo, data.foo, 'different objects') }) +test('custom constructor handler - circular objects', async ({ same, ok, is, isNot }) => { + class Foo { + constructor (s) { + this.s = s + } + } + const a = { foo: new Foo('foo1') } + const b = { foo: new Foo('foo2') } + a.other = b + b.other = a + const data = { a, b } + const cloned = rfdc({ circles: true, constructorHandlers: [[Foo, (o) => new Foo(o.s)]] })(data) + ok(cloned.a.foo instanceof Foo) + ok(cloned.b.foo instanceof Foo) + same(cloned.a.foo.s, data.a.foo.s, 'same values') + same(cloned.b.foo.s, data.b.foo.s, 'same values') + isNot(cloned.a.foo, data.a.foo, 'different objects') + isNot(cloned.b.foo, data.b.foo, 'different objects') + is(cloned.a.other, cloned.b, 'same objects') + is(cloned.b.other, cloned.a, 'same objects') +}) test('custom RegExp handler', async ({ same, ok, isNot }) => { const data = { regex: /foo/ } const cloned = rfdc({ constructorHandlers: [[RegExp, (o) => new RegExp(o)]] })(data)