mirror of https://github.com/sveltejs/svelte
breaking: use `structuredClone` inside `$state.snapshot` (#12413)
* move cloning logic into new file, use structuredClone, add tests * changeset * breaking * tweak * use same cloning approach between server and client * get types mostly working * fix type error that popped up * cheeky hack * we no longer need deep_snapshot * shallow copy state when freezing * throw if argument is a state proxy * docs * regeneratepull/12433/head
parent
e8453e75c6
commit
8d3c0266ce
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
breaking: use structuredClone inside `$state.snapshot`
|
@ -0,0 +1,40 @@
|
|||||||
|
import { DEV } from 'esm-env';
|
||||||
|
import { define_property, is_array, is_frozen, object_freeze } from '../shared/utils.js';
|
||||||
|
import { STATE_FROZEN_SYMBOL, STATE_SYMBOL } from './constants.js';
|
||||||
|
import * as e from './errors.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expects a value that was wrapped with `freeze` and makes it frozen in DEV.
|
||||||
|
* @template T
|
||||||
|
* @param {T} value
|
||||||
|
* @returns {Readonly<T>}
|
||||||
|
*/
|
||||||
|
export function freeze(value) {
|
||||||
|
if (
|
||||||
|
typeof value === 'object' &&
|
||||||
|
value !== null &&
|
||||||
|
!is_frozen(value) &&
|
||||||
|
!(STATE_FROZEN_SYMBOL in value)
|
||||||
|
) {
|
||||||
|
var copy = /** @type {T} */ (value);
|
||||||
|
|
||||||
|
if (STATE_SYMBOL in value) {
|
||||||
|
e.state_frozen_invalid_argument();
|
||||||
|
}
|
||||||
|
|
||||||
|
define_property(copy, STATE_FROZEN_SYMBOL, {
|
||||||
|
value: true,
|
||||||
|
writable: true,
|
||||||
|
enumerable: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Freeze the object in DEV
|
||||||
|
if (DEV) {
|
||||||
|
object_freeze(copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
return /** @type {Readonly<T>} */ (copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import { freeze } from './freeze.js';
|
||||||
|
import { assert, test } from 'vitest';
|
||||||
|
import { proxy } from './proxy.js';
|
||||||
|
|
||||||
|
test('freezes an object', () => {
|
||||||
|
const frozen = freeze({ a: 1 });
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
// @ts-expect-error
|
||||||
|
frozen.a += 1;
|
||||||
|
}, /Cannot assign to read only property/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws if argument is a state proxy', () => {
|
||||||
|
assert.throws(() => freeze(proxy({})), /state_frozen_invalid_argument/);
|
||||||
|
});
|
@ -1,22 +0,0 @@
|
|||||||
// Store the references to globals in case someone tries to monkey patch these, causing the below
|
|
||||||
// to de-opt (this occurs often when using popular extensions).
|
|
||||||
export var is_array = Array.isArray;
|
|
||||||
export var array_from = Array.from;
|
|
||||||
export var object_keys = Object.keys;
|
|
||||||
export var object_assign = Object.assign;
|
|
||||||
export var is_frozen = Object.isFrozen;
|
|
||||||
export var object_freeze = Object.freeze;
|
|
||||||
export var define_property = Object.defineProperty;
|
|
||||||
export var get_descriptor = Object.getOwnPropertyDescriptor;
|
|
||||||
export var get_descriptors = Object.getOwnPropertyDescriptors;
|
|
||||||
export var object_prototype = Object.prototype;
|
|
||||||
export var array_prototype = Array.prototype;
|
|
||||||
export var get_prototype_of = Object.getPrototypeOf;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {any} thing
|
|
||||||
* @returns {thing is Function}
|
|
||||||
*/
|
|
||||||
export function is_function(thing) {
|
|
||||||
return typeof thing === 'function';
|
|
||||||
}
|
|
@ -0,0 +1,54 @@
|
|||||||
|
/** @import { Snapshot } from './types' */
|
||||||
|
import { get_prototype_of, is_array, object_prototype } from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {T} value
|
||||||
|
* @returns {Snapshot<T>}
|
||||||
|
*/
|
||||||
|
export function snapshot(value) {
|
||||||
|
return clone(value, new Map());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {T} value
|
||||||
|
* @param {Map<T, Snapshot<T>>} cloned
|
||||||
|
* @returns {Snapshot<T>}
|
||||||
|
*/
|
||||||
|
function clone(value, cloned) {
|
||||||
|
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 (const element of value) {
|
||||||
|
copy.push(clone(element, cloned));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof (/** @type {T & { toJSON?: any } } */ (value).toJSON) === 'function') {
|
||||||
|
return clone(/** @type {T & { toJSON(): any } } */ (value).toJSON(), cloned);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return /** @type {Snapshot<T>} */ (structuredClone(value));
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
import { snapshot } from './clone';
|
||||||
|
import { assert, test } from 'vitest';
|
||||||
|
import { proxy } from '../client/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());
|
||||||
|
|
||||||
|
// @ts-expect-error I can't figure out a way to exclude prototype properties
|
||||||
|
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