diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 1ba46af4a1..b3c3b0d63d 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -23,7 +23,9 @@ import { ERROR_VALUE, MANAGED_EFFECT, REACTION_RAN, - ASYNC + ASYNC, + EFFECT, + RENDER_EFFECT } from './constants.js'; import { invalidate, old_values } from './reactivity/sources.js'; import { @@ -504,10 +506,16 @@ export function update_effect(effect) { // The effect ran with values that are not the latest ones (it saw its own batch's view). // Don't update its write version — instead remember it so that the batch can bring it // up to date on commit, and tell all subsequent batches that it may need to re-run in their view. + // Render/user effects have made that view visible (in the DOM, or through side effects), which their + // write version can't tell: their dependencies may be back to the values the effect last saw for real + // (e.g. a derived whose value didn't change), so they are dirty. Block/async effects are only maybe + // dirty, since their results are kept per batch, and they run during traversal: marking them dirty + // would re-run them (and restart async work) on every process of a pending batch. var own = /** @type {Batch} */ (own_batch); + var status = (flags & (EFFECT | RENDER_EFFECT | MANAGED_EFFECT)) !== 0 ? DIRTY : MAYBE_DIRTY; own.stale_effects.set(effect, write_version); for (var batch = own.next; batch !== null; batch = batch.next) { - batch.add_dirty_reaction(effect, MAYBE_DIRTY); + batch.add_dirty_reaction(effect, status); } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlapping-batches-stale-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlapping-batches-stale-derived/_config.js new file mode 100644 index 0000000000..87bf987641 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlapping-batches-stale-derived/_config.js @@ -0,0 +1,35 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +// Two overlapping batches write to `b`. The first one commits first, running effects that read +// `sum` with its own (batch-local) view of `b`. The second one sets `b` back to its original +// value, so `sum` ends up where these effects last saw it outside of any batch-local view — +// they still need to re-run, since what they show is the first batch's view. +export default test({ + mode: ['client'], + async test({ assert, target, instance }) { + for (let i = 0; i < 3; i++) { + instance.resolve_all(); + await tick(); + } + assert.htmlEqual(target.innerHTML, '

p0|0

slow0|key0'); + assert.deepEqual(instance.log, [0]); + + instance.set_b(1); + flushSync(); + instance.set_b(0); + + // the first batch resolves and commits, the second one is still pending + instance.resolve_all(); + await tick(); + assert.htmlEqual(target.innerHTML, '

p0|1

slow0|key1'); + assert.deepEqual(instance.log, [0, 1]); + + for (let i = 0; i < 3; i++) { + instance.resolve_all(); + await tick(); + } + assert.htmlEqual(target.innerHTML, '

p0|0

slow0|key0'); + assert.deepEqual(instance.log, [0, 1, 0]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlapping-batches-stale-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlapping-batches-stale-derived/main.svelte new file mode 100644 index 0000000000..3a6eb95326 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlapping-batches-stale-derived/main.svelte @@ -0,0 +1,32 @@ + + +

{await f('p', a)}|{sum}

+{#key c} + {slow}|{await f('key', b)} +{/key}