Skip to content

Commit f5f3879

Browse files
authored
fix: increment derived versions when updating (#12047)
We need to ensure that if derived a depends on derived b, and a is reconnecting to the dependency graph, that if b was updated more recently than a, a also updates. Fixes #11988 Fixes #12044
1 parent 95d07de commit f5f3879

File tree

6 files changed

+111
-8
lines changed

6 files changed

+111
-8
lines changed

.changeset/brave-pigs-obey.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: increment derived versions when updating

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
mark_reactions,
99
current_skip_reaction,
1010
execute_reaction_fn,
11-
destroy_effect_children
11+
destroy_effect_children,
12+
increment_version
1213
} from '../runtime.js';
1314
import { equals, safe_equals } from './equality.js';
1415

@@ -104,6 +105,7 @@ export function update_derived(derived, force_schedule) {
104105
var is_equal = derived.equals(value);
105106

106107
if (!is_equal) {
108+
derived.version = increment_version();
107109
derived.v = value;
108110
mark_reactions(derived, DIRTY, force_schedule);
109111

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,8 +323,6 @@ export function prop(props, key, flags, fallback) {
323323
from_child = true;
324324
set(inner_current_value, value);
325325
get(current_value); // force a synchronisation immediately
326-
// Increment the value so that unowned/disconnected nodes can validate dirtiness again.
327-
current_value.version++;
328326
}
329327

330328
return value;

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import {
1414
set_current_untracked_writes,
1515
set_last_inspected_signal,
1616
set_signal_status,
17-
untrack
17+
untrack,
18+
increment_version
1819
} from '../runtime.js';
1920
import { equals, safe_equals } from './equality.js';
2021
import { CLEAN, DERIVED, DIRTY, BRANCH_EFFECT } from '../constants.js';
@@ -99,7 +100,7 @@ export function set(signal, value) {
99100
signal.v = value;
100101

101102
// Increment write version so that unowned signals can properly track dirtiness
102-
signal.version++;
103+
signal.version = increment_version();
103104

104105
// If the current signal is running for the first time, it won't have any
105106
// reactions as we only allocate and assign the reactions after the signal

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ export function set_last_inspected_signal(signal) {
122122
/** If `true`, `get`ting the signal should not register it as a dependency */
123123
export let current_untracking = false;
124124

125+
/** @type {number} */
126+
let current_version = 0;
127+
125128
// If we are working with a get() chain that has no active container,
126129
// to prevent memory leaks, we skip adding the reaction.
127130
export let current_skip_reaction = false;
@@ -155,6 +158,10 @@ export function set_dev_current_component_function(fn) {
155158
dev_current_component_function = fn;
156159
}
157160

161+
export function increment_version() {
162+
return current_version++;
163+
}
164+
158165
/** @returns {boolean} */
159166
export function is_runes() {
160167
return current_component_context !== null && current_component_context.l === null;
@@ -234,7 +241,6 @@ export function check_dirtiness(reaction) {
234241
// is also dirty.
235242

236243
if (version > /** @type {import('#client').Derived} */ (reaction).version) {
237-
/** @type {import('#client').Derived} */ (reaction).version = version;
238244
return !is_equal;
239245
}
240246

@@ -257,9 +263,9 @@ export function check_dirtiness(reaction) {
257263
// In thise case, we need to re-attach it to the graph and mark it dirty if any of its dependencies have
258264
// changed since.
259265
if (version > /** @type {import('#client').Derived} */ (reaction).version) {
260-
/** @type {import('#client').Derived} */ (reaction).version = version;
261266
is_dirty = true;
262267
}
268+
263269
reactions = dependency.reactions;
264270
if (reactions === null) {
265271
dependency.reactions = [reaction];

packages/svelte/tests/signals/test.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ describe('signals', () => {
232232
// Ensure we're not leaking dependencies
233233
assert.deepEqual(
234234
nested.slice(0, -2).map((s) => s.deps),
235-
[null, null]
235+
[null, null, null, null]
236236
);
237237
};
238238
});
@@ -446,4 +446,95 @@ describe('signals', () => {
446446
assert.equal(state?.reactions, null);
447447
};
448448
});
449+
450+
test('deriveds update upon reconnection #1', () => {
451+
let a = source(false);
452+
let b = source(false);
453+
454+
let c = derived(() => $.get(a));
455+
let d = derived(() => $.get(c));
456+
457+
let last: Record<string, boolean | null> = {};
458+
459+
render_effect(() => {
460+
last = {
461+
a: $.get(a),
462+
b: $.get(b),
463+
c: $.get(c),
464+
d: $.get(a) || $.get(b) ? $.get(d) : null
465+
};
466+
});
467+
468+
return () => {
469+
assert.deepEqual(last, { a: false, b: false, c: false, d: null });
470+
471+
flushSync(() => set(a, true));
472+
flushSync(() => set(b, true));
473+
assert.deepEqual(last, { a: true, b: true, c: true, d: true });
474+
475+
flushSync(() => set(a, false));
476+
flushSync(() => set(b, false));
477+
assert.deepEqual(last, { a: false, b: false, c: false, d: null });
478+
479+
flushSync(() => set(a, true));
480+
flushSync(() => set(b, true));
481+
assert.deepEqual(last, { a: true, b: true, c: true, d: true });
482+
483+
flushSync(() => set(a, false));
484+
flushSync(() => set(b, false));
485+
assert.deepEqual(last, { a: false, b: false, c: false, d: null });
486+
487+
flushSync(() => set(b, true));
488+
assert.deepEqual(last, { a: false, b: true, c: false, d: false });
489+
};
490+
});
491+
492+
test('deriveds update upon reconnection #2', () => {
493+
let a = source(false);
494+
let b = source(false);
495+
let c = source(false);
496+
497+
let d = derived(() => $.get(a) || $.get(b));
498+
499+
let branch = '';
500+
501+
render_effect(() => {
502+
if ($.get(c) && !$.get(d)) {
503+
branch = 'if';
504+
} else {
505+
branch = 'else';
506+
}
507+
});
508+
509+
return () => {
510+
assert.deepEqual(branch, 'else');
511+
512+
flushSync(() => set(c, true));
513+
assert.deepEqual(branch, 'if');
514+
515+
flushSync(() => set(a, true));
516+
assert.deepEqual(branch, 'else');
517+
518+
set(a, false);
519+
set(b, false);
520+
set(c, false);
521+
flushSync();
522+
assert.deepEqual(branch, 'else');
523+
524+
flushSync(() => set(c, true));
525+
assert.deepEqual(branch, 'if');
526+
527+
flushSync(() => set(b, true));
528+
assert.deepEqual(branch, 'else');
529+
530+
set(a, false);
531+
set(b, false);
532+
set(c, false);
533+
flushSync();
534+
assert.deepEqual(branch, 'else');
535+
536+
flushSync(() => set(c, true));
537+
assert.deepEqual(branch, 'if');
538+
};
539+
});
449540
});

0 commit comments

Comments
 (0)