Skip to content

Commit 8d3c026

Browse files
authored
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 * regenerate
1 parent e8453e7 commit 8d3c026

File tree

34 files changed

+433
-187
lines changed

34 files changed

+433
-187
lines changed

.changeset/perfect-actors-bake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
breaking: use structuredClone inside `$state.snapshot`

documentation/docs/03-runes/01-state.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ State declared with `$state.frozen` cannot be mutated; it can only be _reassigne
9191

9292
This can improve performance with large arrays and objects that you weren't planning to mutate anyway, since it avoids the cost of making them reactive. Note that frozen state can _contain_ reactive state (for example, a frozen array of reactive objects).
9393

94-
> Objects and arrays passed to `$state.frozen` will be shallowly frozen using `Object.freeze()`. If you don't want this, pass in a clone of the object or array instead.
94+
> Objects and arrays passed to `$state.frozen` will be shallowly frozen using `Object.freeze()`. If you don't want this, pass in a clone of the object or array instead. The argument cannot be an existing state proxy created with `$state(...)`.
9595
9696
## `$state.snapshot`
9797

packages/svelte/messages/client-errors/errors.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@
6060

6161
> The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
6262
63+
## state_frozen_invalid_argument
64+
65+
> The argument to `$state.frozen(...)` cannot be an object created with `$state(...)`. You should create a copy of it first, for example with `$state.snapshot`
66+
6367
## state_prototype_fixed
6468

6569
> Cannot set prototype of `$state` object

packages/svelte/src/ambient.d.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,75 @@ declare function $state<T>(initial: T): T;
3232
declare function $state<T>(): T | undefined;
3333

3434
declare namespace $state {
35+
type Primitive = string | number | boolean | null | undefined;
36+
37+
type TypedArray =
38+
| Int8Array
39+
| Uint8Array
40+
| Uint8ClampedArray
41+
| Int16Array
42+
| Uint16Array
43+
| Int32Array
44+
| Uint32Array
45+
| Float32Array
46+
| Float64Array
47+
| BigInt64Array
48+
| BigUint64Array;
49+
50+
/** The things that `structuredClone` can handle — https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm */
51+
export type Cloneable =
52+
| ArrayBuffer
53+
| DataView
54+
| Date
55+
| Error
56+
| Map<any, any>
57+
| RegExp
58+
| Set<any>
59+
| TypedArray
60+
// web APIs
61+
| Blob
62+
| CryptoKey
63+
| DOMException
64+
| DOMMatrix
65+
| DOMMatrixReadOnly
66+
| DOMPoint
67+
| DOMPointReadOnly
68+
| DOMQuad
69+
| DOMRect
70+
| DOMRectReadOnly
71+
| File
72+
| FileList
73+
| FileSystemDirectoryHandle
74+
| FileSystemFileHandle
75+
| FileSystemHandle
76+
| ImageBitmap
77+
| ImageData
78+
| RTCCertificate
79+
| VideoFrame;
80+
81+
/** Turn `SvelteDate`, `SvelteMap` and `SvelteSet` into their non-reactive counterparts. (`URL` is uncloneable.) */
82+
type NonReactive<T> = T extends Date
83+
? Date
84+
: T extends Map<infer K, infer V>
85+
? Map<K, V>
86+
: T extends Set<infer K>
87+
? Set<K>
88+
: T;
89+
90+
type Snapshot<T> = T extends Primitive
91+
? T
92+
: T extends Cloneable
93+
? NonReactive<T>
94+
: T extends { toJSON(): infer R }
95+
? R
96+
: T extends Array<infer U>
97+
? Array<Snapshot<U>>
98+
: T extends object
99+
? T extends { [key: string]: any }
100+
? { [K in keyof T]: Snapshot<T[K]> }
101+
: never
102+
: never;
103+
35104
/**
36105
* Declares reactive read-only state that is shallowly immutable.
37106
*
@@ -75,7 +144,7 @@ declare namespace $state {
75144
*
76145
* @param state The value to snapshot
77146
*/
78-
export function snapshot<T>(state: T): T;
147+
export function snapshot<T>(state: T): Snapshot<T>;
79148

80149
/**
81150
* Compare two values, one or both of which is a reactive `$state(...)` proxy.

packages/svelte/src/compiler/phases/3-transform/server/transform-server.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,10 @@ const global_visitors = {
423423
}
424424

425425
if (rune === '$state.snapshot') {
426-
return /** @type {import('estree').Expression} */ (context.visit(node.arguments[0]));
426+
return b.call(
427+
'$.snapshot',
428+
/** @type {import('estree').Expression} */ (context.visit(node.arguments[0]))
429+
);
427430
}
428431

429432
if (rune === '$state.is') {

packages/svelte/src/index-client.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { current_component_context, flush_sync, untrack } from './internal/client/runtime.js';
2-
import { is_array } from './internal/client/utils.js';
2+
import { is_array } from './internal/shared/utils.js';
33
import { user_effect } from './internal/client/index.js';
44
import * as e from './internal/client/errors.js';
55
import { lifecycle_outside_component } from './internal/shared/errors.js';
Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { snapshot } from '../proxy.js';
1+
import { snapshot } from '../../shared/clone.js';
22
import { inspect_effect, validate_effect } from '../reactivity/effects.js';
3-
import { array_prototype, get_prototype_of, object_prototype } from '../utils.js';
43

54
/**
65
* @param {() => any[]} get_value
@@ -13,47 +12,7 @@ export function inspect(get_value, inspector = console.log) {
1312
let initial = true;
1413

1514
inspect_effect(() => {
16-
inspector(initial ? 'init' : 'update', ...deep_snapshot(get_value()));
15+
inspector(initial ? 'init' : 'update', ...snapshot(get_value()));
1716
initial = false;
1817
});
1918
}
20-
21-
/**
22-
* Like `snapshot`, but recursively traverses into normal arrays/objects to find potential states in them.
23-
* @param {any} value
24-
* @param {Map<any, any>} visited
25-
* @returns {any}
26-
*/
27-
function deep_snapshot(value, visited = new Map()) {
28-
if (typeof value === 'object' && value !== null && !visited.has(value)) {
29-
const unstated = snapshot(value);
30-
31-
if (unstated !== value) {
32-
visited.set(value, unstated);
33-
return unstated;
34-
}
35-
36-
const prototype = get_prototype_of(value);
37-
38-
// Only deeply snapshot plain objects and arrays
39-
if (prototype === object_prototype || prototype === array_prototype) {
40-
let contains_unstated = false;
41-
/** @type {any} */
42-
const nested_unstated = Array.isArray(value) ? [] : {};
43-
44-
for (let key in value) {
45-
const result = deep_snapshot(value[key], visited);
46-
nested_unstated[key] = result;
47-
if (result !== value[key]) {
48-
contains_unstated = true;
49-
}
50-
}
51-
52-
visited.set(value, contains_unstated ? nested_unstated : value);
53-
} else {
54-
visited.set(value, value);
55-
}
56-
}
57-
58-
return visited.get(value) ?? value;
59-
}

packages/svelte/src/internal/client/dev/ownership.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { STATE_SYMBOL } from '../constants.js';
55
import { render_effect, user_pre_effect } from '../reactivity/effects.js';
66
import { dev_current_component_function } from '../runtime.js';
7-
import { get_prototype_of } from '../utils.js';
7+
import { get_prototype_of } from '../../shared/utils.js';
88
import * as w from '../warnings.js';
99

1010
/** @type {Record<string, Array<{ start: Location, end: Location, component: Function }>>} */

packages/svelte/src/internal/client/dom/blocks/each.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
resume_effect
2929
} from '../../reactivity/effects.js';
3030
import { source, mutable_source, set } from '../../reactivity/sources.js';
31-
import { is_array, is_frozen } from '../../utils.js';
31+
import { is_array, is_frozen } from '../../../shared/utils.js';
3232
import { INERT, STATE_FROZEN_SYMBOL, STATE_SYMBOL } from '../../constants.js';
3333
import { queue_micro_task } from '../task.js';
3434
import { current_effect } from '../../runtime.js';

packages/svelte/src/internal/client/dom/css.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function append_styles(anchor, css) {
2020

2121
var target = /** @type {ShadowRoot} */ (root).host
2222
? /** @type {ShadowRoot} */ (root)
23-
: /** @type {Document} */ (root).head;
23+
: /** @type {Document} */ (root).head ?? /** @type {Document} */ (root.ownerDocument).head;
2424

2525
if (!target.querySelector('#' + css.hash)) {
2626
const style = document.createElement('style');

0 commit comments

Comments
 (0)