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] 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 0000000000..a0ee39f4e2 --- /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 a56aca9c5f..aa770d6490 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 0000000000..a5d2751812 --- /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 c241cdb445..0aa68f06be 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 90f0f9baac..c094c9e044 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 c200f10dba..fd4c15ee55 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);