fix: ensure deriveds values are correct across batches (#17917)

capture derived updates aswell so they become part of current/previous
so that `batch_values` computation is correct when e.g. using
`$state.eager` with a derived. Fixes #17849
pull/17922/head
Simon H 4 days ago committed by GitHub
parent 63686ae22c
commit 44c4f213e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure deriveds values are correct across batches

@ -366,11 +366,11 @@ export class Batch {
* Associate a change to a given source with the current
* batch, noting its previous and current values
* @param {Source} source
* @param {any} value
* @param {any} old_value
*/
capture(source, value) {
if (value !== UNINITIALIZED && !this.previous.has(source)) {
this.previous.set(source, value);
capture(source, old_value) {
if (old_value !== UNINITIALIZED && !this.previous.has(source)) {
this.previous.set(source, old_value);
}
// Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get`
@ -572,7 +572,7 @@ export class Batch {
// ...and undo changes belonging to other batches
for (const batch of batches) {
if (batch === this) continue;
if (batch === this || batch.is_fork) continue;
for (const [source, previous] of batch.previous) {
if (!batch_values.has(source)) {
@ -1020,13 +1020,6 @@ export function fork(fn) {
source.v = value;
}
// make writable deriveds dirty, so they recalculate correctly
for (source of batch.current.keys()) {
if ((source.f & DERIVED) !== 0) {
set_signal_status(source, DIRTY);
}
}
return {
commit: async () => {
if (committed) {

@ -371,6 +371,7 @@ export function execute_derived(derived) {
* @returns {void}
*/
export function update_derived(derived) {
var old_value = derived.v;
var value = execute_derived(derived);
if (!derived.equals(value)) {
@ -382,6 +383,7 @@ export function update_derived(derived) {
// change, `derived.equals` may incorrectly return `true`
if (!current_batch?.is_fork || derived.deps === null) {
derived.v = value;
current_batch?.capture(derived, old_value);
// deriveds without dependencies should never be recomputed
if (derived.deps === null) {

@ -231,7 +231,11 @@ export function internal_set(source, value, updated_during_traversal = null) {
execute_derived(derived);
}
update_derived_status(derived);
// During time traveling we don't want to reset the status so that
// traversal of the graph in the other batches still happens
if (batch_values === null) {
update_derived_status(derived);
}
}
source.wv = increment_write_version();

@ -0,0 +1,23 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [increment, shift] = target.querySelectorAll('button');
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>clicks: 0 - 0 - 0</button> <button>shift</button> <p>true - true</p>`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>clicks: 1 - 1 - 1</button> <button>shift</button> <p>false - false</p>`
);
}
});

@ -0,0 +1,22 @@
<script>
let count = $state(0);
const delayedCount = $derived(await push(count));
const derivedCount = $derived(count);
let resolvers = [];
function push(value) {
if (!value) return value;
const { promise, resolve } = Promise.withResolvers();
resolvers.push(() => resolve(value));
return promise;
}
</script>
<button onclick={() => count += 1}>
clicks: {count} - {delayedCount} - {derivedCount}
</button>
<button onclick={() => resolvers.shift()?.()}>shift</button>
<p>{$state.eager(count) !== count} - {$state.eager(derivedCount) !== derivedCount}</p>

@ -0,0 +1,16 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [btn] = target.querySelectorAll('button');
btn.click();
await tick();
assert.deepEqual(logs, [10]);
btn.click();
await tick();
assert.deepEqual(logs, [10, 10]);
}
});

@ -0,0 +1,21 @@
<script>
import { fork } from 'svelte';
let s = $state(1);
let d = $derived(s * 10);
</script>
<button
onclick={() => {
const f = fork(() => {
// d has not been read yet, so this write happens with an uninitialized old value
s = 2;
d = 99;
});
f.discard();
console.log(d);
}}
>
test
</button>

@ -18,6 +18,14 @@ export default test({
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<p>0</p>
`
);
increment.click();
await tick();
@ -28,5 +36,27 @@ export default test({
<p>2</p>
`
);
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<p>2</p>
`
);
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<p>4</p>
`
);
}
});

Loading…
Cancel
Save