mirror of https://github.com/sveltejs/svelte
parent
bf0d1efb00
commit
9940630210
@ -0,0 +1,69 @@
|
|||||||
|
import { STATE_SYMBOL } from './constants.js';
|
||||||
|
import {
|
||||||
|
define_property,
|
||||||
|
get_descriptors,
|
||||||
|
get_prototype_of,
|
||||||
|
is_array,
|
||||||
|
object_prototype
|
||||||
|
} from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {T} value
|
||||||
|
* @returns {T}
|
||||||
|
*/
|
||||||
|
export function snapshot(value) {
|
||||||
|
return /** @type {T} */ (
|
||||||
|
clone(/** @type {import('#client').ProxyStateObject} */ (value), new Map())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {import('#client').ProxyStateObject} T
|
||||||
|
* @param {T} value
|
||||||
|
* @param {Map<T, Record<string | symbol, any>>} cloned
|
||||||
|
* @returns {Record<string | symbol, any>}
|
||||||
|
*/
|
||||||
|
function clone(value, cloned) {
|
||||||
|
if (typeof value === 'object' && value != null) {
|
||||||
|
const unwrapped = cloned.get(value);
|
||||||
|
if (unwrapped !== undefined) {
|
||||||
|
return unwrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array(value)) {
|
||||||
|
/** @type {Record<string | symbol, any>} */
|
||||||
|
const array = [];
|
||||||
|
cloned.set(value, array);
|
||||||
|
for (const element of value) {
|
||||||
|
array.push(clone(element, cloned));
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
} else if (get_prototype_of(value) === object_prototype) {
|
||||||
|
/** @type {Record<string | symbol, any>} */
|
||||||
|
const obj = {};
|
||||||
|
const keys = Reflect.ownKeys(value);
|
||||||
|
const descriptors = get_descriptors(value);
|
||||||
|
cloned.set(value, obj);
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
if (key === STATE_SYMBOL) continue;
|
||||||
|
if (descriptors[key].get) {
|
||||||
|
define_property(obj, key, descriptors[key]);
|
||||||
|
} else {
|
||||||
|
/** @type {T} */
|
||||||
|
const property = value[key];
|
||||||
|
obj[key] = clone(property, cloned);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value.toJSON === 'function') {
|
||||||
|
return clone(value.toJSON(), cloned);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return structuredClone(value);
|
||||||
|
}
|
||||||
@ -0,0 +1,102 @@
|
|||||||
|
import { snapshot } from './clone';
|
||||||
|
import { assert, test } from 'vitest';
|
||||||
|
import { proxy } from './proxy';
|
||||||
|
|
||||||
|
test('primitive', () => {
|
||||||
|
assert.equal(42, snapshot(42));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('array', () => {
|
||||||
|
const array = [1, 2, 3];
|
||||||
|
const copy = snapshot(array);
|
||||||
|
|
||||||
|
assert.deepEqual(copy, array);
|
||||||
|
assert.notEqual(copy, array);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('object', () => {
|
||||||
|
const object = { a: 1, b: 2, c: 3 };
|
||||||
|
const copy = snapshot(object);
|
||||||
|
|
||||||
|
assert.deepEqual(copy, object);
|
||||||
|
assert.notEqual(copy, object);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('proxied state', () => {
|
||||||
|
const object = proxy({
|
||||||
|
a: {
|
||||||
|
b: {
|
||||||
|
c: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const copy = snapshot(object);
|
||||||
|
|
||||||
|
assert.deepEqual(copy, object);
|
||||||
|
assert.notEqual(copy, object);
|
||||||
|
|
||||||
|
object.a.b.c = 2;
|
||||||
|
assert.equal(copy.a.b.c, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cycles', () => {
|
||||||
|
const object: { self?: any } = {};
|
||||||
|
object.self = object;
|
||||||
|
const copy = snapshot(object);
|
||||||
|
|
||||||
|
assert.equal(copy.self, copy);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('class with state field', () => {
|
||||||
|
class Foo {
|
||||||
|
x = 1;
|
||||||
|
#y = 2;
|
||||||
|
|
||||||
|
get y() {
|
||||||
|
return this.#y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const copy = snapshot(new Foo());
|
||||||
|
|
||||||
|
assert.deepEqual(copy, { x: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('class with toJSON', () => {
|
||||||
|
class Foo {
|
||||||
|
x = 1;
|
||||||
|
#y = 2;
|
||||||
|
|
||||||
|
get y() {
|
||||||
|
return this.#y;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
x: this.x,
|
||||||
|
y: this.y
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const copy = snapshot(new Foo());
|
||||||
|
|
||||||
|
assert.deepEqual(copy, { x: 1, y: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reactive class', () => {
|
||||||
|
class SvelteMap<T, U> extends Map<T, U> {
|
||||||
|
constructor(init?: Iterable<[T, U]>) {
|
||||||
|
super(init);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = new SvelteMap([[1, 2]]);
|
||||||
|
const copy = snapshot(map);
|
||||||
|
|
||||||
|
assert.ok(copy instanceof Map);
|
||||||
|
assert.notOk(copy instanceof SvelteMap);
|
||||||
|
|
||||||
|
assert.equal(copy.get(1), 2);
|
||||||
|
});
|
||||||
Loading…
Reference in new issue