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; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy