From 6e571dc9095e292bd58e69846ac93aa2660258a7 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Sun, 16 Nov 2025 03:19:30 +0100 Subject: [PATCH] fix: ensure deferred effects can be rescheduled later on (#17147) When deferring effects we didn't unmark the deriveds that lead to those effects. This means that they might not be reached in subsequent runs of `mark_reactions`. Fixes https://github.com/sveltejs/svelte/issues/17118#issuecomment-3521488865 --- .changeset/grumpy-gifts-sit.md | 5 ++ .../src/internal/client/reactivity/batch.js | 24 ++++++++- .../async-fork-update-same-state/_config.js | 49 +++++++++++++++++++ .../async-fork-update-same-state/main.svelte | 37 ++++++++++++++ 4 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 .changeset/grumpy-gifts-sit.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-update-same-state/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-update-same-state/main.svelte diff --git a/.changeset/grumpy-gifts-sit.md b/.changeset/grumpy-gifts-sit.md new file mode 100644 index 0000000000..fab1bd9ccc --- /dev/null +++ b/.changeset/grumpy-gifts-sit.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure deferred effects can be rescheduled later on diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index ab5bd0b788..03a0721057 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -16,7 +16,8 @@ import { BOUNDARY_EFFECT, EAGER_EFFECT, HEAD_EFFECT, - ERROR_VALUE + ERROR_VALUE, + WAS_MARKED } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property } from '../../shared/utils.js'; @@ -274,11 +275,32 @@ export class Batch { const target = (e.f & DIRTY) !== 0 ? this.#dirty_effects : this.#maybe_dirty_effects; target.push(e); + // Since we're not executing these effects now, we need to clear any WAS_MARKED flags + // so that other batches can correctly reach these effects during their own traversal + this.#clear_marked(e.deps); + // mark as clean so they get scheduled if they depend on pending async state set_signal_status(e, CLEAN); } } + /** + * @param {Value[] | null} deps + */ + #clear_marked(deps) { + if (deps === null) return; + + for (const dep of deps) { + if ((dep.f & DERIVED) === 0 || (dep.f & WAS_MARKED) === 0) { + continue; + } + + dep.f ^= WAS_MARKED; + + this.#clear_marked(/** @type {Derived} */ (dep).deps); + } + } + /** * Associate a change to a given source with the current * batch, noting its previous and current values diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-update-same-state/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-update-same-state/_config.js new file mode 100644 index 0000000000..b3e04204b4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-update-same-state/_config.js @@ -0,0 +1,49 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + assert.deepEqual(logs, [0]); + + const [fork1, fork2, commit] = target.querySelectorAll('button'); + + fork1.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +
0
+ ` + ); + assert.deepEqual(logs, [0]); + + fork2.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +0
+ ` + ); + assert.deepEqual(logs, [0]); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +1
+ ` + ); + assert.deepEqual(logs, [0, 1]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-update-same-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-update-same-state/main.svelte new file mode 100644 index 0000000000..45645b4085 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-update-same-state/main.svelte @@ -0,0 +1,37 @@ + + + + + + + + +{count}