move cloning logic into new file, use structuredClone, add tests

pull/12413/head
Rich Harris 2 years ago
parent bf0d1efb00
commit 9940630210

@ -423,7 +423,10 @@ const global_visitors = {
} }
if (rune === '$state.snapshot') { if (rune === '$state.snapshot') {
return /** @type {import('estree').Expression} */ (context.visit(node.arguments[0])); return b.call(
'structuredClone',
/** @type {import('estree').Expression} */ (context.visit(node.arguments[0]))
);
} }
if (rune === '$state.is') { if (rune === '$state.is') {

@ -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);
});

@ -20,7 +20,7 @@ export function append_styles(anchor, css) {
var target = /** @type {ShadowRoot} */ (root).host var target = /** @type {ShadowRoot} */ (root).host
? /** @type {ShadowRoot} */ (root) ? /** @type {ShadowRoot} */ (root)
: /** @type {Document} */ (root).head; : /** @type {Document} */ (root).head ?? /** @type {Document} */ (root.ownerDocument).head;
if (!target.querySelector('#' + css.hash)) { if (!target.querySelector('#' + css.hash)) {
const style = document.createElement('style'); const style = document.createElement('style');

@ -4,7 +4,6 @@ import {
array_prototype, array_prototype,
define_property, define_property,
get_descriptor, get_descriptor,
get_descriptors,
get_prototype_of, get_prototype_of,
is_array, is_array,
is_frozen, is_frozen,
@ -94,63 +93,6 @@ export function proxy(value, parent = null, prev) {
return value; return value;
} }
/**
* @template {import('#client').ProxyStateObject} T
* @param {T} value
* @param {Map<T, Record<string | symbol, any>>} already_unwrapped
* @returns {Record<string | symbol, any>}
*/
function unwrap(value, already_unwrapped) {
if (typeof value === 'object' && value != null && STATE_SYMBOL in value) {
const unwrapped = already_unwrapped.get(value);
if (unwrapped !== undefined) {
return unwrapped;
}
if (is_array(value)) {
/** @type {Record<string | symbol, any>} */
const array = [];
already_unwrapped.set(value, array);
for (const element of value) {
array.push(unwrap(element, already_unwrapped));
}
return array;
} else {
/** @type {Record<string | symbol, any>} */
const obj = {};
const keys = Reflect.ownKeys(value);
const descriptors = get_descriptors(value);
already_unwrapped.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] = unwrap(property, already_unwrapped);
}
}
return obj;
}
}
return value;
}
/**
* @template T
* @param {T} value
* @returns {T}
*/
export function snapshot(value) {
return /** @type {T} */ (
unwrap(/** @type {import('#client').ProxyStateObject} */ (value), new Map())
);
}
/** /**
* @param {import('#client').Source<number>} signal * @param {import('#client').Source<number>} signal
* @param {1 | -1} [d] * @param {1 | -1} [d]

Loading…
Cancel
Save