breaking: overhaul proxies, remove `$state.is` (#12916)

* chore: use closures for state proxies

* use variables

* early return

* tidy up

* move ownership stuff into separate object

* put original value directly on STATE_SYMBOL

* rename

* tidy up

* tidy

* tweak

* fix

* remove is_frozen check

* remove `$state.is`

* avoid mutations

* tweak

* changesets

* changeset

* changeset

* regenerate

* add comment

* add note

* add test
pull/12920/head
Rich Harris 1 month ago committed by GitHub
parent 5797f5e6fd
commit 0812b10100
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: disallow `Object.defineProperty` on state proxies with non-basic descriptors

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: allow frozen objects to be proxied

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: avoid mutations to underlying proxied object with $state

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: remove $state.is rune

@ -101,28 +101,6 @@ To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snaps
This is handy when you want to pass some state to an external library or API that doesn't expect a proxy, such as `structuredClone`.
## `$state.is`
Sometimes you might need to compare two values, one of which is a reactive `$state(...)` proxy but the other is not. For this you can use `$state.is(a, b)`:
```svelte
<script>
let foo = $state({});
let bar = {};
foo.bar = bar;
console.log(foo.bar === bar); // false — `foo.bar` is a reactive proxy
console.log($state.is(foo.bar, bar)); // true
</script>
```
This is handy when you might want to check if the object exists within a deeply reactive object/array.
Under the hood, `$state.is` uses [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) for comparing the values.
> Use this as an escape hatch - most of the time you don't need this. Svelte will warn you at dev time if you happen to run into this problem
## `$derived`
Derived state is declared with the `$derived` rune:

@ -64,6 +64,10 @@
> The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
## state_descriptors_fixed
> Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`.
## state_prototype_fixed
> Cannot set prototype of `$state` object

@ -44,7 +44,7 @@
## state_proxy_equality_mismatch
> Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results. Consider using `$state.is(a, b)` instead%details%
> Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results
`$state(...)` creates a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) of the value it is passed. The proxy and the value have different identities, meaning equality checks will always return `false`:
@ -57,15 +57,4 @@
</script>
```
In the rare case that you need to compare them, you can use `$state.is`, which unwraps proxies:
```svelte
<script>
let value = { foo: 'bar' };
let proxy = $state(value);
$state.is(value, proxy); // true
</script>
```
During development, Svelte will warn you when comparing values with proxies.
To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy.

@ -122,6 +122,10 @@
> Cannot use rune without parentheses
## rune_removed
> The `%name%` rune has been removed
## rune_renamed
> `%name%` is now `%replacement%`

@ -147,27 +147,6 @@ declare namespace $state {
*/
export function snapshot<T>(state: T): Snapshot<T>;
/**
* Compare two values, one or both of which is a reactive `$state(...)` proxy.
*
* Example:
* ```ts
* <script>
* let foo = $state({});
* let bar = {};
*
* foo.bar = bar;
*
* console.log(foo.bar === bar); // false — `foo.bar` is a reactive proxy
* console.log($state.is(foo.bar, bar)); // true
* </script>
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$state.is
*
*/
export function is(a: any, b: any): boolean;
// prevent intellisense from being unhelpful
/** @deprecated */
export const apply: never;

@ -348,6 +348,16 @@ export function rune_missing_parentheses(node) {
e(node, "rune_missing_parentheses", "Cannot use rune without parentheses");
}
/**
* The `%name%` rune has been removed
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function rune_removed(node, name) {
e(node, "rune_removed", `The \`${name}\` rune has been removed`);
}
/**
* `%name%` is now `%replacement%`
* @param {null | number | NodeLike} node

@ -140,13 +140,6 @@ export function CallExpression(node, context) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}
break;
case '$state.is':
if (node.arguments.length !== 2) {
e.rune_invalid_arguments_length(node, rune, 'exactly two arguments');
}
break;
}

@ -61,6 +61,10 @@ export function Identifier(node, context) {
e.rune_renamed(parent, '$state.frozen', '$state.raw');
}
if (name === '$state.is') {
e.rune_removed(parent, '$state.is');
}
e.rune_invalid_name(parent, name);
}
}

@ -24,13 +24,6 @@ export function CallExpression(node, context) {
is_ignored(node, 'state_snapshot_uncloneable') && b.true
);
case '$state.is':
return b.call(
'$.is',
/** @type {Expression} */ (context.visit(node.arguments[0])),
/** @type {Expression} */ (context.visit(node.arguments[1]))
);
case '$effect.root':
return b.call(
'$.effect_root',

@ -28,8 +28,7 @@ export function VariableDeclaration(node, context) {
rune === '$effect.tracking' ||
rune === '$effect.root' ||
rune === '$inspect' ||
rune === '$state.snapshot' ||
rune === '$state.is'
rune === '$state.snapshot'
) {
if (init != null && is_hoisted_function(init)) {
context.state.hoisted.push(

@ -33,14 +33,6 @@ export function CallExpression(node, context) {
);
}
if (rune === '$state.is') {
return b.call(
'Object.is',
/** @type {Expression} */ (context.visit(node.arguments[0])),
/** @type {Expression} */ (context.visit(node.arguments[1]))
);
}
if (rune === '$inspect' || rune === '$inspect().with') {
return transform_inspect_rune(node, context);
}

@ -20,4 +20,5 @@ export const INSPECT_EFFECT = 1 << 17;
export const HEAD_EFFECT = 1 << 18;
export const STATE_SYMBOL = Symbol('$state');
export const STATE_SYMBOL_METADATA = Symbol('$state metadata');
export const LOADING_ATTR_SYMBOL = Symbol('');

@ -20,10 +20,7 @@ export function init_array_prototype_warnings() {
const test = indexOf.call(get_proxied_value(this), get_proxied_value(item), from_index);
if (test !== -1) {
w.state_proxy_equality_mismatch(
'array.indexOf(...)',
': `array.findIndex(entry => $state.is(entry, item))`'
);
w.state_proxy_equality_mismatch('array.indexOf(...)');
}
}
@ -45,10 +42,7 @@ export function init_array_prototype_warnings() {
);
if (test !== -1) {
w.state_proxy_equality_mismatch(
'array.lastIndexOf(...)',
': `array.findLastIndex(entry => $state.is(entry, item))`'
);
w.state_proxy_equality_mismatch('array.lastIndexOf(...)');
}
}
@ -62,10 +56,7 @@ export function init_array_prototype_warnings() {
const test = includes.call(get_proxied_value(this), get_proxied_value(item), from_index);
if (test) {
w.state_proxy_equality_mismatch(
'array.includes(...)',
': `array.some(entry => $state.is(entry, item))`'
);
w.state_proxy_equality_mismatch('array.includes(...)');
}
}
@ -88,7 +79,7 @@ export function init_array_prototype_warnings() {
*/
export function strict_equals(a, b, equal = true) {
if ((a === b) !== (get_proxied_value(a) === get_proxied_value(b))) {
w.state_proxy_equality_mismatch(equal ? '===' : '!==', '');
w.state_proxy_equality_mismatch(equal ? '===' : '!==');
}
return (a === b) === equal;
@ -102,7 +93,7 @@ export function strict_equals(a, b, equal = true) {
*/
export function equals(a, b, equal = true) {
if ((a == b) !== (get_proxied_value(a) == get_proxied_value(b))) {
w.state_proxy_equality_mismatch(equal ? '==' : '!=', '');
w.state_proxy_equality_mismatch(equal ? '==' : '!=');
}
return (a == b) === equal;

@ -1,7 +1,7 @@
/** @import { ProxyMetadata } from '#client' */
/** @typedef {{ file: string, line: number, column: number }} Location */
import { STATE_SYMBOL } from '../constants.js';
import { STATE_SYMBOL_METADATA } from '../constants.js';
import { render_effect, user_pre_effect } from '../reactivity/effects.js';
import { dev_current_component_function } from '../runtime.js';
import { get_prototype_of } from '../../shared/utils.js';
@ -113,7 +113,7 @@ export function mark_module_end(component) {
export function add_owner(object, owner, global = false, skip_warning = false) {
if (object && !global) {
const component = dev_current_component_function;
const metadata = object[STATE_SYMBOL];
const metadata = object[STATE_SYMBOL_METADATA];
if (metadata && !has_owner(metadata, component)) {
let original = get_owner(metadata);
@ -138,8 +138,8 @@ export function add_owner_effect(get_object, Component, skip_warning = false) {
}
/**
* @param {ProxyMetadata<any> | null} from
* @param {ProxyMetadata<any>} to
* @param {ProxyMetadata | null} from
* @param {ProxyMetadata} to
*/
export function widen_ownership(from, to) {
if (to.owners === null) {
@ -166,7 +166,7 @@ export function widen_ownership(from, to) {
* @param {Set<any>} seen
*/
function add_owner_to_object(object, owner, seen) {
const metadata = /** @type {ProxyMetadata} */ (object?.[STATE_SYMBOL]);
const metadata = /** @type {ProxyMetadata} */ (object?.[STATE_SYMBOL_METADATA]);
if (metadata) {
// this is a state proxy, add owner directly, if not globally shared

@ -9,9 +9,9 @@ import { queue_micro_task } from '../../task.js';
* @returns {boolean}
*/
function is_bound_this(bound_value, element_or_component) {
// Find the original target if the value is proxied.
var proxy_target = bound_value && bound_value[STATE_SYMBOL]?.t;
return bound_value === element_or_component || proxy_target === element_or_component;
return (
bound_value === element_or_component || bound_value?.[STATE_SYMBOL] === element_or_component
);
}
/**

@ -278,6 +278,22 @@ export function rune_outside_svelte(rune) {
}
}
/**
* Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`.
* @returns {never}
*/
export function state_descriptors_fixed() {
if (DEV) {
const error = new Error(`state_descriptors_fixed\nProperty descriptors defined on \`$state\` objects must contain \`value\` and always be \`enumerable\`, \`configurable\` and \`writable\`.`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("state_descriptors_fixed");
}
}
/**
* Cannot set prototype of `$state` object
* @returns {never}

@ -150,7 +150,7 @@ export {
validate_prop_bindings
} from './validate.js';
export { raf } from './timing.js';
export { proxy, is } from './proxy.js';
export { proxy } from './proxy.js';
export { create_custom_element } from './dom/elements/custom-element.js';
export {
child,

@ -3,16 +3,14 @@ import { DEV } from 'esm-env';
import { get, current_component_context, untrack, current_effect } from './runtime.js';
import {
array_prototype,
define_property,
get_descriptor,
get_prototype_of,
is_array,
is_frozen,
object_prototype
} from '../shared/utils.js';
import { check_ownership, widen_ownership } from './dev/ownership.js';
import { source, set } from './reactivity/sources.js';
import { STATE_SYMBOL } from './constants.js';
import { STATE_SYMBOL, STATE_SYMBOL_METADATA } from './constants.js';
import { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.js';
@ -24,261 +22,262 @@ import * as e from './errors.js';
* @returns {ProxyStateObject<T> | T}
*/
export function proxy(value, parent = null, prev) {
if (typeof value === 'object' && value != null && !is_frozen(value)) {
// If we have an existing proxy, return it...
if (STATE_SYMBOL in value) {
const metadata = /** @type {ProxyMetadata<T>} */ (value[STATE_SYMBOL]);
// ...unless the proxy belonged to a different object, because
// someone copied the state symbol using `Reflect.ownKeys(...)`
if (metadata.t === value || metadata.p === value) {
if (DEV) {
// Since original parent relationship gets lost, we need to copy over ancestor owners
// into current metadata. The object might still exist on both, so we need to widen it.
widen_ownership(metadata, metadata);
metadata.parent = parent;
}
// if non-proxyable, or is already a proxy, return `value`
if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) {
return value;
}
return metadata.p;
}
const prototype = get_prototype_of(value);
if (prototype !== object_prototype && prototype !== array_prototype) {
return value;
}
var sources = new Map();
var is_proxied_array = is_array(value);
var version = source(0);
/** @type {ProxyMetadata} */
var metadata;
if (DEV) {
metadata = {
parent,
owners: null
};
if (prev) {
// Reuse owners from previous state; necessary because reassignment is not guaranteed to have correct component context.
// If no previous proxy exists we play it safe and assume ownerless state
// @ts-expect-error
const prev_owners = prev.v?.[STATE_SYMBOL_METADATA]?.owners;
metadata.owners = prev_owners ? new Set(prev_owners) : null;
} else {
metadata.owners =
parent === null
? current_component_context !== null
? new Set([current_component_context.function])
: null
: new Set();
}
}
const prototype = get_prototype_of(value);
if (prototype === object_prototype || prototype === array_prototype) {
const proxy = new Proxy(value, state_proxy_handler);
define_property(value, STATE_SYMBOL, {
value: /** @type {ProxyMetadata} */ ({
s: new Map(),
v: source(0),
a: is_array(value),
p: proxy,
t: value
}),
writable: true,
enumerable: false
});
return new Proxy(/** @type {any} */ (value), {
defineProperty(_, prop, descriptor) {
if (
!('value' in descriptor) ||
descriptor.configurable === false ||
descriptor.enumerable === false ||
descriptor.writable === false
) {
// we disallow non-basic descriptors, because unless they are applied to the
// target object — which we avoid, so that state can be forked — we will run
// afoul of the various invariants
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/getOwnPropertyDescriptor#invariants
e.state_descriptors_fixed();
}
if (DEV) {
// @ts-expect-error
value[STATE_SYMBOL].parent = parent;
if (prev) {
// Reuse owners from previous state; necessary because reassignment is not guaranteed to have correct component context.
// If no previous proxy exists we play it safe and assume ownerless state
// @ts-expect-error
const prev_owners = prev?.v?.[STATE_SYMBOL]?.owners;
// @ts-expect-error
value[STATE_SYMBOL].owners = prev_owners ? new Set(prev_owners) : null;
} else {
// @ts-expect-error
value[STATE_SYMBOL].owners =
parent === null
? current_component_context !== null
? new Set([current_component_context.function])
: null
: new Set();
}
var s = sources.get(prop);
if (s === undefined) {
s = source(descriptor.value);
sources.set(prop, s);
} else {
set(s, proxy(descriptor.value, metadata));
}
return proxy;
}
}
return true;
},
return value;
}
deleteProperty(target, prop) {
var s = sources.get(prop);
var exists = s !== undefined ? s.v !== UNINITIALIZED : prop in target;
/**
* @param {Source<number>} signal
* @param {1 | -1} [d]
*/
function update_version(signal, d = 1) {
set(signal, signal.v + d);
}
if (s !== undefined) {
set(s, UNINITIALIZED);
}
/** @type {ProxyHandler<ProxyStateObject<any>>} */
const state_proxy_handler = {
defineProperty(target, prop, descriptor) {
if (descriptor.value) {
/** @type {ProxyMetadata} */
const metadata = target[STATE_SYMBOL];
if (exists) {
update_version(version);
}
const s = metadata.s.get(prop);
if (s !== undefined) set(s, proxy(descriptor.value, metadata));
}
return exists;
},
return Reflect.defineProperty(target, prop, descriptor);
},
deleteProperty(target, prop) {
/** @type {ProxyMetadata} */
const metadata = target[STATE_SYMBOL];
const s = metadata.s.get(prop);
const is_array = metadata.a;
const boolean = delete target[prop];
// If we have mutated an array directly, and the deletion
// was successful we will also need to update the length
// before updating the field or the version. This is to
// ensure any effects observing length can execute before
// effects that listen to the fields otherwise they will
// operate an an index that no longer exists.
if (is_array && boolean) {
const ls = metadata.s.get('length');
const length = target.length - 1;
if (ls !== undefined && ls.v !== length) {
set(ls, length);
get(target, prop, receiver) {
if (DEV && prop === STATE_SYMBOL_METADATA) {
return metadata;
}
}
if (s !== undefined) set(s, UNINITIALIZED);
if (boolean) {
update_version(metadata.v);
}
if (prop === STATE_SYMBOL) {
return value;
}
return boolean;
},
var s = sources.get(prop);
var exists = prop in target;
get(target, prop, receiver) {
if (prop === STATE_SYMBOL) {
return Reflect.get(target, STATE_SYMBOL);
}
// create a source, but only if it's an own property and not a prototype property
if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) {
s = source(proxy(exists ? target[prop] : UNINITIALIZED, metadata));
sources.set(prop, s);
}
/** @type {ProxyMetadata} */
const metadata = target[STATE_SYMBOL];
let s = metadata.s.get(prop);
if (s !== undefined) {
var v = get(s);
return v === UNINITIALIZED ? undefined : v;
}
// create a source, but only if it's an own property and not a prototype property
if (s === undefined && (!(prop in target) || get_descriptor(target, prop)?.writable)) {
s = source(proxy(target[prop], metadata));
metadata.s.set(prop, s);
}
return Reflect.get(target, prop, receiver);
},
getOwnPropertyDescriptor(target, prop) {
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
if (descriptor && 'value' in descriptor) {
var s = sources.get(prop);
if (s) descriptor.value = get(s);
} else if (descriptor === undefined) {
var source = sources.get(prop);
var value = source?.v;
if (source !== undefined && value !== UNINITIALIZED) {
return {
enumerable: true,
configurable: true,
value,
writable: true
};
}
}
if (s !== undefined) {
const value = get(s);
return value === UNINITIALIZED ? undefined : value;
}
return descriptor;
},
return Reflect.get(target, prop, receiver);
},
has(target, prop) {
if (DEV && prop === STATE_SYMBOL_METADATA) {
return true;
}
if (prop === STATE_SYMBOL) {
return true;
}
getOwnPropertyDescriptor(target, prop) {
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
if (descriptor && 'value' in descriptor) {
/** @type {ProxyMetadata} */
const metadata = target[STATE_SYMBOL];
const s = metadata.s.get(prop);
var s = sources.get(prop);
var has = (s !== undefined && s.v !== UNINITIALIZED) || Reflect.has(target, prop);
if (s) {
descriptor.value = get(s);
if (
s !== undefined ||
(current_effect !== null && (!has || get_descriptor(target, prop)?.writable))
) {
if (s === undefined) {
s = source(has ? proxy(target[prop], metadata) : UNINITIALIZED);
sources.set(prop, s);
}
var value = get(s);
if (value === UNINITIALIZED) {
return false;
}
}
}
return descriptor;
},
return has;
},
has(target, prop) {
if (prop === STATE_SYMBOL) {
return true;
}
/** @type {ProxyMetadata} */
const metadata = target[STATE_SYMBOL];
const has = Reflect.has(target, prop);
let s = metadata.s.get(prop);
if (
s !== undefined ||
(current_effect !== null && (!has || get_descriptor(target, prop)?.writable))
) {
set(target, prop, value, receiver) {
var s = sources.get(prop);
var has = prop in target;
// If we haven't yet created a source for this property, we need to ensure
// we do so otherwise if we read it later, then the write won't be tracked and
// the heuristics of effects will be different vs if we had read the proxied
// object property before writing to that property.
if (s === undefined) {
s = source(has ? proxy(target[prop], metadata) : UNINITIALIZED);
metadata.s.set(prop, s);
}
const value = get(s);
if (value === UNINITIALIZED) {
return false;
if (!has || get_descriptor(target, prop)?.writable) {
s = source(undefined);
set(s, proxy(value, metadata));
sources.set(prop, s);
}
} else {
has = s.v !== UNINITIALIZED;
set(s, proxy(value, metadata));
}
}
return has;
},
set(target, prop, value, receiver) {
/** @type {ProxyMetadata} */
const metadata = target[STATE_SYMBOL];
let s = metadata.s.get(prop);
// If we haven't yet created a source for this property, we need to ensure
// we do so otherwise if we read it later, then the write won't be tracked and
// the heuristics of effects will be different vs if we had read the proxied
// object property before writing to that property.
if (s === undefined) {
// the read creates a signal
untrack(() => receiver[prop]);
s = metadata.s.get(prop);
}
if (s !== undefined) {
set(s, proxy(value, metadata));
}
const is_array = metadata.a;
const not_has = !(prop in target);
if (DEV) {
/** @type {ProxyMetadata | undefined} */
const prop_metadata = value?.[STATE_SYMBOL];
if (prop_metadata && prop_metadata?.parent !== metadata) {
widen_ownership(metadata, prop_metadata);
if (DEV) {
/** @type {ProxyMetadata | undefined} */
var prop_metadata = value?.[STATE_SYMBOL_METADATA];
if (prop_metadata && prop_metadata?.parent !== metadata) {
widen_ownership(metadata, prop_metadata);
}
check_ownership(metadata);
}
check_ownership(metadata);
}
// variable.length = value -> clear all signals with index >= value
if (is_array && prop === 'length') {
for (let i = value; i < target.length; i += 1) {
const s = metadata.s.get(i + '');
if (s !== undefined) set(s, UNINITIALIZED);
// variable.length = value -> clear all signals with index >= value
if (is_proxied_array && prop === 'length') {
for (var i = value; i < target.length; i += 1) {
var other_s = sources.get(i + '');
if (other_s !== undefined) set(other_s, UNINITIALIZED);
}
}
}
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
// Set the new value before updating any signals so that any listeners get the new value
if (descriptor?.set) {
descriptor.set.call(receiver, value);
} else {
target[prop] = value;
}
// Set the new value before updating any signals so that any listeners get the new value
if (descriptor?.set) {
descriptor.set.call(receiver, value);
}
if (not_has) {
// If we have mutated an array directly, we might need to
// signal that length has also changed. Do it before updating metadata
// to ensure that iterating over the array as a result of a metadata update
// will not cause the length to be out of sync.
if (is_array) {
const ls = metadata.s.get('length');
const length = target.length;
if (ls !== undefined && ls.v !== length) {
set(ls, length);
if (!has) {
// If we have mutated an array directly, we might need to
// signal that length has also changed. Do it before updating metadata
// to ensure that iterating over the array as a result of a metadata update
// will not cause the length to be out of sync.
if (is_proxied_array && typeof prop === 'string') {
var ls = sources.get('length');
if (ls !== undefined) {
var n = Number(prop);
if (Number.isInteger(n) && n >= ls.v) {
set(ls, n + 1);
}
}
}
update_version(version);
}
update_version(metadata.v);
}
return true;
},
return true;
},
ownKeys(target) {
/** @type {ProxyMetadata} */
const metadata = target[STATE_SYMBOL];
ownKeys(target) {
get(version);
get(metadata.v);
return Reflect.ownKeys(target);
}
};
var own_keys = Reflect.ownKeys(target).filter((key) => {
var source = sources.get(key);
return source === undefined || source.v !== UNINITIALIZED;
});
for (var [key, source] of sources) {
if (source.v !== UNINITIALIZED && !(key in target)) {
own_keys.push(key);
}
}
return own_keys;
},
setPrototypeOf() {
e.state_prototype_fixed();
}
});
}
if (DEV) {
state_proxy_handler.setPrototypeOf = () => {
e.state_prototype_fixed();
};
/**
* @param {Source<number>} signal
* @param {1 | -1} [d]
*/
function update_version(signal, d = 1) {
set(signal, signal.v + d);
}
/**
@ -286,11 +285,9 @@ if (DEV) {
*/
export function get_proxied_value(value) {
if (value !== null && typeof value === 'object' && STATE_SYMBOL in value) {
var metadata = value[STATE_SYMBOL];
if (metadata) {
return metadata.p;
}
return value[STATE_SYMBOL];
}
return value;
}

@ -0,0 +1,87 @@
import { proxy } from './proxy';
import { assert, test } from 'vitest';
test('does not mutate the original object', () => {
const original = { x: 1 };
const state = proxy(original);
state.x = 2;
assert.equal(original.x, 1);
assert.equal(state.x, 2);
});
test('preserves getters', () => {
let count = 0;
const original = {
count: 0,
get x() {
this.count += 1;
count += 1;
return 42;
}
};
const state = proxy(original);
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
state.x;
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
state.x;
assert.equal(original.count, 0);
assert.equal(count, 2);
assert.equal(state.count, 2);
});
test('defines a property', () => {
const original = { y: 0 };
const state = proxy<any>(original);
let value = 0;
Object.defineProperty(state, 'x', {
value: 1
});
Object.defineProperty(state, 'y', {
value: 1
});
assert.equal(state.x, 1);
assert.deepEqual(Object.getOwnPropertyDescriptor(state, 'x'), {
configurable: true,
writable: true,
value: 1,
enumerable: true
});
assert.ok(!('x' in original));
assert.deepEqual(Object.getOwnPropertyDescriptor(original, 'y'), {
configurable: true,
writable: true,
value: 0,
enumerable: true
});
assert.throws(
() =>
Object.defineProperty(state, 'x', {
get: () => value,
set: (v) => (value = v)
}),
/state_descriptors_fixed/
);
});
test('does not re-proxy proxies', () => {
const inner = proxy({ count: 0 });
const outer = proxy({ inner });
assert.equal(inner.count, 0);
assert.equal(outer.inner.count, 0);
inner.count += 1;
assert.equal(inner.count, 1);
assert.equal(outer.inner.count, 1);
});

@ -173,25 +173,16 @@ export type TaskCallback = (now: number) => boolean | void;
export type TaskEntry = { c: TaskCallback; f: () => void };
export interface ProxyMetadata<T = Record<string | symbol, any>> {
/** A map of signals associated to the properties that are reactive */
s: Map<string | symbol, Source<any>>;
/** A version counter, used within the proxy to signal changes in places where there's no other way to signal an update */
v: Source<number>;
/** `true` if the proxified object is an array */
a: boolean;
/** The associated proxy */
p: ProxyStateObject<T>;
/** The original target this proxy was created for */
t: T;
/** Dev-only — the components that 'own' this state, if any. `null` means no owners, i.e. everyone can mutate this state. */
/** Dev-only */
export interface ProxyMetadata {
/** The components that 'own' this state, if any. `null` means no owners, i.e. everyone can mutate this state. */
owners: null | Set<Function>;
/** Dev-only — the parent metadata object */
/** The parent metadata object */
parent: null | ProxyMetadata;
}
export type ProxyStateObject<T = Record<string | symbol, any>> = T & {
[STATE_SYMBOL]: ProxyMetadata;
[STATE_SYMBOL]: T;
};
export * from './reactivity/types';

@ -128,13 +128,12 @@ export function ownership_invalid_mutation(component, owner) {
}
/**
* Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results. Consider using `$state.is(a, b)` instead%details%
* Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results
* @param {string} operator
* @param {string} details
*/
export function state_proxy_equality_mismatch(operator, details) {
export function state_proxy_equality_mismatch(operator) {
if (DEV) {
console.warn(`%c[svelte] state_proxy_equality_mismatch\n%cReactive \`$state(...)\` proxies and the values they proxy have different identities. Because of this, comparisons with \`${operator}\` will produce unexpected results. Consider using \`$state.is(a, b)\` instead${details}`, bold, normal);
console.warn(`%c[svelte] state_proxy_equality_mismatch\n%cReactive \`$state(...)\` proxies and the values they proxy have different identities. Because of this, comparisons with \`${operator}\` will produce unexpected results`, bold, normal);
} else {
// TODO print a link to the documentation
console.warn("state_proxy_equality_mismatch");

@ -3,7 +3,6 @@
export var is_array = Array.isArray;
export var array_from = Array.from;
export var object_keys = Object.keys;
export var is_frozen = Object.isFrozen;
export var define_property = Object.defineProperty;
export var get_descriptor = Object.getOwnPropertyDescriptor;
export var get_descriptors = Object.getOwnPropertyDescriptors;

@ -395,7 +395,6 @@ const RUNES = /** @type {const} */ ([
'$state',
'$state.raw',
'$state.snapshot',
'$state.is',
'$props',
'$bindable',
'$derived',

@ -18,7 +18,7 @@ export default test({
target.innerHTML,
`
<button>1</button>
<button>1</button>
<button>0</button>
`
);
@ -29,8 +29,8 @@ export default test({
assert.htmlEqual(
target.innerHTML,
`
<button>2</button>
<button>2</button>
<button>1</button>
<button>1</button>
`
);
}

@ -1,7 +0,0 @@
import { test } from '../../test';
export default test({
async test({ assert, logs }) {
assert.deepEqual(logs, [false, true]);
}
});

@ -1,10 +0,0 @@
<script>
/** @type {{ bar?: any }}*/
let foo = $state({});
let bar = {};
foo.bar = bar;
console.log(foo.bar === bar); // false because of the $state proxy
console.log($state.is(foo.bar, bar)); // true
</script>

@ -1,4 +0,0 @@
<p>true</p>
<p>true</p>
<p>true</p>
<p>true</p>

@ -1,10 +0,0 @@
<script>
const obj = {};
const a = $state(obj)
const b= $state(obj)
</script>
<p>{a === obj}</p>
<p>{$state.is(a, obj)}</p>
<p>{a === b}</p>
<p>{$state.is(a, b)}</p>

@ -2935,27 +2935,6 @@ declare namespace $state {
*/
export function snapshot<T>(state: T): Snapshot<T>;
/**
* Compare two values, one or both of which is a reactive `$state(...)` proxy.
*
* Example:
* ```ts
* <script>
* let foo = $state({});
* let bar = {};
*
* foo.bar = bar;
*
* console.log(foo.bar === bar); // false — `foo.bar` is a reactive proxy
* console.log($state.is(foo.bar, bar)); // true
* </script>
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$state.is
*
*/
export function is(a: any, b: any): boolean;
// prevent intellisense from being unhelpful
/** @deprecated */
export const apply: never;

Loading…
Cancel
Save