Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-donuts-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

chore: emit `await_reactivity_loss` in `for await` loops
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ 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 { ForOfStatement } from './visitors/ForOfStatement.js';
import { Fragment } from './visitors/Fragment.js';
import { FunctionDeclaration } from './visitors/FunctionDeclaration.js';
import { FunctionExpression } from './visitors/FunctionExpression.js';
Expand Down Expand Up @@ -103,6 +104,7 @@ const visitors = {
EachBlock,
ExportNamedDeclaration,
ExpressionStatement,
ForOfStatement,
Fragment,
FunctionDeclaration,
FunctionExpression,
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
17 changes: 17 additions & 0 deletions packages/svelte/src/compiler/utils/builders.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: _await
};
}

/**
* @param {ESTree.Identifier} id
* @param {ESTree.Pattern[]} params
Expand Down
6 changes: 5 additions & 1 deletion packages/svelte/src/internal/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
45 changes: 45 additions & 0 deletions packages/svelte/src/internal/client/reactivity/async.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,51 @@ 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 {Iterable<T, TReturn> | AsyncIterable<T, TReturn>} iterable
* @returns {AsyncGenerator<T, TReturn | undefined>}
*/
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<T, TReturn>} */
// @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 {
while (true) {
const { done, value } = (await track_reactivity_loss(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 && iterator.return !== undefined) {
// eslint-disable-next-line no-unsafe-finally
return /** @type {TReturn} */ ((await track_reactivity_loss(iterator.return()))().value);
}
}
}

export function unset_context() {
set_active_effect(null);
set_active_reaction(null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { tick } from 'svelte';
import { test } from '../../test';

export default test({
compileOptions: {
dev: true
},

html: `<button>a</button><button>b</button><p>pending</p>`,

async test({ assert, target, warnings }) {
await tick();
assert.htmlEqual(target.innerHTML, '<button>a</button><button>b</button><h1>3</h1>');

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);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script>
let values = $state([1, 2]);

async function get_total() {
let total = 0;

for await (const n of values) {
total += n;
}

return total;
}
</script>

<button onclick={() => values[0]++}>a</button>
<button onclick={() => values[1]++}>b</button>

<svelte:boundary>
<h1>{await get_total()}</h1>

{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>
Loading