diff --git a/.changeset/warm-cougars-behave.md b/.changeset/warm-cougars-behave.md
new file mode 100644
index 0000000000..15e32f7436
--- /dev/null
+++ b/.changeset/warm-cougars-behave.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: reject pending async deriveds on discard
diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js
index 3b82059788..7d14b80519 100644
--- a/packages/svelte/src/internal/client/reactivity/batch.js
+++ b/packages/svelte/src/internal/client/reactivity/batch.js
@@ -41,6 +41,7 @@ import { set_signal_status } from './status.js';
import { legacy_is_updating_store } from './store.js';
import { invariant } from '../../shared/dev.js';
import { log_effect_tree } from '../dev/debug.js';
+import { OBSOLETE } from './deriveds.js';
/** @type {Batch | null} */
let first_batch = null;
@@ -511,6 +512,10 @@ export class Batch {
if (d) deferred.promise.then(d.resolve).catch(d.reject);
}
+ // Clear them or else those that are still pending might get rejected on discard (after merged-into batch is done).
+ // This can happen when batch Y merged into X and Y has a pending boundary and therefore still-pending async deriveds inside.
+ batch.async_deriveds.clear();
+
// Mark is not guaranteed not touch these, so we transfer them
this.transfer_effects(batch.#dirty_effects, batch.#maybe_dirty_effects);
@@ -629,6 +634,10 @@ export class Batch {
for (const fn of this.#discard_callbacks) fn(this);
this.#discard_callbacks.clear();
+ for (const deferred of this.async_deriveds.values()) {
+ deferred.reject(OBSOLETE);
+ }
+
this.#unlink();
this.#deferred?.resolve();
}
@@ -677,12 +686,15 @@ export class Batch {
}
}
- if (!batch.#started) continue;
+ var current = [...batch.current.keys()].filter(
+ (source) => !(/** @type {[any, boolean]} */ (batch.current.get(source))[1])
+ );
+
+ // If not started yet or no sources to update (which is e.g. possible for the very first batch) then bail
+ if (!batch.#started || current.length === 0) continue;
// Re-run async/block effects that depend on distinct values changed in both batches (ignoring deriveds)
- var others = [...batch.current.keys()].filter(
- (s) => !(/** @type {[any, boolean]} */ (batch.current.get(s))[1]) && !this.current.has(s)
- );
+ var others = current.filter((source) => !this.current.has(source));
if (others.length === 0) {
if (is_earlier) {
diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-obsolete-ignore/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-obsolete-ignore/_config.js
new file mode 100644
index 0000000000..e127fd03ec
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-fork-obsolete-ignore/_config.js
@@ -0,0 +1,39 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target, logs }) {
+ await tick();
+ const [fork, real, resolve] = target.querySelectorAll('button');
+
+ fork.click();
+ await tick();
+ resolve.click();
+ await tick();
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ 0
+
+
+
+ `
+ );
+ assert.deepEqual(logs, [0]);
+
+ real.click();
+ await tick();
+ resolve.click();
+ await tick();
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ 1
+
+
+
+ `
+ );
+ assert.deepEqual(logs, [0, 1]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-obsolete-ignore/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-obsolete-ignore/main.svelte
new file mode 100644
index 0000000000..85056e02b8
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-fork-obsolete-ignore/main.svelte
@@ -0,0 +1,23 @@
+
+
+{await push(count)}
+
+
+