From e286d308595a315f5e8ea50d1654a484e6d727a1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 25 Sep 2025 21:52:03 -0400 Subject: [PATCH 01/19] WIP --- .../src/internal/client/reactivity/batch.js | 146 +++++++++++++----- .../internal/client/reactivity/deriveds.js | 71 ++++----- 2 files changed, 138 insertions(+), 79 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 8cf0e4bd9db1..e1f729504dde 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -1,4 +1,4 @@ -/** @import { Derived, Effect, Source } from '#client' */ +/** @import { Derived, Effect, Source, Value } from '#client' */ import { BLOCK_EFFECT, BRANCH_EFFECT, @@ -10,10 +10,11 @@ import { INERT, RENDER_EFFECT, ROOT_EFFECT, - MAYBE_DIRTY + MAYBE_DIRTY, + DERIVED } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; -import { deferred, define_property } from '../../shared/utils.js'; +import { deferred, define_property, noop } from '../../shared/utils.js'; import { active_effect, is_dirty, @@ -165,32 +166,7 @@ export class Batch { previous_batch = null; - /** @type {Map | null} */ - var current_values = null; - - // if there are multiple batches, we are 'time travelling' — - // we need to undo the changes belonging to any batch - // other than the current one - if (async_mode_flag && batches.size > 1) { - current_values = new Map(); - batch_deriveds = new Map(); - - for (const [source, current] of this.current) { - current_values.set(source, { v: source.v, wv: source.wv }); - source.v = current; - } - - for (const batch of batches) { - if (batch === this) continue; - - for (const [source, previous] of batch.#previous) { - if (!current_values.has(source)) { - current_values.set(source, { v: source.v, wv: source.wv }); - source.v = previous; - } - } - } - } + var revert = Batch.apply(this); for (const root of root_effects) { this.#traverse_effect_tree(root); @@ -221,29 +197,20 @@ export class Batch { this.#defer_effects(this.#render_effects); this.#defer_effects(this.#effects); this.#defer_effects(this.#block_effects); - } - if (current_values) { - for (const [source, { v, wv }] of current_values) { - // reset the source to the current value (unless - // it got a newer value as a result of effects running) - if (source.wv <= wv) { - source.v = v; - } + for (const effect of this.#async_effects) { + update_effect(effect); } - batch_deriveds = null; + this.#async_effects = []; } - for (const effect of this.#async_effects) { - update_effect(effect); - } + revert(); for (const effect of this.#boundary_async_effects) { update_effect(effect); } - this.#async_effects = []; this.#boundary_async_effects = []; } @@ -381,6 +348,51 @@ export class Batch { } this.#callbacks.clear(); + + /** + * @param {Value} value + * @param {Set} effects + */ + function get_async_effects(value, effects) { + if (value.reactions !== null) { + for (const reaction of value.reactions) { + const flags = reaction.f; + + if ((flags & DERIVED) !== 0) { + get_async_effects(/** @type {Derived} */ (reaction), effects); + } else if ((flags & ASYNC) !== 0) { + effects.add(/** @type {Effect} */ (reaction)); + } + } + } + } + + if (batches.size > 1) { + const effects = new Set(); + + for (const source of this.current.keys()) { + // TODO do we also need block effects? + get_async_effects(source, effects); + } + + this.#previous.clear(); + + for (const batch of batches) { + if (batch === this) { + continue; + } + + current_batch = batch; + const revert = Batch.apply(batch); + for (const e of effects) { + update_effect(e); + } + revert(); + } + + current_batch = null; + } + batches.delete(this); } @@ -444,6 +456,56 @@ export class Batch { static enqueue(task) { queue_micro_task(task); } + + /** + * @param {Batch} current_batch + */ + static apply(current_batch) { + if (!async_mode_flag || batches.size === 1) { + return noop; + } + + /** @type {Map | null} */ + var current_values = null; + + // if there are multiple batches, we are 'time travelling' — + // we need to undo the changes belonging to any batch + // other than the current one + if (async_mode_flag && batches.size > 1) { + current_values = new Map(); + batch_deriveds = new Map(); + + for (const [source, current] of current_batch.current) { + current_values.set(source, { v: source.v, wv: source.wv }); + source.v = current; + } + + for (const batch of batches) { + if (batch === current_batch) continue; + + for (const [source, previous] of batch.#previous) { + if (!current_values.has(source)) { + current_values.set(source, { v: source.v, wv: source.wv }); + source.v = previous; + } + } + } + } + + return () => { + if (current_values) { + for (const [source, { v, wv }] of current_values) { + // reset the source to the current value (unless + // it got a newer value as a result of effects running) + if (source.wv <= wv) { + source.v = v; + } + } + + batch_deriveds = null; + } + }; + } } /** diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 11405a8e664f..a08a10397dc0 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -109,31 +109,26 @@ export function async_derived(fn, location) { var promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined)); var signal = source(/** @type {V} */ (UNINITIALIZED)); - /** @type {Promise | null} */ - var prev = null; - // only suspend in async deriveds created on initialisation var should_suspend = !active_reaction; + /** @type {Map>} */ + var promises = new Map(); + async_effect(() => { if (DEV) current_async_effect = active_effect; + /** @type {Promise} */ + var current; + try { - var p = fn(); - // Make sure to always access the then property to read any signals - // it might access, so that we track them as dependencies. - if (prev) Promise.resolve(p).catch(() => {}); // avoid unhandled rejection + current = promise = Promise.resolve(fn()); } catch (error) { - p = Promise.reject(error); + current = promise = Promise.reject(error); } if (DEV) current_async_effect = null; - var r = () => p; - promise = prev?.then(r, r) ?? Promise.resolve(p); - - prev = promise; - var batch = /** @type {Batch} */ (current_batch); var pending = boundary.is_pending(); @@ -142,40 +137,42 @@ export function async_derived(fn, location) { if (!pending) batch.increment(); } + promises.set(batch, promise); + /** * @param {any} value * @param {unknown} error */ const handler = (value, error = undefined) => { - prev = null; - current_async_effect = null; if (!pending) batch.activate(); - if (error) { - if (error !== STALE_REACTION) { - signal.f |= ERROR_VALUE; + if (current === promises.get(batch)) { + if (error) { + if (error !== STALE_REACTION) { + signal.f |= ERROR_VALUE; - // @ts-expect-error the error is the wrong type, but we don't care - internal_set(signal, error); - } - } else { - if ((signal.f & ERROR_VALUE) !== 0) { - signal.f ^= ERROR_VALUE; - } - - internal_set(signal, value); - - if (DEV && location !== undefined) { - recent_async_deriveds.add(signal); - - setTimeout(() => { - if (recent_async_deriveds.has(signal)) { - w.await_waterfall(/** @type {string} */ (signal.label), location); - recent_async_deriveds.delete(signal); - } - }); + // @ts-expect-error the error is the wrong type, but we don't care + internal_set(signal, error); + } + } else { + if ((signal.f & ERROR_VALUE) !== 0) { + signal.f ^= ERROR_VALUE; + } + + internal_set(signal, value); + + if (DEV && location !== undefined) { + recent_async_deriveds.add(signal); + + setTimeout(() => { + if (recent_async_deriveds.has(signal)) { + w.await_waterfall(/** @type {string} */ (signal.label), location); + recent_async_deriveds.delete(signal); + } + }); + } } } From fe693befb9b151bf9c242381cbb60f9a6c37c855 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 26 Sep 2025 15:16:39 -0400 Subject: [PATCH 02/19] update test --- .../samples/async-linear-order-same-derived/_config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js index 5e522ebdb536..627566a3b12e 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js @@ -20,7 +20,7 @@ export default test({ resolve2.click(); await tick(); - assert.htmlEqual(p.innerHTML, '1 + 2 = 3'); + assert.htmlEqual(p.innerHTML, '1 + 3 = 4'); resolve1.click(); await tick(); From cb6e8244ae84fbbbed70bb37728803ffda67bbab Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 26 Sep 2025 15:17:22 -0400 Subject: [PATCH 03/19] tweak --- .../src/internal/client/reactivity/batch.js | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index e1f729504dde..726b82410997 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -349,27 +349,27 @@ export class Batch { this.#callbacks.clear(); - /** - * @param {Value} value - * @param {Set} effects - */ - function get_async_effects(value, effects) { - if (value.reactions !== null) { - for (const reaction of value.reactions) { - const flags = reaction.f; - - if ((flags & DERIVED) !== 0) { - get_async_effects(/** @type {Derived} */ (reaction), effects); - } else if ((flags & ASYNC) !== 0) { - effects.add(/** @type {Effect} */ (reaction)); - } - } - } - } - if (batches.size > 1) { const effects = new Set(); + /** + * @param {Value} value + * @param {Set} effects + */ + const get_async_effects = (value, effects) => { + if (value.reactions !== null) { + for (const reaction of value.reactions) { + const flags = reaction.f; + + if ((flags & DERIVED) !== 0) { + get_async_effects(/** @type {Derived} */ (reaction), effects); + } else if ((flags & ASYNC) !== 0) { + effects.add(/** @type {Effect} */ (reaction)); + } + } + } + }; + for (const source of this.current.keys()) { // TODO do we also need block effects? get_async_effects(source, effects); From ecdfd5b40ccd41894263e03ab806772c9faabe3f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 26 Sep 2025 15:42:38 -0400 Subject: [PATCH 04/19] fix --- .../src/internal/client/reactivity/batch.js | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 726b82410997..058084b14b7f 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -350,8 +350,6 @@ export class Batch { this.#callbacks.clear(); if (batches.size > 1) { - const effects = new Set(); - /** * @param {Value} value * @param {Set} effects @@ -370,24 +368,32 @@ export class Batch { } }; - for (const source of this.current.keys()) { - // TODO do we also need block effects? - get_async_effects(source, effects); - } - this.#previous.clear(); + let is_earlier = true; + for (const batch of batches) { if (batch === this) { + is_earlier = true; continue; } - current_batch = batch; - const revert = Batch.apply(batch); - for (const e of effects) { - update_effect(e); + const effects = new Set(); + + for (const source of this.current.keys()) { + if (is_earlier && batch.current.has(source)) continue; + // TODO do we also need block effects? + get_async_effects(source, effects); + } + + if (effects.size > 0) { + current_batch = batch; + const revert = Batch.apply(batch); + for (const e of effects) { + update_effect(e); + } + revert(); } - revert(); } current_batch = null; From 8f85d7ddcb51df34319be65cf136f1e14b2218b8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 26 Sep 2025 18:31:13 -0400 Subject: [PATCH 05/19] fix --- packages/svelte/src/internal/client/reactivity/batch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 058084b14b7f..100cf2afa532 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -186,7 +186,7 @@ export class Batch { // If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with // newly updated sources, which could lead to infinite loops when effects run over and over again. - previous_batch = current_batch; + previous_batch = this; current_batch = null; flush_queued_effects(render_effects); From 8cc385aee8e6cecd7b567d7f3b82f6f0200a6fac Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Sep 2025 13:30:38 -0400 Subject: [PATCH 06/19] tweak --- .../src/internal/client/reactivity/batch.js | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 100cf2afa532..3962ee411882 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -471,45 +471,40 @@ export class Batch { return noop; } - /** @type {Map | null} */ - var current_values = null; - // if there are multiple batches, we are 'time travelling' — // we need to undo the changes belonging to any batch // other than the current one - if (async_mode_flag && batches.size > 1) { - current_values = new Map(); - batch_deriveds = new Map(); - for (const [source, current] of current_batch.current) { - current_values.set(source, { v: source.v, wv: source.wv }); - source.v = current; - } + /** @type {Map} */ + var current_values = new Map(); + batch_deriveds = new Map(); - for (const batch of batches) { - if (batch === current_batch) continue; + for (const [source, current] of current_batch.current) { + current_values.set(source, { v: source.v, wv: source.wv }); + source.v = current; + } - for (const [source, previous] of batch.#previous) { - if (!current_values.has(source)) { - current_values.set(source, { v: source.v, wv: source.wv }); - source.v = previous; - } + for (const batch of batches) { + if (batch === current_batch) continue; + + for (const [source, previous] of batch.#previous) { + if (!current_values.has(source)) { + current_values.set(source, { v: source.v, wv: source.wv }); + source.v = previous; } } } return () => { - if (current_values) { - for (const [source, { v, wv }] of current_values) { - // reset the source to the current value (unless - // it got a newer value as a result of effects running) - if (source.wv <= wv) { - source.v = v; - } + for (const [source, { v, wv }] of current_values) { + // reset the source to the current value (unless + // it got a newer value as a result of effects running) + if (source.wv <= wv) { + source.v = v; } - - batch_deriveds = null; } + + batch_deriveds = null; }; } } From bb1c91fcd723ae6b61179ceec2799ce8b6df9e88 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Sep 2025 13:34:36 -0400 Subject: [PATCH 07/19] simplify --- .../src/internal/client/reactivity/batch.js | 28 ++++--------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 3962ee411882..0f165cdff392 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -104,16 +104,8 @@ export class Batch { #neutered = false; /** - * Async effects (created inside `async_derived`) encountered during processing. - * These run after the rest of the batch has updated, since they should - * always have the latest values - * @type {Effect[]} - */ - #async_effects = []; - - /** - * The same as `#async_effects`, but for effects inside a newly-created - * `` — these do not prevent the batch from committing + * Async effects inside a newly-created `` + * — these do not prevent the batch from committing * @type {Effect[]} */ #boundary_async_effects = []; @@ -174,7 +166,7 @@ export class Batch { // if we didn't start any new async work, and no async work // is outstanding from a previous flush, commit - if (this.#async_effects.length === 0 && this.#pending === 0) { + if (this.#pending === 0) { this.#commit(); var render_effects = this.#render_effects; @@ -197,12 +189,6 @@ export class Batch { this.#defer_effects(this.#render_effects); this.#defer_effects(this.#effects); this.#defer_effects(this.#block_effects); - - for (const effect of this.#async_effects) { - update_effect(effect); - } - - this.#async_effects = []; } revert(); @@ -239,12 +225,8 @@ export class Batch { } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { this.#render_effects.push(effect); } else if ((flags & CLEAN) === 0) { - if ((flags & ASYNC) !== 0) { - var effects = effect.b?.is_pending() - ? this.#boundary_async_effects - : this.#async_effects; - - effects.push(effect); + if ((flags & ASYNC) !== 0 && effect.b?.is_pending()) { + this.#boundary_async_effects.push(effect); } else if (is_dirty(effect)) { if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect); update_effect(effect); From 93a5f54b1b7546e83157612d7987f7933ea9b3f8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Sep 2025 14:19:17 -0400 Subject: [PATCH 08/19] tidy up --- .../src/internal/client/reactivity/batch.js | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 0f165cdff392..40f89c152c64 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -332,24 +332,6 @@ export class Batch { this.#callbacks.clear(); if (batches.size > 1) { - /** - * @param {Value} value - * @param {Set} effects - */ - const get_async_effects = (value, effects) => { - if (value.reactions !== null) { - for (const reaction of value.reactions) { - const flags = reaction.f; - - if ((flags & DERIVED) !== 0) { - get_async_effects(/** @type {Derived} */ (reaction), effects); - } else if ((flags & ASYNC) !== 0) { - effects.add(/** @type {Effect} */ (reaction)); - } - } - } - }; - this.#previous.clear(); let is_earlier = true; @@ -360,20 +342,20 @@ export class Batch { continue; } - const effects = new Set(); - for (const source of this.current.keys()) { if (is_earlier && batch.current.has(source)) continue; - // TODO do we also need block effects? - get_async_effects(source, effects); + mark_effects(source); } - if (effects.size > 0) { + if (queued_root_effects.length > 0) { current_batch = batch; const revert = Batch.apply(batch); - for (const e of effects) { - update_effect(e); + + for (const root of queued_root_effects) { + batch.#traverse_effect_tree(root); } + + queued_root_effects = []; revert(); } } @@ -660,6 +642,26 @@ function flush_queued_effects(effects) { eager_block_effects = null; } +/** + * This is similar to `mark_reactions`, but it only marks async/block effects + * so that these can re-run after another batch has been committed + * @param {Value} value + */ +function mark_effects(value) { + if (value.reactions !== null) { + for (const reaction of value.reactions) { + const flags = reaction.f; + + if ((flags & DERIVED) !== 0) { + mark_effects(/** @type {Derived} */ (reaction)); + } else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0) { + set_signal_status(reaction, DIRTY); + schedule_effect(/** @type {Effect} */ (reaction)); + } + } + } +} + /** * @param {Effect} signal * @returns {void} From dfe85ba05fc010d7b51953f5367ec4252af77d72 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Sep 2025 15:06:04 -0400 Subject: [PATCH 09/19] changeset --- .changeset/lemon-cars-count.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lemon-cars-count.md diff --git a/.changeset/lemon-cars-count.md b/.changeset/lemon-cars-count.md new file mode 100644 index 000000000000..5724e4846059 --- /dev/null +++ b/.changeset/lemon-cars-count.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: rebase pending batches when other batches are committed From 357c0770c9869c6769e955bbc010980343561960 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Sep 2025 16:31:49 -0400 Subject: [PATCH 10/19] abort stale promises on a given batch --- .../internal/client/reactivity/deriveds.js | 67 ++++++++++--------- .../samples/async-derived-module/_config.js | 11 --- .../_config.js | 10 ++- .../main.svelte | 21 +++--- 4 files changed, 50 insertions(+), 59 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index a08a10397dc0..d22be75ea116 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -35,6 +35,7 @@ import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; import { batch_deriveds, current_batch } from './batch.js'; import { unset_context } from './async.js'; +import { deferred } from '../../shared/utils.js'; /** @type {Effect | null} */ export let current_async_effect = null; @@ -115,16 +116,19 @@ export function async_derived(fn, location) { /** @type {Map>} */ var promises = new Map(); + /** @type {Map>} */ + var deferreds = new Map(); + async_effect(() => { if (DEV) current_async_effect = active_effect; - /** @type {Promise} */ - var current; + var d = deferred(); + promise = d.promise; try { - current = promise = Promise.resolve(fn()); + Promise.resolve(fn()).then(d.resolve, d.reject); } catch (error) { - current = promise = Promise.reject(error); + d.reject(error); } if (DEV) current_async_effect = null; @@ -134,7 +138,12 @@ export function async_derived(fn, location) { if (should_suspend) { boundary.update_pending_count(1); - if (!pending) batch.increment(); + if (!pending) { + batch.increment(); + + deferreds.get(batch)?.reject(STALE_REACTION); + deferreds.set(batch, d); + } } promises.set(batch, promise); @@ -148,31 +157,29 @@ export function async_derived(fn, location) { if (!pending) batch.activate(); - if (current === promises.get(batch)) { - if (error) { - if (error !== STALE_REACTION) { - signal.f |= ERROR_VALUE; + if (error) { + if (error !== STALE_REACTION) { + signal.f |= ERROR_VALUE; - // @ts-expect-error the error is the wrong type, but we don't care - internal_set(signal, error); - } - } else { - if ((signal.f & ERROR_VALUE) !== 0) { - signal.f ^= ERROR_VALUE; - } - - internal_set(signal, value); - - if (DEV && location !== undefined) { - recent_async_deriveds.add(signal); - - setTimeout(() => { - if (recent_async_deriveds.has(signal)) { - w.await_waterfall(/** @type {string} */ (signal.label), location); - recent_async_deriveds.delete(signal); - } - }); - } + // @ts-expect-error the error is the wrong type, but we don't care + internal_set(signal, error); + } + } else { + if ((signal.f & ERROR_VALUE) !== 0) { + signal.f ^= ERROR_VALUE; + } + + internal_set(signal, value); + + if (DEV && location !== undefined) { + recent_async_deriveds.add(signal); + + setTimeout(() => { + if (recent_async_deriveds.has(signal)) { + w.await_waterfall(/** @type {string} */ (signal.label), location); + recent_async_deriveds.delete(signal); + } + }); } } @@ -184,7 +191,7 @@ export function async_derived(fn, location) { unset_context(); }; - promise.then(handler, (e) => handler(null, e || 'unknown')); + d.promise.then(handler, (e) => handler(null, e || 'unknown')); if (batch) { return () => { diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js index f7d1d28fdece..318f88bcc9a6 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js @@ -14,17 +14,6 @@ export default test({ const [reset, a, b, increment] = target.querySelectorAll('button'); a.click(); - - // TODO why is this necessary? why isn't `await tick()` enough? - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - flushSync(); await tick(); assert.htmlEqual( target.innerHTML, diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js index 627566a3b12e..cc7b2756fa37 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js @@ -3,26 +3,24 @@ import { test } from '../../test'; export default test({ async test({ assert, target }) { - const [a, b, reset1, reset2, resolve1, resolve2] = target.querySelectorAll('button'); + const [a, b, shift, pop] = target.querySelectorAll('button'); - resolve1.click(); + shift.click(); await tick(); const p = /** @type {HTMLElement} */ (target.querySelector('#test')); assert.htmlEqual(p.innerHTML, '1 + 2 = 3'); - flushSync(() => reset1.click()); flushSync(() => a.click()); - flushSync(() => reset2.click()); flushSync(() => b.click()); - resolve2.click(); + pop.click(); await tick(); assert.htmlEqual(p.innerHTML, '1 + 3 = 4'); - resolve1.click(); + pop.click(); await tick(); assert.htmlEqual(p.innerHTML, '2 + 3 = 5'); diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/main.svelte index cc82db0d7559..48ff06fecee5 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/main.svelte @@ -1,14 +1,14 @@ @@ -16,14 +16,11 @@ - - - - - + + -

{a} + {b} = {await add(a, b)}

+

{a} + {b} = {await push(a, b)}

{#snippet pending()}

loading...

From cbb4b94d4a16d00f278b632af675035c817e07eb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Sep 2025 16:42:45 -0400 Subject: [PATCH 11/19] tidy up --- .../src/internal/client/reactivity/batch.js | 16 ++-------------- .../src/internal/client/reactivity/deriveds.js | 14 ++++++++------ 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 40f89c152c64..c70bda57c5dc 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -97,12 +97,6 @@ export class Batch { */ #deferred = null; - /** - * True if an async effect inside this batch resolved and - * its parent branch was already deleted - */ - #neutered = false; - /** * Async effects inside a newly-created `` * — these do not prevent the batch from committing @@ -299,10 +293,6 @@ export class Batch { } } - neuter() { - this.#neutered = true; - } - flush() { if (queued_root_effects.length > 0) { this.activate(); @@ -323,10 +313,8 @@ export class Batch { * Append and remove branches to/from the DOM */ #commit() { - if (!this.#neutered) { - for (const fn of this.#callbacks) { - fn(); - } + for (const fn of this.#callbacks) { + fn(); } this.#callbacks.clear(); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index d22be75ea116..08fe5eadd7cf 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -26,7 +26,7 @@ import { import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; import * as w from '../warnings.js'; -import { async_effect, destroy_effect } from './effects.js'; +import { async_effect, destroy_effect, teardown } from './effects.js'; import { inspect_effects, internal_set, set_inspect_effects, source } from './sources.js'; import { get_stack } from '../dev/tracing.js'; import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js'; @@ -192,12 +192,14 @@ export function async_derived(fn, location) { }; d.promise.then(handler, (e) => handler(null, e || 'unknown')); + }); - if (batch) { - return () => { - queueMicrotask(() => batch.neuter()); - }; - } + teardown(() => { + queueMicrotask(() => { + for (const d of deferreds.values()) { + d.reject(STALE_REACTION); + } + }); }); if (DEV) { From f8eddac1fdc5c6e6bb426728703c459d246bc785 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Sep 2025 16:46:55 -0400 Subject: [PATCH 12/19] add test --- .../runtime-runes/samples/async-if/_config.js | 30 ++++++++++++++----- .../samples/async-if/main.svelte | 21 +++++++++---- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-if/_config.js index 3cd67952c3cb..ef119d601d0b 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-if/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-if/_config.js @@ -2,30 +2,46 @@ import { tick } from 'svelte'; import { test } from '../../test'; export default test({ - html: `

pending

`, + html: `

pending

`, async test({ assert, target }) { - const [reset, t, f] = target.querySelectorAll('button'); + const [shift, t, f] = target.querySelectorAll('button'); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

yes

' + ); + + f.click(); + await tick(); t.click(); await tick(); + + f.click(); + await tick(); + + shift.click(); + await tick(); assert.htmlEqual( target.innerHTML, - '

yes

' + '

no

' ); - reset.click(); + shift.click(); await tick(); assert.htmlEqual( target.innerHTML, - '

yes

' + '

yes

' ); - f.click(); + shift.click(); await tick(); assert.htmlEqual( target.innerHTML, - '

no

' + '

no

' ); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-if/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-if/main.svelte index 21a4cbef97f2..ed708354a454 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-if/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-if/main.svelte @@ -1,13 +1,24 @@ - - - + + + - {#if await deferred.promise} + {#if await push(condition)}

yes

{:else}

no

From a43dc2957af2589b58ef0ef6f2bc40ba924f144f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Sep 2025 17:00:08 -0400 Subject: [PATCH 13/19] lint --- packages/svelte/src/internal/client/reactivity/deriveds.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 08fe5eadd7cf..b0e679b8636d 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -116,7 +116,7 @@ export function async_derived(fn, location) { /** @type {Map>} */ var promises = new Map(); - /** @type {Map>} */ + /** @type {Map>} */ var deferreds = new Map(); async_effect(() => { From e6b5adc549d211882df04c25a3531a2d9f5120f2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Sep 2025 17:11:44 -0400 Subject: [PATCH 14/19] add explanatory comment --- packages/svelte/src/internal/client/reactivity/batch.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index c70bda57c5dc..3e772469f463 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -319,6 +319,10 @@ export class Batch { this.#callbacks.clear(); + // If there are other pending batches, they now need to be 'rebased' — + // in other words, we re-run block/async effects with the newly + // committed state, unless the batch in question has a more + // recent value for a given source if (batches.size > 1) { this.#previous.clear(); From afdad8bec081aac09442aff08dd6b9c2fc5895eb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Sep 2025 17:24:48 -0400 Subject: [PATCH 15/19] unused --- packages/svelte/src/internal/client/reactivity/deriveds.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index b0e679b8636d..62d34fe49399 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -113,9 +113,6 @@ export function async_derived(fn, location) { // only suspend in async deriveds created on initialisation var should_suspend = !active_reaction; - /** @type {Map>} */ - var promises = new Map(); - /** @type {Map>} */ var deferreds = new Map(); @@ -146,8 +143,6 @@ export function async_derived(fn, location) { } } - promises.set(batch, promise); - /** * @param {any} value * @param {unknown} error From 99d31b433b4cb3ac22392ffc7bc37662e14f587d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Sep 2025 17:26:04 -0400 Subject: [PATCH 16/19] doesn't look like we need the queueMicrotask here --- .../svelte/src/internal/client/reactivity/deriveds.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 62d34fe49399..6b6e7341c7f1 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -190,11 +190,9 @@ export function async_derived(fn, location) { }); teardown(() => { - queueMicrotask(() => { - for (const d of deferreds.values()) { - d.reject(STALE_REACTION); - } - }); + for (const d of deferreds.values()) { + d.reject(STALE_REACTION); + } }); if (DEV) { From 49e1ab5efa3b412d94de26df0b747f673cfeb006 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Sep 2025 21:07:30 -0400 Subject: [PATCH 17/19] unused --- packages/svelte/src/internal/client/reactivity/batch.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 3e772469f463..bee62e21c270 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -376,9 +376,6 @@ export class Batch { schedule_effect(e); } - this.#render_effects = []; - this.#effects = []; - this.flush(); } else { this.deactivate(); From 3bcbc602c3340abb3ec96004ebadb0ab298c286d Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 30 Sep 2025 11:05:26 +0200 Subject: [PATCH 18/19] lint / reinstate comment --- packages/svelte/src/internal/client/reactivity/deriveds.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 6b6e7341c7f1..5d5976a6c115 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -113,16 +113,19 @@ export function async_derived(fn, location) { // only suspend in async deriveds created on initialisation var should_suspend = !active_reaction; - /** @type {Map>} */ + /** @type {Map>>} */ var deferreds = new Map(); async_effect(() => { if (DEV) current_async_effect = active_effect; + /** @type {ReturnType>} */ var d = deferred(); promise = d.promise; try { + // If this code is changed at some point, make sure to still access the then property + // of fn() to read any signals it might access, so that we track them as dependencies. Promise.resolve(fn()).then(d.resolve, d.reject); } catch (error) { d.reject(error); From 1bdc5185a3587bfd9895092f59c84959fbfc8e5f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 30 Sep 2025 08:45:13 -0400 Subject: [PATCH 19/19] fix --- .../src/internal/client/reactivity/batch.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index bee62e21c270..fb704edb1325 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -330,12 +330,22 @@ export class Batch { for (const batch of batches) { if (batch === this) { - is_earlier = true; + is_earlier = false; continue; } - for (const source of this.current.keys()) { - if (is_earlier && batch.current.has(source)) continue; + for (const [source, value] of this.current) { + if (batch.current.has(source)) { + if (is_earlier) { + // bring the value up to date + batch.current.set(source, value); + } else { + // later batch has more recent value, + // no need to re-run these effects + continue; + } + } + mark_effects(source); }