All files / src/internal/shared clone.js

92.92% Statements 105/113
80.64% Branches 25/31
100% Functions 2/2
92.72% Lines 102/110

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 1112x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 11x 11x 11x 11x 11x 11x 1x 1x 11x 2x 2x 2x 2x 2x 2x 2x 2x 2x 11x 11x 11x       2x 2x 2x 2x 2x 2x 2x 2x 2x 135x 135x 14x 14x 13x 14x 3x 3x 3x 3x 111x 111x 3x 3x 3x 10x 14x 7x 7x 7x 7x 7x 12x 12x 12x 7x 7x 7x 3x 14x     3x 14x 1x 1x 1x 1x 1x 1x 1x 14x 123x 135x       123x 123x 123x 135x 111x 111x 111x 111x 111x 111x 135x  
/** @import { Snapshot } from './types' */
import { DEV } from 'esm-env';
import * as w from './warnings.js';
import { get_prototype_of, is_array, object_prototype } from './utils.js';
 
/**
 * In dev, we keep track of which properties could not be cloned. In prod
 * we don't bother, but we keep a dummy array around so that the
 * signature stays the same
 * @type {string[]}
 */
const empty = [];
 
/**
 * @template T
 * @param {T} value
 * @returns {Snapshot<T>}
 */
export function snapshot(value) {
	if (DEV) {
		/** @type {string[]} */
		const paths = [];
 
		const copy = clone(value, new Map(), '', paths);
		if (paths.length === 1 && paths[0] === '') {
			// value could not be cloned
			w.state_snapshot_uncloneable();
		} else if (paths.length > 0) {
			// some properties could not be cloned
			const slice = paths.length > 10 ? paths.slice(0, 7) : paths.slice(0, 10);
			const excess = paths.length - slice.length;
 
			let uncloned = slice.map((path) => `- <value>${path}`).join('\n');
			if (excess > 0) uncloned += `\n- ...and ${excess} more`;
 
			w.state_snapshot_uncloneable(uncloned);
		}
 
		return copy;
	}

	return clone(value, new Map(), '', empty);
}
 
/**
 * @template T
 * @param {T} value
 * @param {Map<T, Snapshot<T>>} cloned
 * @param {string} path
 * @param {string[]} paths
 * @returns {Snapshot<T>}
 */
function clone(value, cloned, path, paths) {
	if (typeof value === 'object' && value !== null) {
		const unwrapped = cloned.get(value);
		if (unwrapped !== undefined) return unwrapped;
 
		if (is_array(value)) {
			const copy = /** @type {Snapshot<any>} */ ([]);
			cloned.set(value, copy);
 
			for (let i = 0; i < value.length; i += 1) {
				copy.push(clone(value[i], cloned, DEV ? `${path}[${i}]` : path, paths));
			}
 
			return copy;
		}
 
		if (get_prototype_of(value) === object_prototype) {
			/** @type {Snapshot<any>} */
			const copy = {};
			cloned.set(value, copy);
 
			for (var key in value) {
				// @ts-expect-error
				copy[key] = clone(value[key], cloned, DEV ? `${path}.${key}` : path, paths);
			}
 
			return copy;
		}
 
		if (value instanceof Date) {
			return /** @type {Snapshot<T>} */ (structuredClone(value));
		}
 
		if (typeof (/** @type {T & { toJSON?: any } } */ (value).toJSON) === 'function') {
			return clone(
				/** @type {T & { toJSON(): any } } */ (value).toJSON(),
				cloned,
				DEV ? `${path}.toJSON()` : path,
				paths
			);
		}
	}
 
	if (value instanceof EventTarget) {
		// can't be cloned
		return /** @type {Snapshot<T>} */ (value);
	}
 
	try {
		return /** @type {Snapshot<T>} */ (structuredClone(value));
	} catch (e) {
		if (DEV) {
			paths.push(path);
		}
 
		return /** @type {Snapshot<T>} */ (value);
	}
}