From e0808de2707575563b7f33ec2a5412f39be5f4ea Mon Sep 17 00:00:00 2001 From: Kylesoda <249518290+kylesoda@users.noreply.github.com> Date: Fri, 20 Feb 2026 23:25:15 -0500 Subject: [PATCH] feat: implement deepClone function for deep cloning of objects and arrays --- lib/shared/helpers/deep-clone.test.ts | 145 ++++++++++++++++++++++++++ lib/shared/helpers/deep-clone.ts | 93 +++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 lib/shared/helpers/deep-clone.test.ts create mode 100644 lib/shared/helpers/deep-clone.ts diff --git a/lib/shared/helpers/deep-clone.test.ts b/lib/shared/helpers/deep-clone.test.ts new file mode 100644 index 0000000..0c86779 --- /dev/null +++ b/lib/shared/helpers/deep-clone.test.ts @@ -0,0 +1,145 @@ +import { describe, expect, it } from 'vitest' +import { deepClone } from '@/shared/helpers/deep-clone' + +describe('deepClone', () => { + it('clona valores primitivos', () => { + expect(deepClone(42)).toBe(42) + expect(deepClone('abc')).toBe('abc') + expect(deepClone(true)).toBe(true) + expect(deepClone(null)).toBeNull() + expect(deepClone(undefined)).toBeUndefined() + expect(deepClone(123n)).toBe(123n) + }) + + it('clona arrays y objetos anidados (referencias diferentes)', () => { + const obj = { a: 1, b: { c: 2 } } + const arr = [1, { x: 2 }] + const src = { obj, arr } + + const cloned = deepClone(src) + expect(cloned).not.toBe(src) + expect(cloned.obj).not.toBe(src.obj) + expect(cloned.arr).not.toBe(src.arr) + expect(cloned).toEqual(src) + + // mutar clon no debe afectar al original + cloned.obj.b.c = 99 + expect(src.obj.b.c).toBe(2) + }) + + it('maneja Date y RegExp', () => { + const d = new Date() + const r = /abc/gi + + const cd = deepClone(d) + const cr = deepClone(r) + + expect(cd).not.toBe(d) + expect(cd.getTime()).toBe(d.getTime()) + + expect(cr).not.toBe(r) + expect(cr.source).toBe(r.source) + expect(cr.flags).toBe(r.flags) + }) + + it('clona Map y Set (clonando claves/valores)', () => { + const keyObj = { k: 'v' } + const m = new Map([[keyObj, { nested: 1 }]]) + const s = new Set([keyObj, 2, 'x']) + + const cm = deepClone(m) + const cs = deepClone(s) + + expect(cm).not.toBe(m) + expect(cs).not.toBe(s) + + // Map: la clave será un objeto distinto pero con los mismos datos + const [[clonedKey, clonedVal]] = Array.from(cm.entries()) + expect(clonedKey).not.toBe(keyObj) + expect(clonedKey).toEqual(keyObj) + expect(clonedVal).toEqual({ nested: 1 }) + + // Set: debe contener un objeto equivalente al original + const found = Array.from(cs.values()).find( + (v) => typeof v === 'object' && (v as any).k === 'v', + ) + expect(found).toBeDefined() + expect(found).not.toBe(keyObj) + }) + + it('clona TypedArray, ArrayBuffer y DataView', () => { + const buf = new ArrayBuffer(8) + const dv = new DataView(buf) + dv.setInt8(0, 42) + const ta = new Uint8Array([1, 2, 3]) + + const cbuf = deepClone(buf) + const cdv = deepClone(dv) + const cta = deepClone(ta) + + expect(cbuf).not.toBe(buf) + expect(cdv).not.toBe(dv) + expect(cdv.getInt8(0)).toBe(42) + + expect(cta).not.toBe(ta) + expect(Array.from(cta)).toEqual([1, 2, 3]) + expect(cta).toBeInstanceOf(Uint8Array) + }) + + it('preserva prototype y métodos de instancia', () => { + class C { + a = 1 + method() { + return this.a + } + } + const inst = new C() + inst.a = 5 + + const cloned = deepClone(inst as any) + expect(cloned).not.toBe(inst) + expect(cloned).toBeInstanceOf(C) + expect(cloned.method()).toBe(5) + }) + + it('mantiene referencias a funciones (no las clona)', () => { + const fn = () => 1 + const src = { fn } + const cloned = deepClone(src as any) + expect(cloned.fn).toBe(fn) + }) + + it('maneja referencias circulares', () => { + const a: any = { name: 'a' } + a.self = a + + const c = deepClone(a) + expect(c).not.toBe(a) + expect(c.self).toBe(c) + expect(c.name).toBe('a') + }) + + it('preserva descriptores y propiedades con símbolos', () => { + const s = Symbol('sym') + const obj: any = {} + + Object.defineProperty(obj, 'hidden', { + value: 42, + enumerable: false, + configurable: true, + writable: true, + }) + obj[s] = { foo: 'bar' } + + const c = deepClone(obj) + + const desc = Object.getOwnPropertyDescriptor(c, 'hidden')! + expect(desc.enumerable).toBe(false) + expect(desc.value).toBe(42) + + const symKeys = Object.getOwnPropertySymbols(c) + expect(symKeys.length).toBe(1) + expect(c[s]).toEqual({ foo: 'bar' }) + expect(c[s]).not.toBe(obj[s]) + }) +}) diff --git a/lib/shared/helpers/deep-clone.ts b/lib/shared/helpers/deep-clone.ts new file mode 100644 index 0000000..886e012 --- /dev/null +++ b/lib/shared/helpers/deep-clone.ts @@ -0,0 +1,93 @@ +export function deepClone(value: T): T { + const seen = new WeakMap() + + const getRegExpFlags = (r: RegExp) => { + let flags = '' + if (r.global) flags += 'g' + if (r.ignoreCase) flags += 'i' + if (r.multiline) flags += 'm' + if (r.dotAll) flags += 's' + if (r.unicode) flags += 'u' + if (r.sticky) flags += 'y' + return flags + } + + function _clone(v: any): any { + if (v === null || typeof v !== 'object') { + return v + } + + if (seen.has(v)) { + return seen.get(v) + } + + if (v instanceof Date) { + return new Date(v.getTime()) + } + + if (v instanceof RegExp) { + return new RegExp(v.source, getRegExpFlags(v)) + } + + if (v instanceof ArrayBuffer) { + return v.slice(0) + } + + if (ArrayBuffer.isView(v)) { + if (v instanceof DataView) { + const buf = _clone(v.buffer) + return new DataView(buf, v.byteOffset, v.byteLength) + } + + return new (v.constructor as any)(v) + } + + if (v instanceof Map) { + const m = new Map() + seen.set(v, m) + for (const [k, val] of v.entries()) { + m.set(_clone(k), _clone(val)) + } + return m + } + + if (v instanceof Set) { + const s = new Set() + seen.set(v, s) + for (const item of v.values()) s.add(_clone(item)) + return s + } + + if (Array.isArray(v)) { + const arr: any[] = [] + seen.set(v, arr) + for (let i = 0; i < v.length; i++) arr[i] = _clone(v[i]) + return arr as any + } + + if (typeof v === 'function') { + return v + } + + const proto = Object.getPrototypeOf(v) + const out = Object.create(proto) + seen.set(v, out) + + const descriptors = Object.getOwnPropertyDescriptors(v) + for (const [key, desc] of Object.entries(descriptors)) { + if ('value' in desc) desc.value = _clone(desc.value) + Object.defineProperty(out, key, desc as PropertyDescriptor) + } + + const symbols = Object.getOwnPropertySymbols(v) + for (const s of symbols) { + const sd = Object.getOwnPropertyDescriptor(v, s)! + if (sd && 'value' in sd) sd.value = _clone(sd.value) + Object.defineProperty(out, s, sd as PropertyDescriptor) + } + + return out + } + + return _clone(value) as T +}