Skip to content

Commit 1c26d8f

Browse files
committed
chore: emit await_reactivity_loss in for await loops
1 parent d82edf6 commit 1c26d8f

File tree

6 files changed

+87
-1
lines changed

6 files changed

+87
-1
lines changed

.changeset/quiet-donuts-wonder.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
chore: emit `await_reactivity_loss` in `for await` loops

packages/svelte/src/compiler/phases/3-transform/client/transform-client.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { EachBlock } from './visitors/EachBlock.js';
2727
import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js';
2828
import { ExpressionStatement } from './visitors/ExpressionStatement.js';
2929
import { Fragment } from './visitors/Fragment.js';
30+
import { ForOfStatement } from './visitors/ForOfStatement.js';
3031
import { FunctionDeclaration } from './visitors/FunctionDeclaration.js';
3132
import { FunctionExpression } from './visitors/FunctionExpression.js';
3233
import { HtmlTag } from './visitors/HtmlTag.js';
@@ -104,6 +105,7 @@ const visitors = {
104105
ExportNamedDeclaration,
105106
ExpressionStatement,
106107
Fragment,
108+
ForOfStatement,
107109
FunctionDeclaration,
108110
FunctionExpression,
109111
HtmlTag,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/** @import { Expression, ForOfStatement, Pattern, Statement, VariableDeclaration } from 'estree' */
2+
/** @import { ComponentContext } from '../types' */
3+
import * as b from '#compiler/builders';
4+
import { dev, is_ignored } from '../../../../state.js';
5+
6+
/**
7+
* @param {ForOfStatement} node
8+
* @param {ComponentContext} context
9+
*/
10+
export function ForOfStatement(node, context) {
11+
if (node.await && dev && !is_ignored(node, 'await_reactivity_loss')) {
12+
const left = /** @type {VariableDeclaration | Pattern} */ (context.visit(node.left));
13+
const argument = /** @type {Expression} */ (context.visit(node.right));
14+
const body = /** @type {Statement} */ (context.visit(node.body));
15+
const right = b.call('$.for_await_track_reactivity_loss', argument);
16+
return b.for_of(left, right, body, true);
17+
}
18+
19+
context.next();
20+
}

packages/svelte/src/compiler/utils/builders.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,23 @@ export function export_default(declaration) {
214214
return { type: 'ExportDefaultDeclaration', declaration };
215215
}
216216

217+
/**
218+
* @param {ESTree.VariableDeclaration | ESTree.Pattern} left
219+
* @param {ESTree.Expression} right
220+
* @param {ESTree.Statement} body
221+
* @param {boolean} [await]
222+
* @returns {ESTree.ForOfStatement}
223+
*/
224+
export function for_of(left, right, body, await = false) {
225+
return {
226+
type: 'ForOfStatement',
227+
left,
228+
right,
229+
body,
230+
await
231+
};
232+
}
233+
217234
/**
218235
* @param {ESTree.Identifier} id
219236
* @param {ESTree.Pattern[]} params

packages/svelte/src/internal/client/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,11 @@ export {
9898
props_id,
9999
with_script
100100
} from './dom/template.js';
101-
export { save, track_reactivity_loss } from './reactivity/async.js';
101+
export {
102+
for_await_track_reactivity_loss,
103+
save,
104+
track_reactivity_loss
105+
} from './reactivity/async.js';
102106
export { flushSync as flush, suspend } from './reactivity/batch.js';
103107
export {
104108
async_derived,

packages/svelte/src/internal/client/reactivity/async.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,44 @@ export async function track_reactivity_loss(promise) {
119119
};
120120
}
121121

122+
/**
123+
* Used in `for await` loops in DEV, so
124+
* that we can emit `await_reactivity_loss` warnings
125+
* after each `async_iterator` result resolves and
126+
* after the `async_iterator` return resolves (if it runs)
127+
* @template T
128+
* @template TReturn
129+
* @param {AsyncIterator<T, TReturn>} async_iterator
130+
* @returns {AsyncGenerator<T, TReturn | undefined>}
131+
*/
132+
export async function* for_await_track_reactivity_loss(async_iterator) {
133+
// This is based on the algorithms described in ECMA-262:
134+
// ForIn/OfBodyEvaluation
135+
// https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset
136+
// AsyncIteratorClose
137+
// https://tc39.es/ecma262/multipage/abstract-operations.html#sec-asynciteratorclose
138+
139+
/** Whether the completion of the iterator was "normal", meaning it wasn't ended via `break` or a similar method */
140+
let normal_completion = false;
141+
try {
142+
while (true) {
143+
const { done, value } = (await track_reactivity_loss(async_iterator.next()))();
144+
if (done) {
145+
normal_completion = true;
146+
break;
147+
}
148+
yield value;
149+
}
150+
} finally {
151+
// If the iterator had a normal completion and `return` is defined on the iterator, call it and return the value
152+
if (normal_completion && async_iterator.return !== undefined) {
153+
return /** @type {TReturn} */ (
154+
(await track_reactivity_loss(async_iterator.return()))().value
155+
);
156+
}
157+
}
158+
}
159+
122160
export function unset_context() {
123161
set_active_effect(null);
124162
set_active_reaction(null);

0 commit comments

Comments
 (0)