From acc8361cd26d7d24c401e03541632ffc629adf51 Mon Sep 17 00:00:00 2001
From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com>
Date: Wed, 2 Apr 2025 22:43:10 -0700
Subject: [PATCH 01/12] init
---
.changeset/breezy-baboons-exercise.md | 5 +++
.../98-reference/.generated/compile-errors.md | 6 +++
.../svelte/messages/compile-errors/script.md | 4 ++
packages/svelte/src/ambient.d.ts | 24 +++++++++++
packages/svelte/src/compiler/errors.js | 9 ++++
.../2-analyze/visitors/CallExpression.js | 18 ++++++++
.../client/visitors/CallExpression.js | 2 +
packages/svelte/src/internal/client/index.js | 10 ++++-
.../src/internal/client/reactivity/sources.js | 43 +++++++++++++++++++
packages/svelte/src/utils.js | 1 +
packages/svelte/types/index.d.ts | 24 +++++++++++
11 files changed, 145 insertions(+), 1 deletion(-)
create mode 100644 .changeset/breezy-baboons-exercise.md
diff --git a/.changeset/breezy-baboons-exercise.md b/.changeset/breezy-baboons-exercise.md
new file mode 100644
index 000000000000..e1da1d3f46a3
--- /dev/null
+++ b/.changeset/breezy-baboons-exercise.md
@@ -0,0 +1,5 @@
+---
+'svelte': minor
+---
+
+feat: add `$state.invalidate` rune
diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md
index a8c39aaf9713..d2dc537006fe 100644
--- a/documentation/docs/98-reference/.generated/compile-errors.md
+++ b/documentation/docs/98-reference/.generated/compile-errors.md
@@ -828,6 +828,12 @@ Cannot export state from a module if it is reassigned. Either export a function
`%rune%(...)` can only be used as a variable declaration initializer or a class field
```
+### state_invalidate_nonreactive_argument
+
+```
+`$state.invalidate` only takes a variable declared with `$state` or `$state.raw` as its argument
+```
+
### store_invalid_scoped_subscription
```
diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md
index aabcbeae4812..a194b7e3dc97 100644
--- a/packages/svelte/messages/compile-errors/script.md
+++ b/packages/svelte/messages/compile-errors/script.md
@@ -220,6 +220,10 @@ It's possible to export a snippet from a `
+ *
+ * ```
+ */
+ export function invalidate(source: unknown): void;
+
/**
* Declares state that is _not_ made deeply reactive — instead of mutating it,
* you must reassign it.
diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js
index 6bf973948b92..2930ce46bb8b 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -480,6 +480,15 @@ export function state_invalid_placement(node, rune) {
e(node, 'state_invalid_placement', `\`${rune}(...)\` can only be used as a variable declaration initializer or a class field\nhttps://svelte.dev/e/state_invalid_placement`);
}
+/**
+ * `$state.invalidate` only takes a variable declared with `$state` or `$state.raw` as its argument
+ * @param {null | number | NodeLike} node
+ * @returns {never}
+ */
+export function state_invalidate_nonreactive_argument(node) {
+ e(node, 'state_invalidate_nonreactive_argument', `\`$state.invalidate\` only takes a variable declared with \`$state\` or \`$state.raw\` as its argument\nhttps://svelte.dev/e/state_invalidate_nonreactive_argument`);
+}
+
/**
* Cannot subscribe to stores that are not declared at the top level of the component
* @param {null | number | NodeLike} node
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
index 2eac934b332c..d299abf62654 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
@@ -111,6 +111,24 @@ export function CallExpression(node, context) {
break;
}
+ case '$state.invalidate':
+ if (node.arguments.length !== 1) {
+ e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
+ } else {
+ let arg = node.arguments[0];
+ if (arg.type !== 'Identifier') {
+ e.rune_invalid_arguments(node, rune);
+ }
+ let binding = context.state.scope.get(arg.name);
+ if (binding) {
+ if (binding.kind === 'raw_state' || binding.kind === 'state') {
+ binding.reassigned = true;
+ break;
+ }
+ }
+ e.state_invalidate_nonreactive_argument(node);
+ }
+
case '$state':
case '$state.raw':
case '$derived':
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
index fda43ad7911a..4f5f52e96921 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
@@ -23,6 +23,8 @@ export function CallExpression(node, context) {
/** @type {Expression} */ (context.visit(node.arguments[0])),
is_ignored(node, 'state_snapshot_uncloneable') && b.true
);
+ case '$state.invalidate':
+ return b.call('$.invalidate', node.arguments[0]);
case '$effect.root':
return b.call(
diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js
index a5f93e8b171b..5e72a46554fb 100644
--- a/packages/svelte/src/internal/client/index.js
+++ b/packages/svelte/src/internal/client/index.js
@@ -113,7 +113,15 @@ export {
user_effect,
user_pre_effect
} from './reactivity/effects.js';
-export { mutable_source, mutate, set, state, update, update_pre } from './reactivity/sources.js';
+export {
+ invalidate,
+ mutable_source,
+ mutate,
+ set,
+ state,
+ update,
+ update_pre
+} from './reactivity/sources.js';
export {
prop,
rest_props,
diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index e4834902fe3f..36189cb749fe 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -211,6 +211,49 @@ export function internal_set(source, value) {
return value;
}
+/**
+ * @param {Source} source
+ */
+export function invalidate(source) {
+ source.wv = increment_write_version();
+
+ mark_reactions(source, DIRTY);
+
+ // It's possible that the current reaction might not have up-to-date dependencies
+ // whilst it's actively running. So in the case of ensuring it registers the reaction
+ // properly for itself, we need to ensure the current effect actually gets
+ // scheduled. i.e: `$effect(() => x++)`
+ if (
+ is_runes() &&
+ active_effect !== null &&
+ (active_effect.f & CLEAN) !== 0 &&
+ (active_effect.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0
+ ) {
+ if (untracked_writes === null) {
+ set_untracked_writes([source]);
+ } else {
+ untracked_writes.push(source);
+ }
+ }
+
+ if (DEV && inspect_effects.size > 0) {
+ const inspects = Array.from(inspect_effects);
+
+ for (const effect of inspects) {
+ // Mark clean inspect-effects as maybe dirty and then check their dirtiness
+ // instead of just updating the effects - this way we avoid overfiring.
+ if ((effect.f & CLEAN) !== 0) {
+ set_signal_status(effect, MAYBE_DIRTY);
+ }
+ if (check_dirtiness(effect)) {
+ update_effect(effect);
+ }
+ }
+
+ inspect_effects.clear();
+ }
+}
+
/**
* @template {number | bigint} T
* @param {Source} source
diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js
index d4d106d56deb..9c64fe0fadb4 100644
--- a/packages/svelte/src/utils.js
+++ b/packages/svelte/src/utils.js
@@ -430,6 +430,7 @@ export function is_mathml(name) {
const RUNES = /** @type {const} */ ([
'$state',
+ '$state.invalidate',
'$state.raw',
'$state.snapshot',
'$props',
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index c6000fc4b67f..6509b1cf1e21 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -2760,6 +2760,30 @@ declare namespace $state {
: never
: never;
+ /**
+ * Forces an update on a `$state` or `$state.raw` variable.
+ * This is primarily meant as an escape hatch to be able to use external or native classes
+ * with Svelte's reactivity system.
+ * If you used Svelte 3 or 4, this is the equivalent of `foo = foo`.
+ * Example:
+ * ```svelte
+ *
+ *
+ * ```
+ */
+ export function invalidate(source: unknown): void;
+
/**
* Declares state that is _not_ made deeply reactive — instead of mutating it,
* you must reassign it.
From dfae0e9b8f46f8e3a2e6123331b26bef7ccf10f3 Mon Sep 17 00:00:00 2001
From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com>
Date: Wed, 2 Apr 2025 23:18:45 -0700
Subject: [PATCH 02/12] tweak error, maybe fix lint
---
.../src/compiler/phases/2-analyze/visitors/CallExpression.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
index d299abf62654..ccabd1e460fe 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
@@ -110,14 +110,14 @@ export function CallExpression(node, context) {
break;
}
-
+ /* eslint-disable no-fallthrough */
case '$state.invalidate':
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
} else {
let arg = node.arguments[0];
if (arg.type !== 'Identifier') {
- e.rune_invalid_arguments(node, rune);
+ e.state_invalidate_nonreactive_argument(node);
}
let binding = context.state.scope.get(arg.name);
if (binding) {
From b05dbbfdc61c6f00a32daebe83c91462c6775169 Mon Sep 17 00:00:00 2001
From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com>
Date: Wed, 2 Apr 2025 23:26:25 -0700
Subject: [PATCH 03/12] actually fix lint
---
packages/svelte/src/ambient.d.ts | 6 +++---
packages/svelte/types/index.d.ts | 6 +++---
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts
index 1953c2780f3b..e421b79a1729 100644
--- a/packages/svelte/src/ambient.d.ts
+++ b/packages/svelte/src/ambient.d.ts
@@ -102,11 +102,11 @@ declare namespace $state {
* ```svelte
*
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index 6509b1cf1e21..70f7a057f6c4 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -2769,11 +2769,11 @@ declare namespace $state {
* ```svelte
*
From 524d22954b5aa93bea8cd190648eb7c7a153861d Mon Sep 17 00:00:00 2001
From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com>
Date: Thu, 3 Apr 2025 19:20:10 -0700
Subject: [PATCH 04/12] add support for class fields
---
.../98-reference/.generated/compile-errors.md | 27 ++++++++++++-
.../svelte/messages/compile-errors/script.md | 25 +++++++++++-
packages/svelte/src/ambient.d.ts | 2 +-
packages/svelte/src/compiler/errors.js | 13 +++++-
.../2-analyze/visitors/CallExpression.js | 40 ++++++++++++++++---
.../client/visitors/CallExpression.js | 20 +++++++++-
packages/svelte/types/index.d.ts | 2 +-
7 files changed, 117 insertions(+), 12 deletions(-)
diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md
index d2dc537006fe..3deeef83782b 100644
--- a/documentation/docs/98-reference/.generated/compile-errors.md
+++ b/documentation/docs/98-reference/.generated/compile-errors.md
@@ -828,10 +828,35 @@ Cannot export state from a module if it is reassigned. Either export a function
`%rune%(...)` can only be used as a variable declaration initializer or a class field
```
+### state_invalidate_invalid_this_property
+
+```
+`$state.invalidate` can only be called with an argument referencing `this` in a class using a non-computed property
+```
+
+Like how you can't use `$state` or `$derived` when declaring computed class fields, you can't use `$state.invalidate` to invalidate a computed class field. For example, while `count` here is not itself a computed property, you can't invalidate it if you reference it in a computed property:
+```js
+class Box {
+ value;
+ constructor(initial) {
+ this.value = initial;
+ }
+}
+const property = 'count';
+class Counter {
+ count = $state(new Box(0));
+ increment() {
+ this.count.value += 1;
+ $state.invalidate(this[property]); // this doesn't work
+ $state.invalidate(this.count); // this works
+ }
+}
+```
+
### state_invalidate_nonreactive_argument
```
-`$state.invalidate` only takes a variable declared with `$state` or `$state.raw` as its argument
+`$state.invalidate` only takes a variable or non-computed class field declared with `$state` or `$state.raw` as its argument
```
### store_invalid_scoped_subscription
diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md
index a194b7e3dc97..18e51a8185e0 100644
--- a/packages/svelte/messages/compile-errors/script.md
+++ b/packages/svelte/messages/compile-errors/script.md
@@ -220,9 +220,32 @@ It's possible to export a snippet from a `
+
+```
+
+`$state.invalidate` can also be used with reactive class fields:
+
+```js
+class Box {
+ value;
+
+ constructor(initial) {
+ this.value = initial;
+ }
+}
+
+class Counter {
+ count = $state(new Box(0));
+
+ increment() {
+ this.count.value++;
+ $state.invalidate(this.count);
+ }
+}
+```
+
## Passing state into functions
JavaScript is a _pass-by-value_ language — when you call a function, the arguments are the _values_ rather than the _variables_. In other words:
diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index 36189cb749fe..3bb8a2e4cfaa 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -215,6 +215,15 @@ export function internal_set(source, value) {
* @param {Source} source
*/
export function invalidate(source) {
+ if (
+ active_reaction !== null &&
+ !untracking &&
+ is_runes() &&
+ (active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 &&
+ !reaction_sources?.includes(source)
+ ) {
+ e.state_unsafe_mutation();
+ }
source.wv = increment_write_version();
mark_reactions(source, DIRTY);
From d4394c55d3ffae1b2d4a59692adab37f2c264ebc Mon Sep 17 00:00:00 2001
From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com>
Date: Thu, 10 Apr 2025 18:32:02 -0700
Subject: [PATCH 08/12] try adding support for individual property
invalidation, might revert later
---
.../2-analyze/visitors/CallExpression.js | 16 ++++++--
.../client/visitors/CallExpression.js | 41 ++++++++++++-------
.../svelte/src/internal/client/constants.js | 1 +
packages/svelte/src/internal/client/index.js | 2 +-
packages/svelte/src/internal/client/proxy.js | 27 +++++++++++-
.../src/internal/client/reactivity/sources.js | 5 ++-
6 files changed, 70 insertions(+), 22 deletions(-)
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
index 1d7af8b4f5c4..80b6a5cdde12 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
@@ -1,9 +1,9 @@
-/** @import { ArrowFunctionExpression, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, VariableDeclarator } from 'estree' */
+/** @import { ArrowFunctionExpression, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, MemberExpression, VariableDeclarator } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { get_rune } from '../../scope.js';
import * as e from '../../../errors.js';
-import { get_parent, unwrap_optional } from '../../../utils/ast.js';
+import { get_parent, object, unwrap_optional } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js';
import { dev, locate_node, source } from '../../../state.js';
import * as b from '../../../utils/builders.js';
@@ -121,10 +121,18 @@ export function CallExpression(node, context) {
}
if (arg.type === 'MemberExpression') {
if (arg.object.type !== 'ThisExpression') {
- e.state_invalidate_nonreactive_argument(node);
+ const obj = object((arg = /** @type {MemberExpression} */ (context.visit(arg))));
+ if (obj?.type === 'Identifier') {
+ // there isn't really a good way to tell because of stuff like `notproxied = proxied`
+ break;
+ } else if (obj?.type !== 'ThisExpression') {
+ e.state_invalidate_nonreactive_argument(node);
+ }
+ } else if (arg.computed) {
+ e.state_invalidate_invalid_this_property(node);
}
const class_body = context.path.findLast((parent) => parent.type === 'ClassBody');
- if (arg.computed || !class_body) {
+ if (!class_body) {
e.state_invalidate_invalid_this_property(node);
}
const possible_this_bindings = context.path.filter((parent, index) => {
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
index 3983479fe7fd..e29ec869482e 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
@@ -1,10 +1,11 @@
-/** @import { CallExpression, Expression } from 'estree' */
+/** @import { CallExpression, Expression, Identifier } from 'estree' */
/** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
import * as e from '../../../../errors.js';
+import { object } from '../../../../utils/ast.js';
/**
* @param {CallExpression} node
@@ -29,20 +30,32 @@ export function CallExpression(node, context) {
if (node.arguments[0].type === 'Identifier') {
return b.call('$.invalidate', node.arguments[0]);
} else if (node.arguments[0].type === 'MemberExpression') {
- const { property } = node.arguments[0];
- let field;
- switch (property.type) {
- case 'Identifier':
- field = context.state.public_state.get(property.name);
- break;
- case 'PrivateIdentifier':
- field = context.state.private_state.get(property.name);
- break;
+ const { object: obj, property } = node.arguments[0];
+ const root = object(node.arguments[0]);
+ if (obj.type === 'ThisExpression') {
+ let field;
+ switch (property.type) {
+ case 'Identifier':
+ field = context.state.public_state.get(property.name);
+ break;
+ case 'PrivateIdentifier':
+ field = context.state.private_state.get(property.name);
+ break;
+ }
+ if (!field || (field.kind !== 'state' && field.kind !== 'raw_state')) {
+ e.state_invalidate_nonreactive_argument(node);
+ }
+ return b.call('$.invalidate', b.member(b.this, field.id));
}
- if (!field || (field.kind !== 'state' && field.kind !== 'raw_state')) {
- e.state_invalidate_nonreactive_argument(node);
- }
- return b.call('$.invalidate', b.member(b.this, field.id));
+ /** @type {Expression[]} */
+ const source_args = /** @type {Expression[]} */ ([
+ context.visit(obj),
+ node.arguments[0].computed
+ ? context.visit(property)
+ : b.literal(/** @type {Identifier} */ (property).name)
+ ]);
+ const arg = b.call('$.lookup_source', ...source_args);
+ return b.call('$.invalidate', arg);
}
case '$effect.root':
diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js
index 7e5196c606b4..6e3f62984e46 100644
--- a/packages/svelte/src/internal/client/constants.js
+++ b/packages/svelte/src/internal/client/constants.js
@@ -25,3 +25,4 @@ export const EFFECT_IS_UPDATING = 1 << 21;
export const STATE_SYMBOL = Symbol('$state');
export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol('');
+export const PROXY_SOURCES = Symbol('proxy sources');
diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js
index 88091ed84807..b409dcf5a427 100644
--- a/packages/svelte/src/internal/client/index.js
+++ b/packages/svelte/src/internal/client/index.js
@@ -148,7 +148,7 @@ export {
} from './runtime.js';
export { validate_binding, validate_each_keys } from './validate.js';
export { raf } from './timing.js';
-export { proxy } from './proxy.js';
+export { proxy, lookup_source } from './proxy.js';
export { create_custom_element } from './dom/elements/custom-element.js';
export {
child,
diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js
index 5e0aa3dbc35f..32cf1eb664a9 100644
--- a/packages/svelte/src/internal/client/proxy.js
+++ b/packages/svelte/src/internal/client/proxy.js
@@ -9,7 +9,7 @@ import {
object_prototype
} from '../shared/utils.js';
import { state as source, set } from './reactivity/sources.js';
-import { STATE_SYMBOL } from './constants.js';
+import { STATE_SYMBOL, PROXY_SOURCES } from './constants.js';
import { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.js';
import { get_stack } from './dev/tracing.js';
@@ -124,6 +124,10 @@ export function proxy(value) {
return value;
}
+ if (prop === PROXY_SOURCES) {
+ return sources;
+ }
+
var s = sources.get(prop);
var exists = prop in target;
@@ -165,7 +169,7 @@ export function proxy(value) {
},
has(target, prop) {
- if (prop === STATE_SYMBOL) {
+ if (prop === STATE_SYMBOL || prop === PROXY_SOURCES) {
return true;
}
@@ -317,3 +321,22 @@ export function get_proxied_value(value) {
export function is(a, b) {
return Object.is(get_proxied_value(a), get_proxied_value(b));
}
+
+/**
+ * @param {Record} object
+ * @param {string | symbol} property
+ * @returns {Source | null}
+ */
+export function lookup_source(object, property) {
+ if (typeof object !== 'object' || object === null) return null;
+ if (STATE_SYMBOL in object) {
+ if (property in object) {
+ /** @type {Map} */
+ const sources = object[PROXY_SOURCES];
+ if (sources.has(property)) {
+ return /** @type {Source} */ (sources.get(property));
+ }
+ }
+ }
+ return null;
+}
diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index 7724edad6822..044b80542998 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -221,9 +221,12 @@ export function internal_set(source, value) {
}
/**
- * @param {Source} source
+ * @param {Source | null} source
*/
export function invalidate(source) {
+ if (source === null) {
+ return;
+ }
if (
active_reaction !== null &&
!untracking &&
From 885f3d61536c22bb560ff3ce1695e4045524320d Mon Sep 17 00:00:00 2001
From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com>
Date: Tue, 15 Apr 2025 17:27:39 -0700
Subject: [PATCH 09/12] add error message if source doesn't exist, cleanup code
---
documentation/docs/02-runes/02-$state.md | 11 ++++-
.../98-reference/.generated/client-errors.md | 6 +++
.../svelte/messages/client-errors/errors.md | 4 ++
.../client/visitors/CallExpression.js | 43 +++++++++++++++----
packages/svelte/src/internal/client/errors.js | 15 +++++++
.../src/internal/client/reactivity/sources.js | 4 +-
6 files changed, 70 insertions(+), 13 deletions(-)
diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md
index 8415ac9947c9..8742e7836916 100644
--- a/documentation/docs/02-runes/02-$state.md
+++ b/documentation/docs/02-runes/02-$state.md
@@ -184,7 +184,7 @@ In the case that you aren't using a proxied `$state` via use of `$state.raw` or
```
-`$state.invalidate` can also be used with reactive class fields:
+`$state.invalidate` can also be used with reactive class fields, and properties of `$state` objects:
```js
class Box {
@@ -199,10 +199,17 @@ class Counter {
count = $state(new Box(0));
increment() {
- this.count.value++;
+ this.count.value += 1;
$state.invalidate(this.count);
}
}
+
+let counter = $state({count: new Box(0)});
+
+function increment() {
+ counter.count.value += 1;
+ $state.invalidate(counter.count);
+}
```
## Passing state into functions
diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md
index 32348bb78182..cf992c8cab3e 100644
--- a/documentation/docs/98-reference/.generated/client-errors.md
+++ b/documentation/docs/98-reference/.generated/client-errors.md
@@ -116,6 +116,12 @@ The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`.
```
+### state_invalidate_invalid_source
+
+```
+The argument passed to `$state.invalidate` must be a variable or class field declared with `$state` or `$state.raw`, or a property of a `$state` object.
+```
+
### state_prototype_fixed
```
diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md
index c4e68f8fee80..7511abf8d695 100644
--- a/packages/svelte/messages/client-errors/errors.md
+++ b/packages/svelte/messages/client-errors/errors.md
@@ -76,6 +76,10 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
> Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`.
+## state_invalidate_invalid_source
+
+> The argument passed to `$state.invalidate` must be a variable or class field declared with `$state` or `$state.raw`, or a property of a `$state` object.
+
## state_prototype_fixed
> Cannot set prototype of `$state` object
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
index e29ec869482e..d18f9ff7d99d 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
@@ -1,17 +1,31 @@
-/** @import { CallExpression, Expression, Identifier } from 'estree' */
+/** @import { CallExpression, Expression, Identifier, MemberExpression, Node } from 'estree' */
/** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
import * as e from '../../../../errors.js';
-import { object } from '../../../../utils/ast.js';
/**
* @param {CallExpression} node
* @param {Context} context
*/
export function CallExpression(node, context) {
+ /**
+ * Some nodes that get replaced should keep their locations (for better source maps and such)
+ * @template {Node} N
+ * @param {N} node
+ * @param {N} replacement
+ * @returns {N}
+ */
+ function attach_locations(node, replacement) {
+ return {
+ ...replacement,
+ start: node.start,
+ end: node.end,
+ loc: node.loc
+ };
+ }
switch (get_rune(node, context.state.scope)) {
case '$host':
return b.id('$$props.$$host');
@@ -28,11 +42,13 @@ export function CallExpression(node, context) {
/* eslint-disable no-fallthrough */
case '$state.invalidate':
if (node.arguments[0].type === 'Identifier') {
- return b.call('$.invalidate', node.arguments[0]);
+ return b.call(
+ attach_locations(/** @type {Expression} */ (node.callee), b.id('$.invalidate')),
+ node.arguments[0]
+ );
} else if (node.arguments[0].type === 'MemberExpression') {
- const { object: obj, property } = node.arguments[0];
- const root = object(node.arguments[0]);
- if (obj.type === 'ThisExpression') {
+ const { object, property } = node.arguments[0];
+ if (object.type === 'ThisExpression') {
let field;
switch (property.type) {
case 'Identifier':
@@ -45,17 +61,26 @@ export function CallExpression(node, context) {
if (!field || (field.kind !== 'state' && field.kind !== 'raw_state')) {
e.state_invalidate_nonreactive_argument(node);
}
- return b.call('$.invalidate', b.member(b.this, field.id));
+ return b.call(
+ attach_locations(/** @type {Expression} */ (node.callee), b.id('$.invalidate')),
+ attach_locations(node.arguments[0], b.member(object, field.id))
+ );
}
/** @type {Expression[]} */
const source_args = /** @type {Expression[]} */ ([
- context.visit(obj),
+ context.visit(object),
node.arguments[0].computed
? context.visit(property)
: b.literal(/** @type {Identifier} */ (property).name)
]);
const arg = b.call('$.lookup_source', ...source_args);
- return b.call('$.invalidate', arg);
+ return b.call(
+ attach_locations(/** @type {Expression} */ (node.callee), b.id('$.invalidate')),
+ attach_locations(
+ /** @type {Expression} */ (node.arguments[0]),
+ /** @type {Expression} */ (arg)
+ )
+ );
}
case '$effect.root':
diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js
index 429dd99da9b9..c869e82a502d 100644
--- a/packages/svelte/src/internal/client/errors.js
+++ b/packages/svelte/src/internal/client/errors.js
@@ -291,6 +291,21 @@ export function state_descriptors_fixed() {
}
}
+/**
+ * The argument passed to `$state.invalidate` must be a variable or class field declared with `$state` or `$state.raw`, or a property of a `$state` object.
+ * @returns {never}
+ */
+export function state_invalidate_invalid_source() {
+ if (DEV) {
+ const error = new Error(`state_invalidate_invalid_source\nThe argument passed to \`$state.invalidate\` must be a variable or class field declared with \`$state\` or \`$state.raw\`, or a property of a \`$state\` object.\nhttps://svelte.dev/e/state_invalidate_invalid_source`);
+
+ error.name = 'Svelte error';
+ throw error;
+ } else {
+ throw new Error(`https://svelte.dev/e/state_invalidate_invalid_source`);
+ }
+}
+
/**
* Cannot set prototype of `$state` object
* @returns {never}
diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index 8aa7b650196c..20fa1106baac 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -224,8 +224,8 @@ export function internal_set(source, value) {
* @param {Source | null} source
*/
export function invalidate(source) {
- if (source === null) {
- return;
+ if (source === null || (source.f & DERIVED) !== 0) {
+ e.state_invalidate_invalid_source();
}
if (
active_reaction !== null &&
From 0adad95dc0c08e77450cabeb29a526bdfc3738cd Mon Sep 17 00:00:00 2001
From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com>
Date: Thu, 17 Apr 2025 23:39:38 -0700
Subject: [PATCH 10/12] fix
---
packages/svelte/src/internal/client/proxy.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js
index 561b4a4f6ced..32cf1eb664a9 100644
--- a/packages/svelte/src/internal/client/proxy.js
+++ b/packages/svelte/src/internal/client/proxy.js
@@ -9,7 +9,7 @@ import {
object_prototype
} from '../shared/utils.js';
import { state as source, set } from './reactivity/sources.js';
-import { STATE_SYMBOL, PROXY_SOURCES } from '#client/constants.js';
+import { STATE_SYMBOL, PROXY_SOURCES } from './constants.js';
import { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.js';
import { get_stack } from './dev/tracing.js';
From 32cee9f77f9fa508372005b022722246118a77c2 Mon Sep 17 00:00:00 2001
From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com>
Date: Mon, 21 Apr 2025 16:52:27 -0700
Subject: [PATCH 11/12] tweak jsdoc description
---
packages/svelte/src/ambient.d.ts | 4 +++-
packages/svelte/types/index.d.ts | 4 +++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts
index ff8fd370d1b0..11586918b51a 100644
--- a/packages/svelte/src/ambient.d.ts
+++ b/packages/svelte/src/ambient.d.ts
@@ -94,7 +94,7 @@ declare namespace $state {
: never;
/**
- * Forces an update on a `$state` or `$state.raw` variable or class field.
+ * Forces an update on a variable or class field declared with `$state` or `$state.raw`, or a property of a `$state` object.
* This is primarily meant as an escape hatch to be able to use external or native classes
* with Svelte's reactivity system.
* If you used Svelte 3 or 4, this is the equivalent of `foo = foo`.
@@ -114,6 +114,8 @@ declare namespace $state {
* Count is {counter.count}
*
* ```
+ *
+ * https://svelte.dev/docs/svelte/$state#$state.invalidate
*/
export function invalidate(source: unknown): void;
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index b5f9a4b54a02..97a86a3f83de 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -2926,7 +2926,7 @@ declare namespace $state {
: never;
/**
- * Forces an update on a `$state` or `$state.raw` variable or class field.
+ * Forces an update on a variable or class field declared with `$state` or `$state.raw`, or a property of a `$state` object.
* This is primarily meant as an escape hatch to be able to use external or native classes
* with Svelte's reactivity system.
* If you used Svelte 3 or 4, this is the equivalent of `foo = foo`.
@@ -2946,6 +2946,8 @@ declare namespace $state {
* Count is {counter.count}
*
* ```
+ *
+ * https://svelte.dev/docs/svelte/$state#$state.invalidate
*/
export function invalidate(source: unknown): void;
From ff8dd1033263f8a3ac67628a0c503282a62f8ded Mon Sep 17 00:00:00 2001
From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com>
Date: Mon, 21 Apr 2025 16:58:27 -0700
Subject: [PATCH 12/12] lint
---
packages/svelte/src/ambient.d.ts | 4 ++--
packages/svelte/types/index.d.ts | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts
index 11586918b51a..28373a784532 100644
--- a/packages/svelte/src/ambient.d.ts
+++ b/packages/svelte/src/ambient.d.ts
@@ -94,7 +94,7 @@ declare namespace $state {
: never;
/**
- * Forces an update on a variable or class field declared with `$state` or `$state.raw`, or a property of a `$state` object.
+ * Forces an update on a variable or class field declared with `$state` or `$state.raw`, or a property of a `$state` object.
* This is primarily meant as an escape hatch to be able to use external or native classes
* with Svelte's reactivity system.
* If you used Svelte 3 or 4, this is the equivalent of `foo = foo`.
@@ -114,7 +114,7 @@ declare namespace $state {
* Count is {counter.count}
*
* ```
- *
+ *
* https://svelte.dev/docs/svelte/$state#$state.invalidate
*/
export function invalidate(source: unknown): void;
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index 97a86a3f83de..208a129f31a8 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -2926,7 +2926,7 @@ declare namespace $state {
: never;
/**
- * Forces an update on a variable or class field declared with `$state` or `$state.raw`, or a property of a `$state` object.
+ * Forces an update on a variable or class field declared with `$state` or `$state.raw`, or a property of a `$state` object.
* This is primarily meant as an escape hatch to be able to use external or native classes
* with Svelte's reactivity system.
* If you used Svelte 3 or 4, this is the equivalent of `foo = foo`.
@@ -2946,7 +2946,7 @@ declare namespace $state {
* Count is {counter.count}
*
* ```
- *
+ *
* https://svelte.dev/docs/svelte/$state#$state.invalidate
*/
export function invalidate(source: unknown): void;