diff --git a/.changeset/grumpy-gifts-sit.md b/.changeset/grumpy-gifts-sit.md new file mode 100644 index 000000000000..fab1bd9cccd8 --- /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 84308ef3ed61..49415f8913d2 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 000000000000..b3e04204b4d9 --- /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 000000000000..45645b40857a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-update-same-state/main.svelte @@ -0,0 +1,37 @@ + + + + + + + + +

{count}