From 8f7e8b936ef72e89858839e2d3c85916377d204f Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 12 Nov 2025 20:23:44 +0100 Subject: [PATCH] fix: ensure deferred effects can be rescheduled later on 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 84308ef3ed..49415f8913 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'; @@ -270,11 +271,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}