diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 6a662e0c85..50dda42c4a 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -1050,6 +1050,10 @@ export class Batch { wv_values.set(source, current.wv); } + for (const [effect, wv] of this.stale_effects) { + wv_values.set(effect, wv); + } + for (let batch = first_batch; batch !== null; batch = batch.next) { if (batch === this) continue; @@ -1567,8 +1571,9 @@ export function fork(fn) { } } + // All the block/async effects the fork executed are now guaranteed to be up to date for (const effect of batch.stale_effects.keys()) { - effect.wv = effect.wv > write_version ? effect.wv : write_version; + effect.wv = write_version; } batch.stale_effects.clear(); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 9bb1e4e2e4..debadee6eb 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -189,8 +189,10 @@ export function is_dirty(reaction) { if ( (flags & CONNECTED) !== 0 && // During time traveling we don't want to reset the status so that - // traversal of the graph in the other batches still happens - batch_values === null + // traversal of the graph in the other batches still happens. Effects + // can be reset because block/async effects execute right away and others + // are deferred and re-dirtied as needed. + (batch_values === null || (flags & DERIVED) === 0) ) { set_signal_status(reaction, CLEAN); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-effect-write-version/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-effect-write-version/_config.js new file mode 100644 index 0000000000..5d164fcc82 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-effect-write-version/_config.js @@ -0,0 +1,41 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = ` + + + +`; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + const [fork_button, resolve, commit] = target.querySelectorAll('button'); + + assert.deepEqual(logs, [[0, true]]); + assert.htmlEqual(target.innerHTML, `${buttons}
0
`); + + fork_button.click(); + + await tick(); + assert.deepEqual(logs, [ + [0, true], + [2, true] + ]); + assert.htmlEqual(target.innerHTML, `${buttons}0
`); + + // `delayed` changes, but `nonnegative` stays true. The async expression has + // already consumed the fork's `doubled`, so it should not run again. + resolve.click(); + await tick(); + assert.deepEqual(logs, [ + [0, true], + [2, true] + ]); + assert.htmlEqual(target.innerHTML, `${buttons}0
`); + + commit.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}2
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-effect-write-version/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-effect-write-version/main.svelte new file mode 100644 index 0000000000..49083cc97b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-effect-write-version/main.svelte @@ -0,0 +1,27 @@ + + + + + + +{await load(doubled, nonnegative)}