From 1c26d8f146d08c443a0fefd7e5e01a6c88399877 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Mon, 28 Jul 2025 17:26:41 -0700 Subject: [PATCH 1/8] chore: emit `await_reactivity_loss` in `for await` loops --- .changeset/quiet-donuts-wonder.md | 5 +++ .../3-transform/client/transform-client.js | 2 + .../client/visitors/ForOfStatement.js | 20 ++++++++++ .../svelte/src/compiler/utils/builders.js | 17 +++++++++ packages/svelte/src/internal/client/index.js | 6 ++- .../src/internal/client/reactivity/async.js | 38 +++++++++++++++++++ 6 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 .changeset/quiet-donuts-wonder.md create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/ForOfStatement.js diff --git a/.changeset/quiet-donuts-wonder.md b/.changeset/quiet-donuts-wonder.md new file mode 100644 index 000000000000..a0ee39f4e2b0 --- /dev/null +++ b/.changeset/quiet-donuts-wonder.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: emit `await_reactivity_loss` in `for await` loops diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index a56aca9c5f0b..aa770d649059 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -27,6 +27,7 @@ import { EachBlock } from './visitors/EachBlock.js'; import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js'; import { ExpressionStatement } from './visitors/ExpressionStatement.js'; import { Fragment } from './visitors/Fragment.js'; +import { ForOfStatement } from './visitors/ForOfStatement.js'; import { FunctionDeclaration } from './visitors/FunctionDeclaration.js'; import { FunctionExpression } from './visitors/FunctionExpression.js'; import { HtmlTag } from './visitors/HtmlTag.js'; @@ -104,6 +105,7 @@ const visitors = { ExportNamedDeclaration, ExpressionStatement, Fragment, + ForOfStatement, FunctionDeclaration, FunctionExpression, HtmlTag, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ForOfStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ForOfStatement.js new file mode 100644 index 000000000000..a5d2751812cc --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ForOfStatement.js @@ -0,0 +1,20 @@ +/** @import { Expression, ForOfStatement, Pattern, Statement, VariableDeclaration } from 'estree' */ +/** @import { ComponentContext } from '../types' */ +import * as b from '#compiler/builders'; +import { dev, is_ignored } from '../../../../state.js'; + +/** + * @param {ForOfStatement} node + * @param {ComponentContext} context + */ +export function ForOfStatement(node, context) { + if (node.await && dev && !is_ignored(node, 'await_reactivity_loss')) { + const left = /** @type {VariableDeclaration | Pattern} */ (context.visit(node.left)); + const argument = /** @type {Expression} */ (context.visit(node.right)); + const body = /** @type {Statement} */ (context.visit(node.body)); + const right = b.call('$.for_await_track_reactivity_loss', argument); + return b.for_of(left, right, body, true); + } + + context.next(); +} diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index c241cdb44532..0aa68f06bea7 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -214,6 +214,23 @@ export function export_default(declaration) { return { type: 'ExportDefaultDeclaration', declaration }; } +/** + * @param {ESTree.VariableDeclaration | ESTree.Pattern} left + * @param {ESTree.Expression} right + * @param {ESTree.Statement} body + * @param {boolean} [await] + * @returns {ESTree.ForOfStatement} + */ +export function for_of(left, right, body, await = false) { + return { + type: 'ForOfStatement', + left, + right, + body, + await + }; +} + /** * @param {ESTree.Identifier} id * @param {ESTree.Pattern[]} params diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 90f0f9baaccb..c094c9e04449 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -98,7 +98,11 @@ export { props_id, with_script } from './dom/template.js'; -export { save, track_reactivity_loss } from './reactivity/async.js'; +export { + for_await_track_reactivity_loss, + save, + track_reactivity_loss +} from './reactivity/async.js'; export { flushSync as flush, suspend } from './reactivity/batch.js'; export { async_derived, diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index c200f10dba8f..fd4c15ee550d 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -119,6 +119,44 @@ export async function track_reactivity_loss(promise) { }; } +/** + * Used in `for await` loops in DEV, so + * that we can emit `await_reactivity_loss` warnings + * after each `async_iterator` result resolves and + * after the `async_iterator` return resolves (if it runs) + * @template T + * @template TReturn + * @param {AsyncIterator} async_iterator + * @returns {AsyncGenerator} + */ +export async function* for_await_track_reactivity_loss(async_iterator) { + // This is based on the algorithms described in ECMA-262: + // ForIn/OfBodyEvaluation + // https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset + // AsyncIteratorClose + // https://tc39.es/ecma262/multipage/abstract-operations.html#sec-asynciteratorclose + + /** Whether the completion of the iterator was "normal", meaning it wasn't ended via `break` or a similar method */ + let normal_completion = false; + try { + while (true) { + const { done, value } = (await track_reactivity_loss(async_iterator.next()))(); + if (done) { + normal_completion = true; + break; + } + yield value; + } + } finally { + // If the iterator had a normal completion and `return` is defined on the iterator, call it and return the value + if (normal_completion && async_iterator.return !== undefined) { + return /** @type {TReturn} */ ( + (await track_reactivity_loss(async_iterator.return()))().value + ); + } + } +} + export function unset_context() { set_active_effect(null); set_active_reaction(null); From ff85f271adc9a99cafccd43415593e7bfa1e0595 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:10:59 -0700 Subject: [PATCH 2/8] oops --- packages/svelte/src/compiler/utils/builders.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 0aa68f06bea7..81c4e6b8e0c5 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -218,16 +218,16 @@ export function export_default(declaration) { * @param {ESTree.VariableDeclaration | ESTree.Pattern} left * @param {ESTree.Expression} right * @param {ESTree.Statement} body - * @param {boolean} [await] + * @param {boolean} [_await] * @returns {ESTree.ForOfStatement} */ -export function for_of(left, right, body, await = false) { +export function for_of(left, right, body, _await = false) { return { type: 'ForOfStatement', left, right, body, - await + await: _await }; } From 1a26daf2fd64857c39f7c7a1c489d0562e3afc49 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:21:00 -0700 Subject: [PATCH 3/8] fix lint --- packages/svelte/src/internal/client/reactivity/async.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index fd4c15ee550d..659fb061c379 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -150,6 +150,7 @@ export async function* for_await_track_reactivity_loss(async_iterator) { } finally { // If the iterator had a normal completion and `return` is defined on the iterator, call it and return the value if (normal_completion && async_iterator.return !== undefined) { + // eslint-disable-next-line no-unsafe-finally return /** @type {TReturn} */ ( (await track_reactivity_loss(async_iterator.return()))().value ); From a3e1329c091f1729a92194ffe3eec5d4dc73c246 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:42:33 -0700 Subject: [PATCH 4/8] fix import order --- .../compiler/phases/3-transform/client/transform-client.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index aa770d649059..0737f531a5b4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -26,8 +26,8 @@ import { DebugTag } from './visitors/DebugTag.js'; import { EachBlock } from './visitors/EachBlock.js'; import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js'; import { ExpressionStatement } from './visitors/ExpressionStatement.js'; -import { Fragment } from './visitors/Fragment.js'; import { ForOfStatement } from './visitors/ForOfStatement.js'; +import { Fragment } from './visitors/Fragment.js'; import { FunctionDeclaration } from './visitors/FunctionDeclaration.js'; import { FunctionExpression } from './visitors/FunctionExpression.js'; import { HtmlTag } from './visitors/HtmlTag.js'; @@ -104,8 +104,8 @@ const visitors = { EachBlock, ExportNamedDeclaration, ExpressionStatement, - Fragment, ForOfStatement, + Fragment, FunctionDeclaration, FunctionExpression, HtmlTag, From 0b7da7d83fa4987514cd3c4789d9aa00a3c80b71 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 5 Aug 2025 21:08:11 -0400 Subject: [PATCH 5/8] input is an iterable, not an iterator --- .../src/internal/client/reactivity/async.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 659fb061c379..fb6bc15c844e 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -126,21 +126,25 @@ export async function track_reactivity_loss(promise) { * after the `async_iterator` return resolves (if it runs) * @template T * @template TReturn - * @param {AsyncIterator} async_iterator + * @param {Iterable | AsyncIterable} iterable * @returns {AsyncGenerator} */ -export async function* for_await_track_reactivity_loss(async_iterator) { +export async function* for_await_track_reactivity_loss(iterable) { // This is based on the algorithms described in ECMA-262: // ForIn/OfBodyEvaluation // https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset // AsyncIteratorClose // https://tc39.es/ecma262/multipage/abstract-operations.html#sec-asynciteratorclose + /** @type {AsyncIterator} */ + // @ts-ignore + const iterator = iterable[Symbol.asyncIterator]?.() ?? iterable[Symbol.iterator]?.(); + /** Whether the completion of the iterator was "normal", meaning it wasn't ended via `break` or a similar method */ let normal_completion = false; try { while (true) { - const { done, value } = (await track_reactivity_loss(async_iterator.next()))(); + const { done, value } = (await track_reactivity_loss(iterator.next()))(); if (done) { normal_completion = true; break; @@ -149,11 +153,9 @@ export async function* for_await_track_reactivity_loss(async_iterator) { } } finally { // If the iterator had a normal completion and `return` is defined on the iterator, call it and return the value - if (normal_completion && async_iterator.return !== undefined) { + if (normal_completion && iterator.return !== undefined) { // eslint-disable-next-line no-unsafe-finally - return /** @type {TReturn} */ ( - (await track_reactivity_loss(async_iterator.return()))().value - ); + return /** @type {TReturn} */ ((await track_reactivity_loss(iterator.return()))().value); } } } From bbc64f951510c18708fd22917a7b71d60e315d29 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 5 Aug 2025 21:10:07 -0400 Subject: [PATCH 6/8] handle non-iterables --- packages/svelte/src/internal/client/reactivity/async.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index fb6bc15c844e..def112f6d828 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -140,6 +140,10 @@ export async function* for_await_track_reactivity_loss(iterable) { // @ts-ignore const iterator = iterable[Symbol.asyncIterator]?.() ?? iterable[Symbol.iterator]?.(); + if (iterator === undefined) { + throw new TypeError('value is not async iterable'); + } + /** Whether the completion of the iterator was "normal", meaning it wasn't ended via `break` or a similar method */ let normal_completion = false; try { From f158cbd252872bfd435f71c4a25274470bb3d527 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 5 Aug 2025 21:10:13 -0400 Subject: [PATCH 7/8] add test --- .../_config.js | 24 +++++++++++++++++++ .../main.svelte | 24 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js new file mode 100644 index 000000000000..bde65a499f3b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js @@ -0,0 +1,24 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + html: `

pending

`, + + async test({ assert, target, warnings }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

3

'); + + assert.equal( + warnings[0], + 'Detected reactivity loss when reading `values[1]`. This happens when state is read in an async function after an earlier `await`' + ); + + assert.equal(warnings[1].name, 'TracedAtError'); + + assert.equal(warnings.length, 2); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/main.svelte new file mode 100644 index 000000000000..92a6ec18bcb2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/main.svelte @@ -0,0 +1,24 @@ + + + + + + +

{await get_total()}

+ + {#snippet pending()} +

pending

+ {/snippet} +
From 88ba8e46f71b8a5355c96d0e64d0f8252efcd864 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 5 Aug 2025 21:15:30 -0400 Subject: [PATCH 8/8] typescript. shrug --- packages/svelte/src/internal/client/reactivity/async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index def112f6d828..2b133e5f4492 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -126,7 +126,7 @@ export async function track_reactivity_loss(promise) { * after the `async_iterator` return resolves (if it runs) * @template T * @template TReturn - * @param {Iterable | AsyncIterable} iterable + * @param {Iterable | AsyncIterable} iterable * @returns {AsyncGenerator} */ export async function* for_await_track_reactivity_loss(iterable) {