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/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index 741e24fde01e..2c9d3e260d25 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -166,6 +166,54 @@ 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.invalidate` + +In the case that you aren't using a proxied `$state` via use of `$state.raw` or a class instance, you may need to tell Svelte a `$state` has changed. You can do so via `$state.invalidate`: + +```svelte + + +``` + +`$state.invalidate` can also be used with reactive class fields, and properties of `$state` objects: + +```js +class Box { + value; + + constructor(initial) { + this.value = initial; + } +} + +class Counter { + count = $state(new Box(0)); + + increment() { + 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 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/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 8fdb7770aa9b..4d6b20c27be0 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -196,6 +196,12 @@ This restriction only applies when using the `experimental.async` option, which 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/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 957a9f67c7b0..d11498d66e70 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -908,6 +908,37 @@ Cannot export state from a module if it is reassigned. Either export a function `%rune%(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor. ``` +### 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 or non-computed class field declared with `$state` or `$state.raw` as its argument +``` + ### store_invalid_scoped_subscription ``` diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index 57ecca048977..ad5094564bed 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -146,6 +146,10 @@ This restriction only applies when using the `experimental.async` option, which > 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/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index 5c1080acedfe..535427b9c224 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -260,6 +260,33 @@ class Counter { > `%rune%(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor. +## 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 or non-computed class field declared with `$state` or `$state.raw` as its argument + ## store_invalid_scoped_subscription > Cannot subscribe to stores that are not declared at the top level of the component diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index ad32eaa56f5e..14b7e78fb106 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -93,6 +93,32 @@ declare namespace $state { : never : never; + /** + * 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`. + * Example: + * ```svelte + * + * + * ``` + * + * https://svelte.dev/docs/svelte/$state#$state.invalidate + */ + 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 e763a6e0733a..94f8f7d30363 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -529,6 +529,24 @@ export function state_invalid_placement(node, rune) { e(node, 'state_invalid_placement', `\`${rune}(...)\` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.\nhttps://svelte.dev/e/state_invalid_placement`); } +/** + * `$state.invalidate` can only be called with an argument referencing `this` in a class using a non-computed property + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function state_invalidate_invalid_this_property(node) { + e(node, 'state_invalidate_invalid_this_property', `\`$state.invalidate\` can only be called with an argument referencing \`this\` in a class using a non-computed property\nhttps://svelte.dev/e/state_invalidate_invalid_this_property`); +} + +/** + * `$state.invalidate` only takes a variable or non-computed class field 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 or non-computed class field 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 9b6337b9ed9a..7fdff6ffe5b0 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 } 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 '#compiler/builders'; @@ -111,6 +111,62 @@ 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' && arg.type !== 'MemberExpression') { + e.state_invalidate_nonreactive_argument(node); + } + if (arg.type === 'MemberExpression') { + if (arg.object.type !== 'ThisExpression') { + 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 (!class_body) { + e.state_invalidate_invalid_this_property(node); + } + const possible_this_bindings = context.path.filter((parent, index) => { + return ( + parent.type === 'FunctionDeclaration' || + (parent.type === 'FunctionExpression' && + context.path[index - 1]?.type !== 'MethodDefinition') + ); + }); + if (possible_this_bindings.length === 0) { + break; + } + const class_index = context.path.indexOf(class_body); + const last_possible_this_index = context.path.indexOf( + /** @type {AST.SvelteNode} */ (possible_this_bindings.at(-1)) + ); + if (class_index < last_possible_this_index) { + e.state_invalidate_invalid_this_property(node); + } + // we can't really do anything else yet, so we just wait for the transformation phase + // where we know which class fields are reactive (and what their private aliases are) + break; + } else { + 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': 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 c126742d3c5c..bc9e989b20ae 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,16 +1,33 @@ -/** @import { CallExpression, Expression } 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 '#compiler/builders'; import { get_rune } from '../../../scope.js'; import { transform_inspect_rune } from '../../utils.js'; +import * as e from '../../../../errors.js'; import { should_proxy } from '../utils.js'; +import { get_name } from '../../../nodes.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 + }; + } const rune = get_rune(node, context.state.scope); switch (rune) { @@ -57,6 +74,42 @@ export function CallExpression(node, context) { /** @type {Expression} */ (context.visit(node.arguments[0])), is_ignored(node, 'state_snapshot_uncloneable') && b.true ); + /* eslint-disable no-fallthrough */ + case '$state.invalidate': + if (node.arguments[0].type === 'Identifier') { + return b.call( + attach_locations(/** @type {Expression} */ (node.callee), b.id('$.invalidate')), + node.arguments[0] + ); + } else if (node.arguments[0].type === 'MemberExpression') { + const { object, property } = node.arguments[0]; + if (object.type === 'ThisExpression') { + const name = /** @type {string} */ (get_name(property)); + const field = context.state.state_fields.get(name); + if (!field || (field.type !== '$state' && field.type !== '$state.raw')) { + e.state_invalidate_nonreactive_argument(node); + } + return b.call( + attach_locations(/** @type {Expression} */ (node.callee), b.id('$.invalidate')), + attach_locations(node.arguments[0], b.member(object, field.key)) + ); + } + /** @type {Expression[]} */ + const source_args = /** @type {Expression[]} */ ([ + 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( + attach_locations(/** @type {Expression} */ (node.callee), b.id('$.invalidate')), + attach_locations( + /** @type {Expression} */ (node.arguments[0]), + /** @type {Expression} */ (arg) + ) + ); + } case '$effect.root': return b.call( diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js index 41d3202ce9ea..414c1d6a319c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js @@ -25,6 +25,10 @@ export function CallExpression(node, context) { return b.arrow([], b.block([])); } + if (rune === '$state.invalidate') { + return b.void0; + } + if (rune === '$effect.pending') { return b.literal(0); } diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 50a7a21ae80f..5e6a7a7af79d 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -29,6 +29,7 @@ export const ERROR_VALUE = 1 << 23; 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'); export const PROXY_PATH_SYMBOL = Symbol('proxy path'); /** allow users to ignore aborted signal errors if `reason.name === 'StaleReactionError` */ diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 937971da5e0b..e42bf89201c0 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -393,6 +393,22 @@ 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/index.js b/packages/svelte/src/internal/client/index.js index 90f0f9baaccb..e0392733d904 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -117,7 +117,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, @@ -151,7 +159,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 3ae4b87ed5d6..21b03dd2ca08 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -15,6 +15,7 @@ import { is_array, object_prototype } from '../shared/utils.js'; +import { PROXY_PATH_SYMBOL, PROXY_SOURCES, STATE_SYMBOL } from '#client/constants'; import { state as source, set, @@ -22,7 +23,6 @@ import { flush_inspect_effects, set_inspect_effects_deferred } from './reactivity/sources.js'; -import { PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; import { UNINITIALIZED } from '../../constants.js'; import * as e from './errors.js'; import { get_stack, tag } from './dev/tracing.js'; @@ -163,6 +163,10 @@ export function proxy(value) { return value; } + if (prop === PROXY_SOURCES) { + return sources; + } + if (DEV && prop === PROXY_PATH_SYMBOL) { return update_path; } @@ -218,7 +222,7 @@ export function proxy(value) { }, has(target, prop) { - if (prop === STATE_SYMBOL) { + if (prop === STATE_SYMBOL || prop === PROXY_SOURCES) { return true; } @@ -388,6 +392,25 @@ 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; +} + const ARRAY_MUTATING_METHODS = new Set([ 'copyWithin', 'fill', diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 3b2087d56bc9..1054f32c206c 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -261,6 +261,81 @@ export function flush_inspect_effects() { inspect_effects.clear(); } +/** + * @param {Source | null} source + */ +export function invalidate(source) { + if (source === null || (source.f & DERIVED) !== 0) { + e.state_invalidate_invalid_source(); + } + if ( + active_reaction !== null && + // since we are untracking the function inside `$inspect.with` we need to add this check + // to ensure we error if state is set inside an inspect effect + (!untracking || (active_reaction.f & INSPECT_EFFECT) !== 0) && + is_runes() && + (active_reaction.f & (DERIVED | BLOCK_EFFECT | ASYNC | INSPECT_EFFECT)) !== 0 && + !current_sources?.includes(source) + ) { + e.state_unsafe_mutation(); + } + + if (DEV) { + if (tracing_mode_flag || active_effect !== null) { + const error = get_stack('UpdatedAt'); + + if (error !== null) { + source.updated ??= new Map(); + let entry = source.updated.get(error.stack); + + if (!entry) { + entry = { error, count: 0 }; + source.updated.set(error.stack, entry); + } + + entry.count++; + } + } + + if (active_effect !== null) { + source.set_during_effect = true; + } + } + + if ((source.f & DERIVED) !== 0) { + // if we are assigning to a dirty derived we set it to clean/maybe dirty but we also eagerly execute it to track the dependencies + if ((source.f & DIRTY) !== 0) { + execute_derived(/** @type {Derived} */ (source)); + } + set_signal_status(source, (source.f & UNOWNED) === 0 ? CLEAN : MAYBE_DIRTY); + } + + 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 && !inspect_effects_deferred) { + flush_inspect_effects(); + } +} + /** * @template {number | bigint} T * @param {Source} source diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index cd79cfc27467..efe717844e05 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -437,6 +437,7 @@ const STATE_CREATION_RUNES = /** @type {const} */ ([ const RUNES = /** @type {const} */ ([ ...STATE_CREATION_RUNES, + '$state.invalidate', '$state.snapshot', '$props', '$props.id', diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index eff6d6166a5e..e32250cf8a8a 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -9,7 +9,13 @@ import { user_effect, user_pre_effect } from '../../src/internal/client/reactivity/effects'; -import { state, set, update, update_pre } from '../../src/internal/client/reactivity/sources'; +import { + state, + set, + update, + update_pre, + invalidate +} from '../../src/internal/client/reactivity/sources'; import type { Derived, Effect, Source, Value } from '../../src/internal/client/types'; import { proxy } from '../../src/internal/client/proxy'; import { derived } from '../../src/internal/client/reactivity/deriveds'; @@ -1391,6 +1397,26 @@ describe('signals', () => { }; }); + test('invalidate reruns dependent effects', () => { + let updates = 0; + return () => { + const a = state(0); + const destroy = effect_root(() => { + render_effect(() => { + $.get(a); + updates++; + }); + }); + set(a, 1); + flushSync(); + assert.equal(updates, 2); + invalidate(a); + flushSync(); + assert.equal(updates, 3); + destroy(); + }; + }); + test('$effect.root inside deriveds stay alive independently', () => { const log: any[] = []; const c = state(0); diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 64aa9e23baf6..01a725e9314b 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -3177,6 +3177,32 @@ declare namespace $state { : never : never; + /** + * 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`. + * Example: + * ```svelte + * + * + * ``` + * + * https://svelte.dev/docs/svelte/$state#$state.invalidate + */ + export function invalidate(source: unknown): void; + /** * Declares state that is _not_ made deeply reactive — instead of mutating it, * you must reassign it.