diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 03483ea9ac..5fa57e9b6d 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -119,8 +119,17 @@ export function read_batch_local_value(reaction) { * @returns {{ v: any }} */ export function own_value(signal) { - var own = batch_values === null ? undefined : current_batch?.current.get(signal); - return own === undefined ? signal : own; + if (batch_values === null) return signal; + + var own = current_batch?.current.get(signal); + if (own !== undefined) return own; + + // Outside of effect flushes, the view is the one of the current batch: it may show another value, e.g. a later + // batch's previous one. While effects are flushed, the view is the one of the flushed batch, but writes belong + // to a new batch (and deriveds are captured into both) + return previous_batch === null && batch_values.has(signal) + ? { v: batch_values.get(signal) } + : signal; } /** @type {Effect | null} */ diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 2abc477fef..cd8fe6eb45 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -427,7 +427,7 @@ export function update_derived(derived) { var value = execute_derived(derived); var d = own_value(derived); - if (d.v === derived.v ? !derived.equals(value) : !derived.equals.call(d, value)) { + if (d === derived ? !derived.equals(value) : !derived.equals.call(d, value)) { if (current_batch !== null || previous_batch !== null) { // `capture` decides whether the underlying value is updated (it isn't in a fork, // or if a later batch holds a newer value) and records it in the batch either way. @@ -436,9 +436,8 @@ export function update_derived(derived) { // to the previous batch, not the new one (which can already exist if an earlier // effect wrote to a source). Not adding it to either can cause bugs, so we add it to both. // See https://github.com/sveltejs/svelte/pull/18117 for more details. - // Order matters: current_batch first so it gets the correct previous value. + previous_batch?.capture(derived, value, true); current_batch?.capture(derived, value, true); - previous_batch?.capture(derived, value, true, current_batch !== null); } else { derived.v = value; derived.wv = increment_write_version(); diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 24d15550e8..aa7a94016f 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -191,7 +191,8 @@ var count_deps = 0; */ export function internal_set(source, value, updated_during_traversal = null) { var s = own_value(source); - if (s.v === source.v ? !source.equals(value) : !source.equals.call(s, value)) { + + if (s === source ? !source.equals(value) : !source.equals.call(s, value)) { if (is_destroying_effect) { old_values.set(source, value); } else if (!old_values.has(source)) { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 7518b11742..b4e5d1c7e1 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -52,6 +52,7 @@ import { Batch, batch_values, current_batch, + first_batch, flushSync, held_sources, previous_batch, @@ -342,17 +343,17 @@ export function update_reaction(reaction) { function update_dependencies(reaction) { var deps = reaction.deps; - // Don't remove reactions during fork; - // they must remain for when fork is discarded - var is_fork = current_batch?.is_fork; - if (new_deps !== null) { var i; - if (!is_fork) { - remove_reactions(reaction, skipped_deps); + if (deps !== null && skipped_deps < deps.length && keep_dependencies(deps)) { + for (i = skipped_deps; i < deps.length; i++) { + if (!includes.call(new_deps, deps[i])) new_deps.push(deps[i]); + } } + remove_reactions(reaction, skipped_deps); + if (deps !== null && skipped_deps > 0) { deps.length = skipped_deps + new_deps.length; for (i = 0; i < new_deps.length; i++) { @@ -367,7 +368,7 @@ function update_dependencies(reaction) { (deps[i].reactions ??= []).push(reaction); } } - } else if (!is_fork && deps !== null && skipped_deps < deps.length) { + } else if (deps !== null && skipped_deps < deps.length && !keep_dependencies(deps)) { remove_reactions(reaction, skipped_deps); deps.length = skipped_deps; } @@ -375,6 +376,31 @@ function update_dependencies(reaction) { return deps; } +/** + * Whether a reaction should keep the dependencies it didn't read this time. While several batches exist (or in + * a fork), they're needed if it read a value that some batch changes: other batches (or the real world, once the + * fork is discarded) see another value for it, so the reaction may take another path there (e.g. `a || b` with a + * different `a`), in which it depends on them + * @param {Value[]} deps the previous dependencies, of which the first `skipped_deps` were read again + */ +function keep_dependencies(deps) { + if (batch_values === null) return false; + + for (var batch = first_batch; batch !== null; batch = batch.next) { + for (var i = 0; i < skipped_deps; i++) { + if (batch.current.has(deps[i])) return true; + } + + if (new_deps !== null) { + for (i = 0; i < new_deps.length; i++) { + if (batch.current.has(new_deps[i])) return true; + } + } + } + + return false; +} + /** * @template V * @param {Reaction} signal diff --git a/packages/svelte/tests/runtime-runes/samples/async-equal-result-later-batch-reader/_config.js b/packages/svelte/tests/runtime-runes/samples/async-equal-result-later-batch-reader/_config.js new file mode 100644 index 0000000000..3cc13951df --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-equal-result-later-batch-reader/_config.js @@ -0,0 +1,40 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = + ''; + +// Like `async-equal-result-dropped-later-batch`, but the result is read by something that doesn't wait +// for it: a batch's async result equals the real value, which a later (still pending) batch wrote. The +// batch itself sees that later batch's previous value though, so the result changes its world, and +// whatever read the previous value has to update +export default test({ + mode: ['client'], + async test({ assert, target }) { + await tick(); + const [b1, b2c1, resolve_latest, resolve_oldest, resolve_all] = + target.querySelectorAll('button'); + + assert.htmlEqual(target.innerHTML, `${buttons}
zero
0c0`); + + b1.click(); + await tick(); + b2c1.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}zero
0c0`); + + // the later batch's result becomes the real value, but that batch still waits for c1 + resolve_latest.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}zero
0c0`); + + // the earlier batch's equal result completes it + resolve_oldest.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}pos
1c0`); + + resolve_all.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}pos
2c1`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-equal-result-later-batch-reader/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-equal-result-later-batch-reader/main.svelte new file mode 100644 index 0000000000..df9941e28f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-equal-result-later-batch-reader/main.svelte @@ -0,0 +1,39 @@ + + + + + + + + +{p}
+{#if true}{b}{/if} +{await f('c' + c)}