Skip to content

Commit d55107f

Browse files
fix: delete from batch_values on updates (#17115)
* fix: delete from batch_values on updates This fixes a bug where a derived would still show its old value even after it was indirectly updated again within the same batch. This can for example happen by reading a derived on an effect, then writing to a source in that same effect that makes the derived update, and then read the derived value in a sibling effect - it still shows the old value without the fix. The fix is to _delete_ the value from batch_values, as it's now the newest value across all batches. In order to not prevent breakage on other batches we have to leave the status of deriveds as-is, i.e. within is_dirty and update_derived we cannot update its status. That might be a bit more inefficient as you now have to traverse the graph for each `get` of that derived (it's a bit like they are all disconnected) but we can always optimize that later if need be. Another advantage of this fix is that we can get rid of duplicate logic we had to add about unmarking and reconnecting deriveds, because that logic was only needed for the case where deriveds are read after they are updated, which now no longer hits that if-branch * keep derived cache, but clear it in mark_reactions (#17116) --------- Co-authored-by: Rich Harris <rich.harris@vercel.com>
1 parent 22435b2 commit d55107f

File tree

4 files changed

+24
-37
lines changed

4 files changed

+24
-37
lines changed

packages/svelte/src/internal/client/reactivity/deriveds.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
import { equals, safe_equals } from './equality.js';
2727
import * as e from '../errors.js';
2828
import * as w from '../warnings.js';
29-
import { async_effect, destroy_effect, teardown } from './effects.js';
29+
import { async_effect, destroy_effect, effect_tracking, teardown } from './effects.js';
3030
import { eager_effects, internal_set, set_eager_effects, source } from './sources.js';
3131
import { get_stack } from '../dev/tracing.js';
3232
import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js';
@@ -365,8 +365,14 @@ export function update_derived(derived) {
365365
return;
366366
}
367367

368+
// During time traveling we don't want to reset the status so that
369+
// traversal of the graph in the other batches still happens
368370
if (batch_values !== null) {
369-
batch_values.set(derived, derived.v);
371+
// only cache the value if we're in a tracking context, otherwise we won't
372+
// clear the cache in `mark_reactions` when dependencies are updated
373+
if (effect_tracking()) {
374+
batch_values.set(derived, derived.v);
375+
}
370376
} else {
371377
var status = (derived.f & CONNECTED) === 0 ? MAYBE_DIRTY : CLEAN;
372378
set_signal_status(derived, status);

packages/svelte/src/internal/client/reactivity/sources.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import * as e from '../errors.js';
3434
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
3535
import { get_stack, tag_proxy } from '../dev/tracing.js';
3636
import { component_context, is_runes } from '../context.js';
37-
import { Batch, eager_block_effects, schedule_effect } from './batch.js';
37+
import { Batch, batch_values, eager_block_effects, schedule_effect } from './batch.js';
3838
import { proxy } from '../proxy.js';
3939
import { execute_derived } from './deriveds.js';
4040

@@ -334,12 +334,17 @@ function mark_reactions(signal, status) {
334334
}
335335

336336
if ((flags & DERIVED) !== 0) {
337+
var derived = /** @type {Derived} */ (reaction);
338+
339+
batch_values?.delete(derived);
340+
337341
if ((flags & WAS_MARKED) === 0) {
338342
// Only connected deriveds can be reliably unmarked right away
339343
if (flags & CONNECTED) {
340344
reaction.f |= WAS_MARKED;
341345
}
342-
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
346+
347+
mark_reactions(derived, MAYBE_DIRTY);
343348
}
344349
} else if (not_dirty) {
345350
if ((flags & BLOCK_EFFECT) !== 0) {

packages/svelte/src/internal/client/runtime.js

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,12 @@ export function is_dirty(reaction) {
179179
}
180180
}
181181

182-
if ((flags & CONNECTED) !== 0) {
182+
if (
183+
(flags & CONNECTED) !== 0 &&
184+
// During time traveling we don't want to reset the status so that
185+
// traversal of the graph in the other batches still happens
186+
batch_values === null
187+
) {
183188
set_signal_status(reaction, CLEAN);
184189
}
185190
}
@@ -611,25 +616,15 @@ export function get(signal) {
611616
} else if (is_derived) {
612617
derived = /** @type {Derived} */ (signal);
613618

614-
var should_reconnect = is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0;
615-
616619
if (batch_values?.has(derived)) {
617-
// This happens as part of is_dirty normally, but we return early
618-
// here so we need to do it separately
619-
remove_marked_flag(derived);
620-
621-
if (should_reconnect) {
622-
reconnect(derived);
623-
}
624-
625620
return batch_values.get(derived);
626621
}
627622

628623
if (is_dirty(derived)) {
629624
update_derived(derived);
630625
}
631626

632-
if (should_reconnect) {
627+
if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) {
633628
reconnect(derived);
634629
}
635630
} else if (batch_values?.has(signal)) {
@@ -662,23 +657,6 @@ function reconnect(derived) {
662657
}
663658
}
664659

665-
/**
666-
* Removes the WAS_MARKED flag from the derived and its dependencies
667-
* @param {Value} derived
668-
*/
669-
function remove_marked_flag(derived) {
670-
// We cannot stop at the first non-marked derived because batch_values can
671-
// cause "holes" of unmarked deriveds in an otherwise marked graph
672-
if ((derived.f & DERIVED) === 0) return;
673-
674-
derived.f &= ~WAS_MARKED;
675-
676-
// Only deriveds with dependencies can be marked
677-
for (const dep of /** @type {Value[]} */ (/** @type {Derived} */ (derived).deps)) {
678-
remove_marked_flag(dep);
679-
}
680-
}
681-
682660
/** @param {Derived} derived */
683661
function depends_on_old_values(derived) {
684662
if (derived.v === UNINITIALIZED) return true; // we don't know, so assume the worst

packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@ export default test({
77

88
button?.click();
99
await tick();
10-
// TODO this is wrong: it should be [5]
11-
assert.deepEqual(logs, [4]);
10+
assert.deepEqual(logs, [5]);
1211

1312
button?.click();
1413
await tick();
15-
// TODO this is wrong: it should be [5, 7]
16-
assert.deepEqual(logs, [4, 7]);
14+
assert.deepEqual(logs, [5, 7]);
1715
}
1816
});

0 commit comments

Comments
 (0)