diff --git a/packages/svelte/src/internal/client/reactivity/equality.js b/packages/svelte/src/internal/client/reactivity/equality.js index ed8aeb600a..9b2c1fc605 100644 --- a/packages/svelte/src/internal/client/reactivity/equality.js +++ b/packages/svelte/src/internal/client/reactivity/equality.js @@ -5,7 +5,16 @@ import { active_batch, current_batch } from './batch.js'; function get_value(signal) { var batch = active_batch ?? (current_batch?.is_fork ? current_batch : null); var override = batch?.values?.get(signal); - return override === undefined ? signal.v : override[0]; + + if (override === undefined) return signal.v; + + // In a fork, or for a batch's own writes, the override _is_ the value being + // written over. An override owned by another live batch is just that batch's + // pre-write world being shown to us — writes must compare against the real + // (pending) value, otherwise a write that equals the visible-but-stale value + // would be swallowed, and one that equals the pending value would be + // treated as a change + return batch?.is_fork || override[1] === null ? override[0] : signal.v; } /** @type {Equals} */ diff --git a/packages/svelte/tests/runtime-runes/samples/async-write-equals-real-value/_config.js b/packages/svelte/tests/runtime-runes/samples/async-write-equals-real-value/_config.js new file mode 100644 index 0000000000..be2a3bcfd3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-write-equals-real-value/_config.js @@ -0,0 +1,36 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = ''; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + const [b, z, shift] = target.querySelectorAll('button'); + + assert.htmlEqual(target.innerHTML, `
0
a
${buttons}`); + assert.deepEqual(logs, ['fetch a']); + + // write x = 'b' — the batch is pending on its async expression + b.click(); + await tick(); + assert.deepEqual(logs, ['fetch a', 'fetch b']); + + // an independent batch runs the effect, which writes x = 'b' — the + // real value is already 'b', so this is a no-op: the async expression + // must not re-run (no needless refetch), and the batch must not be + // entangled with the pending one (z commits immediately) + z.click(); + await tick(); + assert.deepEqual(logs, ['fetch a', 'fetch b']); + assert.htmlEqual(target.innerHTML, `1
a
${buttons}`); + + // the pending batch settles + shift.click(); + await tick(); + shift.click(); + await tick(); + assert.deepEqual(logs, ['fetch a', 'fetch b']); + assert.htmlEqual(target.innerHTML, `1
b
${buttons}`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-write-equals-real-value/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-write-equals-real-value/main.svelte new file mode 100644 index 0000000000..22276aba5a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-write-equals-real-value/main.svelte @@ -0,0 +1,25 @@ + + +{z}
+{await delay(x)}
+ + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-write-equals-visible-value/_config.js b/packages/svelte/tests/runtime-runes/samples/async-write-equals-visible-value/_config.js new file mode 100644 index 0000000000..b1b6c717af --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-write-equals-visible-value/_config.js @@ -0,0 +1,33 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = ''; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + const [b, reset, shift] = target.querySelectorAll('button'); + + assert.htmlEqual(target.innerHTML, `0
a
${buttons}`); + assert.deepEqual(logs, ['fetch a']); + + // write x = 'b' — the batch is pending on its async expression + b.click(); + await tick(); + assert.deepEqual(logs, ['fetch a', 'fetch b']); + + // an independent batch runs the effect, which resets x = 'a'. The real + // (pending) value is 'b', so this is a genuine change and must not be + // swallowed just because the effect's world still shows 'a' + reset.click(); + await tick(); + assert.deepEqual(logs, ['fetch a', 'fetch b', 'fetch a']); + + // resolve all in-flight runs — the reset must win + shift.click(); + await tick(); + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `1
a
${buttons}`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-write-equals-visible-value/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-write-equals-visible-value/main.svelte new file mode 100644 index 0000000000..fe70fd762b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-write-equals-visible-value/main.svelte @@ -0,0 +1,25 @@ + + +{z}
+{await delay(x)}
+ + +