From 7018c092479f9bee2b07f533246a6cf2a683e794 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 6 Oct 2026 13:55:01 +0200 Subject: [PATCH] make await blocks fork-aware --- .../src/internal/client/dom/blocks/await.js | 87 ++++++++--- .../_config.js | 63 ++++++++ .../main.svelte | 29 ++++ .../_config.js | 40 ++++++ .../main.svelte | 38 +++++ .../async-fork-await-block-pending/_config.js | 136 ++++++++++++++++++ .../main.svelte | 27 ++++ .../_config.js | 23 +++ .../main.svelte | 25 ++++ .../async-fork-await-block-static/_config.js | 25 ++++ .../async-fork-await-block-static/main.svelte | 19 +++ .../samples/async-fork-await-block/_config.js | 89 ++++++++++++ .../async-fork-await-block/main.svelte | 24 ++++ 13 files changed, 603 insertions(+), 22 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-await-block-deferred-commit/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-await-block-deferred-commit/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-await-block-interleaved/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-await-block-interleaved/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-await-block-pending/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-await-block-pending/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-await-block-real-pending/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-await-block-real-pending/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-await-block-static/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-await-block-static/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-await-block/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-await-block/main.svelte diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 194a7ccc0e..ed4f6dbb06 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -11,7 +11,9 @@ import { hydrate_node } from '../hydration.js'; import { queue_micro_task } from '../task.js'; +import { should_defer_append } from '../operations.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; +import { ASYNC, DESTROYED, EFFECT_PRESERVED } from '#client/constants'; import { is_runes } from '../../context.js'; import { Batch, current_batch, flushSync, is_flushing_sync } from '../../reactivity/batch.js'; import { BranchManager } from './branches.js'; @@ -51,11 +53,38 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { var branches = new BranchManager(node); - block(() => { + // The latest run of the block in the real world, and in each fork. A run is superseded + // by later runs in the same world; a fork's run joins the real world once it is committed + var current = {}; + /** @type {Map} */ + var forks = new Map(); + + /** @param {Batch} batch */ + var settle = (batch) => { + // committed (as opposed to discarded)? + if (!batch.is_fork && forks.has(batch)) current = /** @type {{}} */ (forks.get(batch)); + forks.delete(batch); + }; + + var effect = block(() => { var batch = /** @type {Batch} */ (current_batch); var input = get_input(); + var run = {}; - var destroyed = false; + // Like other async work, a fork's promise resolves into the fork, so that its pending/then/catch + // state stays in the fork until it's committed — unless the block is terminal (has no dependencies), + // in which case it can't re-run and is the same in every batch (see `should_defer_append`) + var fork = should_defer_append() && batch.is_fork; + + if (fork) { + forks.set(batch, run); + batch.oncommit(settle); + batch.ondiscard(settle); + } else { + current = run; + // supersedes the runs of forks that have since been committed + for (const b of forks.keys()) if (!b.is_fork) forks.delete(b); + } /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ // @ts-ignore coercing `node` to a `Comment` causes TypeScript and Prettier to fight @@ -75,22 +104,32 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { * @param {() => void} fn */ const resolve = (fn) => { - if (destroyed) return; + if ((effect.f & DESTROYED) !== 0 || (run !== current && forks.get(batch) !== run)) return; resolved = true; - // We don't want to restore the previous batch here; {#await} blocks don't follow the async logic - // we have elsewhere, instead pending/resolve/fail states are each their own batch so to speak. - restore(false); - // ...but it might still be set here. That means a `save(...)` has restored it — but that batch will - // likely already have been committed by the time it resolves, and this resolve should be processed - // in a separate batch. We're not using batch.deactivate()/activate() above because get_input() - // could write to sources, which would then incorrectly create a new batch or could mess with - // async_derived expecting a current_batch to exist. - if (current_batch === batch) { - batch.deactivate(); + // a committed fork's run is now the real world's run (no-op for real runs) + if (!batch.is_fork) current = run; + + // A fork's promise resolves into the fork — and once committed, into the batch the fork became (or was + // merged into) while that is still pending, so that it lands along with the rest of the fork's state + restore(fork); + var pending = fork && /** @type {Batch} */ (current_batch).linked; + + if (!pending) { + // Otherwise, we don't want to restore the previous batch here; {#await} blocks don't follow the async + // logic we have elsewhere, instead pending/resolve/fail states are each their own batch so to speak. + // ...but it might still be set here. That means a `save(...)` has restored it — but that batch will + // likely already have been committed by the time it resolves, and this resolve should be processed + // in a separate batch. We're not using batch.deactivate()/activate() above because get_input() + // could write to sources, which would then incorrectly create a new batch or could mess with + // async_derived expecting a current_batch to exist. + if (fork || current_batch === batch) { + current_batch?.deactivate(); + } } + // Make sure we have a batch, since the branch manager expects one to exist - Batch.ensure(); + var target = Batch.ensure(); try { fn(); @@ -98,8 +137,10 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { unset_context(false); // without this, the DOM does not update until two ticks after the promise - // resolves, which is unexpected behaviour (and somewhat irksome to test) - if (!is_flushing_sync) flushSync(); + // resolves, which is unexpected behaviour (and somewhat irksome to test). + // A pending batch is flushed directly, so that no other work runs while it is active + if (pending) target.flush(); + else if (!is_flushing_sync) flushSync(); } }; @@ -116,8 +157,8 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { branches.ensure(CATCH, catch_fn && ((target) => catch_fn(target, error))); if (!catch_fn) { - // Rethrow the error if no catch block exists - throw error.v; + // Rethrow the error if no catch block exists (in a fork, `error.v` isn't it) + throw e; } }); } @@ -145,9 +186,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { // continue in hydration mode set_hydrating(true); } + }, EFFECT_PRESERVED); // branches are created asynchronously: keep the effect even without dependencies - return () => { - destroyed = true; - }; - }); + // Like the results of async deriveds, the value/error are those of the batch whose run produced + // them: the real world writing them must not overtake a fork's own result (see `Batch#overtake`) + value.e = error.e = effect; + value.f |= ASYNC; + error.f |= ASYNC; } diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-deferred-commit/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-deferred-commit/_config.js new file mode 100644 index 0000000000..85d02494a2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-deferred-commit/_config.js @@ -0,0 +1,63 @@ +import { fork, tick } from 'svelte'; +import { test } from '../../test'; + +// A fork that is committed while it still waits for other async work (here: `

`) is a pending real batch: +// its {#await} promise is the real world's promise from then on, and as long as the batch is pending, +// the promise's states land along with the rest of the fork's state. Once the batch is done, they +// are their own batches again, like for any real update +export default test({ + mode: ['client'], + async test({ assert, target, instance }) { + instance.settle('p', 0); + await tick(); + instance.settle('aw', 0); + await tick(); + assert.htmlEqual(target.innerHTML, '

0

0'); + + // resolves before the batch is done; the real world's older promise is obsolete once the fork is committed + instance.set({ a: 1 }); + await tick(); + assert.htmlEqual(target.innerHTML, '

0

pending'); + let f = fork(() => instance.set({ b: 10 })); + await tick(); + f.commit(); + await tick(); + instance.settle('aw', 11); + await tick(); + assert.htmlEqual(target.innerHTML, '

0

pending'); + instance.settle('aw', 1); + await tick(); + assert.htmlEqual(target.innerHTML, '

0

pending'); + instance.settle('p', 10); + await tick(); + assert.htmlEqual(target.innerHTML, '

10

11'); + + // resolves after the batch is done + f = fork(() => instance.set({ b: 20 })); + await tick(); + f.commit(); + await tick(); + assert.htmlEqual(target.innerHTML, '

10

11'); + instance.settle('p', 20); + await tick(); + assert.htmlEqual(target.innerHTML, '

20

pending'); + instance.settle('aw', 21); + await tick(); + assert.htmlEqual(target.innerHTML, '

20

21'); + + // a real update after the commit supersedes the fork's promise + f = fork(() => instance.set({ b: 30 })); + await tick(); + f.commit(); + await tick(); + instance.set({ a: 2 }); + await tick(); + instance.settle('aw', 31); + await tick(); + instance.settle('aw', 32); + await tick(); + instance.settle('p', 30); + await tick(); + assert.htmlEqual(target.innerHTML, '

30

32'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-deferred-commit/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-deferred-commit/main.svelte new file mode 100644 index 0000000000..393695108f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-deferred-commit/main.svelte @@ -0,0 +1,29 @@ + + +

{await f('p', b)}

+{#await f('aw', a + b)}pending{:then v}{v}{/await} diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-interleaved/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-interleaved/_config.js new file mode 100644 index 0000000000..2943ab3808 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-interleaved/_config.js @@ -0,0 +1,40 @@ +import { fork, tick } from 'svelte'; +import { test } from '../../test'; + +// Resolving a fork's {#await} promise must not interfere with real work happening at the same time +export default test({ + mode: ['client'], + async test({ assert, target, instance }) { + instance.settle('a', 0); + instance.settle('b', 0); + await tick(); + assert.htmlEqual(target.innerHTML, '00|0'); + + // the real update's pending state is queued while the fork's promise resolves: + // it must not end up in the fork + let f = fork(() => instance.set({ b: 1 })); + await tick(); + instance.set({ a: 1 }); + instance.settle('b', 1); + await tick(); + assert.htmlEqual(target.innerHTML, 'pending0|1'); + f.discard(); + await tick(); + assert.htmlEqual(target.innerHTML, 'pending0|1'); + instance.settle('a', 1); + await tick(); + assert.htmlEqual(target.innerHTML, '10|1'); + + // a real update right after the fork's promise resolved (in the fork) must still show + f = fork(() => instance.set({ b: 2 })); + await tick(); + instance.latest('b').then(() => instance.set({ a: 5 })); + instance.settle('b', 2); + await tick(); + assert.htmlEqual(target.innerHTML, 'pending0|5'); + f.discard(); + instance.settle('a', 5); + await tick(); + assert.htmlEqual(target.innerHTML, '50|5'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-interleaved/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-interleaved/main.svelte new file mode 100644 index 0000000000..7d91736736 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-interleaved/main.svelte @@ -0,0 +1,38 @@ + + +{#await f('a', a)}pending{:then v}{v}{/await} +{#await f('b', b)}pending{:then v}{v}|{a}{/await} diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-pending/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-pending/_config.js new file mode 100644 index 0000000000..f5bfea5c60 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-pending/_config.js @@ -0,0 +1,136 @@ +import { fork, tick } from 'svelte'; +import { test } from '../../test'; + +// The pending state of a fork's {#await} promise, and its result, must not show in the real world unless +// the fork is committed. Real updates must keep working while a fork is alive, and a promise of the real +// world must still resolve in the real world when a fork re-runs the block +export default test({ + mode: ['client'], + async test({ assert, target, instance }) { + await tick(); + assert.htmlEqual(target.innerHTML, 'pending'); + instance.settle(0); + await tick(); + assert.htmlEqual(target.innerHTML, '0'); + + // pending and resolved in the fork, then discarded + let f = fork(() => instance.set({ b: 1 })); + await tick(); + assert.htmlEqual(target.innerHTML, '0'); + instance.settle(1); + await tick(); + assert.htmlEqual(target.innerHTML, '0'); + f.discard(); + await tick(); + assert.htmlEqual(target.innerHTML, '0'); + + // rejected in the fork, then discarded + f = fork(() => instance.set({ b: -1 })); + instance.settle(-1); + await tick(); + assert.htmlEqual(target.innerHTML, '0'); + f.discard(); + await tick(); + assert.htmlEqual(target.innerHTML, '0'); + + // For comparison, a real update: pending, then resolved + instance.set({ b: 2 }); + await tick(); + assert.htmlEqual(target.innerHTML, 'pending'); + instance.settle(2); + await tick(); + assert.htmlEqual(target.innerHTML, '2'); + + // committed while pending: shows the pending state, just like a real update would + f = fork(() => instance.set({ b: 3 })); + await tick(); + assert.htmlEqual(target.innerHTML, '2'); + await f.commit(); + assert.htmlEqual(target.innerHTML, 'pending'); + instance.settle(3); + await tick(); + assert.htmlEqual(target.innerHTML, '3'); + + // a real update while the fork is pending (a + b: real 4 + 3, fork 4 + 10) + f = fork(() => instance.set({ b: 10 })); + await tick(); + instance.set({ a: 4 }); + await tick(); + assert.htmlEqual(target.innerHTML, 'pending'); + instance.settle(14); + await tick(); + assert.htmlEqual(target.innerHTML, 'pending'); + instance.settle(7); + await tick(); + assert.htmlEqual(target.innerHTML, '7'); + instance.settle(10); // the fork's obsolete promise (from before it saw a = 4) + await tick(); + assert.htmlEqual(target.innerHTML, '7'); + await f.commit(); + assert.htmlEqual(target.innerHTML, '14'); + + // a real update while the fork is pending, then discarded (real 5 + 10, fork 5 + 20) + f = fork(() => instance.set({ b: 20 })); + await tick(); + instance.set({ a: 5 }); + await tick(); + assert.htmlEqual(target.innerHTML, 'pending'); + instance.settle(15); + instance.settle(25); + await tick(); + assert.htmlEqual(target.innerHTML, '15'); + f.discard(); + await tick(); + assert.htmlEqual(target.innerHTML, '15'); + instance.settle(24); + + // a real promise that is still pending when a fork re-runs the block must resolve in the real world + instance.set({ a: 6 }); + await tick(); + assert.htmlEqual(target.innerHTML, 'pending'); + f = fork(() => instance.set({ b: 30 })); + await tick(); + instance.settle(16); + await tick(); + assert.htmlEqual(target.innerHTML, '16'); + f.discard(); + instance.settle(36); + await tick(); + assert.htmlEqual(target.innerHTML, '16'); + + // ...also when the fork's promise resolves first, and the fork is committed afterwards + instance.set({ a: 7 }); + await tick(); + f = fork(() => instance.set({ b: 40 })); + instance.settle(47); + await tick(); + assert.htmlEqual(target.innerHTML, 'pending'); + instance.settle(17); + await tick(); + assert.htmlEqual(target.innerHTML, '17'); + await f.commit(); + assert.htmlEqual(target.innerHTML, '47'); + + // ...but once the fork is committed, the real world's older promise is obsolete + instance.set({ a: 8 }); + await tick(); + f = fork(() => instance.set({ b: 50 })); + await tick(); + await f.commit(); + assert.htmlEqual(target.innerHTML, 'pending'); + instance.settle(48); + await tick(); + assert.htmlEqual(target.innerHTML, 'pending'); + instance.settle(58); + await tick(); + assert.htmlEqual(target.innerHTML, '58'); + + // rejected in the fork, then committed + f = fork(() => instance.set({ b: -60 })); + instance.settle(-52); + await tick(); + assert.htmlEqual(target.innerHTML, '58'); + await f.commit(); + assert.htmlEqual(target.innerHTML, '-52'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-pending/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-pending/main.svelte new file mode 100644 index 0000000000..bd4dad240d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-pending/main.svelte @@ -0,0 +1,27 @@ + + +{#await f(a + b)}pending{:then v}{v}{:catch e}{e}{/await} diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-real-pending/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-real-pending/_config.js new file mode 100644 index 0000000000..5ea6661bf9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-real-pending/_config.js @@ -0,0 +1,23 @@ +import { fork, tick } from 'svelte'; +import { test } from '../../test'; + +// A fork re-running an {#await} block must not make the real world's pending promise obsolete +export default test({ + mode: ['client'], + async test({ assert, target, instance }) { + await tick(); + assert.htmlEqual(target.innerHTML, 'pending'); + + const f = fork(() => instance.set_b(1)); + await tick(); + + instance.settle(0); + await tick(); + assert.htmlEqual(target.innerHTML, '0'); + + f.discard(); + instance.settle(1); + await tick(); + assert.htmlEqual(target.innerHTML, '0'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-real-pending/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-real-pending/main.svelte new file mode 100644 index 0000000000..0aa523d5de --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-real-pending/main.svelte @@ -0,0 +1,25 @@ + + +{#await f(b)}pending{:then v}{v}{/await} diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-static/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-static/_config.js new file mode 100644 index 0000000000..68f3110be1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-static/_config.js @@ -0,0 +1,25 @@ +import { fork, tick } from 'svelte'; +import { test } from '../../test'; + +// An {#await} block without dependencies can't re-run, so it is the same in every batch: if it's +// created in a fork, a real batch that adopts the fork's branch must see its states as well +export default test({ + mode: ['client'], + async test({ assert, target, instance }) { + const f = fork(() => instance.set({ show: true, x: 1 })); + await tick(); + assert.htmlEqual(target.innerHTML, '

0

'); + + instance.set({ show: true }); + await tick(); + assert.htmlEqual(target.innerHTML, 'pending

0

'); + + instance.settle(5); + await tick(); + assert.htmlEqual(target.innerHTML, '5

0

'); + + f.discard(); + await tick(); + assert.htmlEqual(target.innerHTML, '5

0

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-static/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-static/main.svelte new file mode 100644 index 0000000000..727c7ebaed --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block-static/main.svelte @@ -0,0 +1,19 @@ + + +{#if show}{#await promise}pending{:then v}{v}{/await}{/if}

{x}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-await-block/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block/_config.js new file mode 100644 index 0000000000..9e11f52db5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block/_config.js @@ -0,0 +1,89 @@ +import { fork, tick } from 'svelte'; +import { test } from '../../test'; + +// A fork's {#await} results (then and catch) must not end up in the real world +// unless the fork is committed +export default test({ + mode: ['client'], + async test({ assert, target, instance }) { + instance.resolve_all(); + await tick(); + assert.htmlEqual(target.innerHTML, '0'); + + // resolved, then discarded + let f = fork(() => instance.set_b(2)); + instance.resolve_all(); + await tick(); + assert.htmlEqual(target.innerHTML, '0'); + f.discard(); + await tick(); + assert.htmlEqual(target.innerHTML, '0'); + assert.equal(instance.pending(), 0); // no refetch in the real world + + // rejected, then discarded + f = fork(() => instance.set_b(-1)); + instance.resolve_all(); + await tick(); + assert.htmlEqual(target.innerHTML, '0'); + f.discard(); + await tick(); + assert.htmlEqual(target.innerHTML, '0'); + + // resolved, then committed + f = fork(() => instance.set_b(3)); + instance.resolve_all(); + await tick(); + assert.htmlEqual(target.innerHTML, '0'); + await f.commit(); + assert.htmlEqual(target.innerHTML, '3'); + assert.equal(instance.pending(), 0); + + // rejected, then committed + f = fork(() => instance.set_b(-2)); + instance.resolve_all(); + await tick(); + assert.htmlEqual(target.innerHTML, '3'); + await f.commit(); + assert.htmlEqual(target.innerHTML, '-2'); + + // For comparison, a real update: without a pending snippet, the previous + // content is removed (a microtask later), and the result shows once it resolves + instance.set_b(4); + await tick(); + assert.htmlEqual(target.innerHTML, ''); + instance.resolve_all(); + await tick(); + assert.htmlEqual(target.innerHTML, '4'); + + // committed while pending (after the pending state was reached in the fork): + // the real world is in the same state as after a real update + f = fork(() => instance.set_b(5)); + await tick(); + assert.htmlEqual(target.innerHTML, '4'); + await f.commit(); + assert.htmlEqual(target.innerHTML, ''); + instance.resolve_all(); + await tick(); + assert.htmlEqual(target.innerHTML, '5'); + assert.equal(instance.pending(), 0); + + // committed synchronously (`fork` flushes synchronously, which reaches the pending state) + f = fork(() => instance.set_b(6)); + assert.htmlEqual(target.innerHTML, '5'); + f.commit(); + assert.htmlEqual(target.innerHTML, ''); + instance.resolve_all(); + await tick(); + assert.htmlEqual(target.innerHTML, '6'); + assert.equal(instance.pending(), 0); + + // discarded while pending, resolved afterwards + f = fork(() => instance.set_b(7)); + await tick(); + f.discard(); + instance.resolve_all(); + await tick(); + assert.htmlEqual(target.innerHTML, '6'); + assert.equal(instance.pending(), 0); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-await-block/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block/main.svelte new file mode 100644 index 0000000000..b1607eb2ed --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-await-block/main.svelte @@ -0,0 +1,24 @@ + + +{#await f(b) then v}{v}{:catch e}{e}{/await}