From bc51a4fae8079b1faadf070161615b093d925428 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 5 May 2026 16:24:24 -0400 Subject: [PATCH] fix: differentiate between stale and obsolete async deriveds --- .../src/internal/client/reactivity/async.js | 4 ++-- .../src/internal/client/reactivity/batch.js | 5 ++--- .../internal/client/reactivity/deriveds.js | 21 ++++++++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 6aea790c36..af9a4e6fb4 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -334,8 +334,8 @@ export function increment_pending() { boundary.update_pending_count(1, batch); batch.increment(blocking, effect); - return (skip = false) => { + return () => { boundary.update_pending_count(-1, batch); - batch.decrement(blocking, effect, skip); + batch.decrement(blocking, effect); }; } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 4239cda04b..f019b75aea 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -642,9 +642,8 @@ export class Batch { /** * @param {boolean} blocking * @param {Effect} effect - * @param {boolean} skip - whether to skip updates (because this is triggered by a stale reaction) */ - decrement(blocking, effect, skip) { + decrement(blocking, effect) { let pending_count = this.#pending.get(effect) ?? 0; if (pending_count === 1) { @@ -663,7 +662,7 @@ export class Batch { } } - if (this.#decrement_queued || skip) return; + if (this.#decrement_queued) return; this.#decrement_queued = true; queue_micro_task(() => { diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index eb934d96ff..9999c31152 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -100,6 +100,8 @@ export function derived(fn) { return signal; } +const OBSOLETE = {}; + /** * @template V * @param {() => V | Promise} fn @@ -180,13 +182,13 @@ export function async_derived(fn, label, location) { } if (/** @type {Boundary} */ (parent.b).is_rendered()) { - deferreds.get(batch)?.reject(STALE_REACTION); + deferreds.get(batch)?.reject(OBSOLETE); deferreds.delete(batch); // delete to ensure correct order in Map iteration below } else { // While the boundary is still showing pending, a new run supersedes all older in-flight runs // for this async expression. Cancel eagerly so resolution cannot commit stale values. for (const d of deferreds.values()) { - d.reject(STALE_REACTION); + d.reject(OBSOLETE); } deferreds.clear(); } @@ -203,15 +205,14 @@ export function async_derived(fn, label, location) { reactivity_loss_tracker = null; } - if (decrement_pending) { - // don't trigger an update if we're only here because - // the promise was superseded before it could resolve - var skip = error === STALE_REACTION; - decrement_pending(skip); + // if the promise was rejected by the user, via `getAbortSignal`, then + // wait for a subsequent resolution instead of flushing the batch + if (error === STALE_REACTION) { + return; } - if (error === STALE_REACTION || (effect.f & DESTROYED) !== 0) { - return; + if (decrement_pending) { + decrement_pending(); } batch.activate(); @@ -255,7 +256,7 @@ export function async_derived(fn, label, location) { teardown(() => { for (const d of deferreds.values()) { - d.reject(STALE_REACTION); + d.reject(OBSOLETE); } });