From d175cc499d5f44db2d53ecd9316306dc03992e06 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 20 Mar 2026 12:06:10 +0100 Subject: [PATCH] fix --- .../internal/client/reactivity/deriveds.js | 31 +++++-- .../async-overlap-multiple-5/_config.js | 87 +++++++++++++++++++ .../async-overlap-multiple-5/main.svelte | 23 +++++ .../async-overlap-multiple-6/_config.js | 76 ++++++++++++++++ .../async-overlap-multiple-6/main.svelte | 23 +++++ .../async-overlap-multiple-7/_config.js | 87 +++++++++++++++++++ .../async-overlap-multiple-7/main.svelte | 23 +++++ 7 files changed, 342 insertions(+), 8 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/main.svelte diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 07e7613e07..d86cef1237 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -44,6 +44,7 @@ import { batch_values, current_batch } from './batch.js'; import { increment_pending, unset_context } from './async.js'; import { deferred, includes, noop } from '../../shared/utils.js'; import { set_signal_status, update_derived_status } from './status.js'; +import { queue_micro_task } from '../dom/task.js'; /** * This allows us to track 'reactivity loss' that occurs when signals @@ -127,7 +128,7 @@ export function async_derived(fn, label, location) { /** @type {Map>>} */ var deferreds = new Map(); - async_effect(() => { + var async_e = async_effect(() => { if (DEV) { reactivity_loss_tracker = { effect: /** @type {Effect} */ (active_effect), @@ -160,7 +161,8 @@ export function async_derived(fn, label, location) { if (should_suspend) { // we only increment the batch's pending state for updates, not creation, otherwise // we will decrement to zero before the work that depends on this promise (e.g. a - // template effect) has initialized, causing the batch to resolve prematurely + // template effect) has initialized, causing the batch to resolve prematurely. + // Also see test async-overlap-multiple-6 if ((effect.f & REACTION_RAN) !== 0) { var decrement_pending = increment_pending(); } @@ -192,6 +194,21 @@ export function async_derived(fn, label, location) { batch.register_async_derived(d.reject); deferreds.set(batch, d); + + // Check if a later batch started work earlier than an earlier one. + // This could happen when two batches write to the same async derived + // but the earlier one is only writing to it after going through another + // async derived, while the later one is writing to it immediately. + // In that case, to ensure the order is preserved and the later batch + // is invoked with the right values, we have to restart the later batch's async derived. + for (const b of deferreds.keys()) { + if (b === batch) break; + if (b.id > batch.id) { + set_signal_status(async_e, DIRTY); + b.schedule(async_e); + queue_micro_task(() => b.flush()); + } + } } /** @@ -212,12 +229,10 @@ export function async_derived(fn, label, location) { // All prior async derived runs are now stale, but we have to // wait for the corresponding batches to resolve before proceeding. - // We sort because batch order is important, not in which order the - // corresponding async work started. - for (const [b, d] of [...deferreds].sort(([a], [b]) => a.id - b.id)) { - if (b === batch) break; - waits.push(b.settled()); - b.reject_async(d.reject); + for (const [other_batch, other_d] of deferreds) { + if (other_batch === batch) break; + waits.push(other_batch.settled()); + other_batch.reject_async(other_d.reject); } if (waits.length > 0) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/_config.js new file mode 100644 index 0000000000..f07c86dc0c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/_config.js @@ -0,0 +1,87 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [a, c, shift, pop] = target.querySelectorAll('button'); + + a.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + c.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 1 | b 2 | c 0 | d 2 + + + + + ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 1 | b 2 | c 1 | d 3 + + + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/main.svelte new file mode 100644 index 0000000000..cf028718c2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/main.svelte @@ -0,0 +1,23 @@ + + +a {a} | b {b} | c {c} | d {d} + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/_config.js new file mode 100644 index 0000000000..27152889a2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/_config.js @@ -0,0 +1,76 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [a, c, shift, pop] = target.querySelectorAll('button'); + + a.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + c.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + // Although the second batch is eventually connected to the first one, we can't see that + // at this point yet and so the second one flushes right away. + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 1 | d 1 + + + + + ` + ); + + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 1 | d 1 + + + + + ` + ); + + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 1 | b 2 | c 1 | d 3 + + + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/main.svelte new file mode 100644 index 0000000000..cf028718c2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/main.svelte @@ -0,0 +1,23 @@ + + +a {a} | b {b} | c {c} | d {d} + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/_config.js new file mode 100644 index 0000000000..62ba0e3f46 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/_config.js @@ -0,0 +1,87 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [a, c, shift, pop] = target.querySelectorAll('button'); + + a.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + c.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + shift.click(); // schedules second step of first batch and schedules rerun of second batch + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + pop.click(); // second batch resolves but knows it needs to wait on first batch + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + shift.click(); // obsolete second batch promise (already rejected) + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + shift.click(); // first batch resolves, with it second can now resolve as well + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 1 | b 2 | c 1 | d 3 + + + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/main.svelte new file mode 100644 index 0000000000..cf028718c2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/main.svelte @@ -0,0 +1,23 @@ + + +a {a} | b {b} | c {c} | d {d} + + + + \ No newline at end of file