import {isArrayOrPlainObject, isTypedArray} from '../internal/is'; import {getSelfHandlers} from '../internal/value/handlers'; import type {ArrayOrPlainObject, Constructor, PlainObject, TypedArray} from '../models'; // #region Special variables const CLONE_NAME = 'clone'; // #endregion // #region Functions /** * Clone any kind of value _(shallowly)_ * * @param value Value to clone * @param flat Clone only the value itself, without cloning nested values * @returns Cloned value */ export function clone(value: Value, flat: true): Value; /** * Clone any kind of value _(deeply, if needed)_ * * @param value Value to clone * @returns Cloned value */ export function clone(value: Value): Value; export function clone(value: unknown, flat?: boolean): unknown { return flat === true ? copy(value) : cloneValue(value, 0, new WeakMap(), false); } clone.handlers = getSelfHandlers(clone, { callback: tryStructuredClone, method: CLONE_NAME, }); clone.deregister = deregisterCloner; clone.register = registerCloner; function cloneArrayBuffer( value: ArrayBuffer, depth?: number, references?: WeakMap, ): ArrayBuffer { if (typeof depth === 'number' && depth >= MAX_CLONE_DEPTH) { return value; } const cloned = new ArrayBuffer(value.byteLength); new Uint8Array(cloned).set(new Uint8Array(value)); references?.set(value, cloned); return cloned; } function cloneDataView( value: DataView, depth: number, references: WeakMap, ): DataView { if (depth >= MAX_CLONE_DEPTH) { return value; } const buffer = cloneArrayBuffer(value.buffer as ArrayBuffer); const cloned = new DataView(buffer, value.byteOffset, value.byteLength); references.set(value, cloned); return cloned; } function cloneMap( map: Map, depth: number, references: WeakMap, flat: boolean, ): Map { if (depth >= MAX_CLONE_DEPTH) { return map; } const cloned = new Map(); const entries = map.entries(); for (const entry of entries) { cloned.set( flat ? entry[0] : cloneValue(entry[0], depth + 1, references, false), flat ? entry[1] : cloneValue(entry[1], depth + 1, references, false), ); } references.set(map, cloned); return cloned; } function cloneNode(node: Node, depth: number, references: WeakMap): Node { if (depth >= MAX_CLONE_DEPTH) { return node; } const cloned = node.cloneNode(true); references.set(node, cloned); return cloned; } function cloneObject( value: ArrayOrPlainObject, depth: number, references: WeakMap, flat: boolean, ): ArrayOrPlainObject { if (flat || depth >= MAX_CLONE_DEPTH) { return Array.isArray(value) ? value.slice() : {...value}; } const cloned = (Array.isArray(value) ? [] : {}) as PlainObject; const keys = [...Object.keys(value), ...Object.getOwnPropertySymbols(value)]; const {length} = keys; for (let index = 0; index < length; index += 1) { const key = keys[index]; cloned[key] = cloneValue((value as PlainObject)[key], depth + 1, references, false); } references.set(value, cloned); return cloned; } function cloneRegularExpression( value: RegExp, depth: number, references: WeakMap, ): RegExp { if (depth >= MAX_CLONE_DEPTH) { return value; } const cloned = new RegExp(value.source, value.flags); cloned.lastIndex = value.lastIndex; references.set(value, cloned); return cloned; } function cloneSet( set: Set, depth: number, references: WeakMap, flat: boolean, ): Set { if (depth >= MAX_CLONE_DEPTH) { return set; } const cloned = new Set(); const values = [...set.values()]; const {length} = values; for (let index = 0; index < length; index += 1) { cloned.add(flat ? values[index] : cloneValue(values[index], depth + 1, references, false)); } references.set(set, cloned); return cloned; } function cloneTypedArray( value: TypedArray, depth: number, references: WeakMap, ): TypedArray { if (depth >= MAX_CLONE_DEPTH) { return value; } const cloned = new (value.constructor as new (...args: unknown[]) => TypedArray)(value); references.set(value, cloned); return cloned as TypedArray; } function cloneValue( value: unknown, depth: number, references: WeakMap, flat: boolean, ): unknown { switch (true) { case value == null: case typeof value === 'bigint': case typeof value === 'boolean': case typeof value === 'number': case typeof value === 'string': return value; case typeof value === 'function': return; case typeof value === 'symbol': return Symbol(value.description); case references.has(value as object): return references.get(value); case value instanceof ArrayBuffer: return cloneArrayBuffer(value, depth, references); case value instanceof DataView: return cloneDataView(value, depth, references); case value instanceof Date: return new Date(value.getTime()); case value instanceof RegExp: return cloneRegularExpression(value, depth, references); case value instanceof Map: return cloneMap(value, depth, references, flat); case typeof Node !== 'undefined' && value instanceof Node: return cloneNode(value, depth, references); case value instanceof Set: return cloneSet(value, depth, references, flat); case isArrayOrPlainObject(value): return cloneObject(value, depth, references, flat); case isTypedArray(value): return cloneTypedArray(value, depth, references); default: return clone.handlers.handle(value, depth, references); } } /** * Copy any kind of value * * - Clones the value shallowly, without cloning nested values * - To copy a value deeply, use `clone` instead * * @param value Value to copy * @returns Copied value */ export function copy(value: Value): Value { return cloneValue(value, 0, new WeakMap(), true) as Value; } /** * Deregister a clone handler for a specific class * * _Available as `deregisterCloner` and `template.deregister`_ * * @param constructor Class constructor */ export function deregisterCloner(constructor: Constructor): void { clone.handlers.deregister(constructor); } /** * Register a clone handler for a specific class * * _Available as `registerCloner` and `template.register`_ * * @param constructor Class constructor * @param handler Method name or clone function _(defaults to method name `clone`)_ */ export function registerCloner( constructor: Constructor, handler?: string | ((value: Instance) => Instance), ): void { clone.handlers.register(constructor, handler); } function tryStructuredClone( value: object, depth: number, references: WeakMap, ): unknown { if (depth >= MAX_CLONE_DEPTH) { return value; } try { const cloned = structuredClone(value); references.set(value, cloned); return cloned; } catch { references.set(value, value); return value; } } // #endregion // #region Variables const MAX_CLONE_DEPTH = 100; // #endregion