Skip to content

Commit 2b2bd06

Browse files
authored
fix: ensure derived signals properly capture consumers (#10213)
* fix: ensure derived signals properly capture consumers * fix: ensure derived signals properly capture consumers
1 parent 1798e58 commit 2b2bd06

File tree

3 files changed

+106
-42
lines changed

3 files changed

+106
-42
lines changed

.changeset/thick-cycles-rule.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: ensure derived signals properly capture consumers

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

Lines changed: 44 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ let current_scheduler_mode = FLUSH_MICROTASK;
4848
let is_micro_task_queued = false;
4949
let is_task_queued = false;
5050
let is_raf_queued = false;
51+
let is_flushing_effect = false;
5152
// Used for $inspect
5253
export let is_batching_effect = false;
5354

@@ -295,7 +296,6 @@ function is_signal_dirty(signal) {
295296
let i;
296297
for (i = 0; i < length; i++) {
297298
const dependency = dependencies[i];
298-
299299
if ((dependency.f & MAYBE_DIRTY) !== 0 && !is_signal_dirty(dependency)) {
300300
set_signal_status(dependency, CLEAN);
301301
continue;
@@ -343,7 +343,7 @@ function execute_signal_fn(signal) {
343343
current_consumer = signal;
344344
current_block = signal.b;
345345
current_component_context = signal.x;
346-
current_skip_consumer = current_effect === null && (signal.f & UNOWNED) !== 0;
346+
current_skip_consumer = !is_flushing_effect && (signal.f & UNOWNED) !== 0;
347347
current_untracking = false;
348348

349349
// Render effects are invoked when the UI is about to be updated - run beforeUpdate at that point
@@ -366,27 +366,28 @@ function execute_signal_fn(signal) {
366366
res = /** @type {() => V} */ (init)();
367367
}
368368
let dependencies = /** @type {import('./types.js').Signal<unknown>[]} **/ (signal.d);
369-
370369
if (current_dependencies !== null) {
371370
let i;
372371
if (dependencies !== null) {
372+
const deps_length = dependencies.length;
373373
// Include any dependencies up until the current_dependencies_index.
374-
const full_dependencies =
374+
const full_current_dependencies =
375375
current_dependencies_index === 0
376-
? dependencies
376+
? current_dependencies
377377
: dependencies.slice(0, current_dependencies_index).concat(current_dependencies);
378-
const dep_length = full_dependencies.length;
378+
const current_dep_length = full_current_dependencies.length;
379379
// If we have more than 16 elements in the array then use a Set for faster performance
380380
// TODO: evaluate if we should always just use a Set or not here?
381-
const current_dependencies_set = dep_length > 16 ? new Set(full_dependencies) : null;
382-
383-
for (i = current_dependencies_index; i < dep_length; i++) {
384-
const dependency = full_dependencies[i];
381+
const full_current_dependencies_set =
382+
current_dep_length > 16 ? new Set(full_current_dependencies) : null;
383+
for (i = current_dependencies_index; i < deps_length; i++) {
384+
const dependency = dependencies[i];
385385
if (
386-
(current_dependencies_set !== null && !current_dependencies_set.has(dependency)) ||
387-
!full_dependencies.includes(dependency)
386+
(full_current_dependencies_set !== null &&
387+
!full_current_dependencies_set.has(dependency)) ||
388+
!full_current_dependencies.includes(dependency)
388389
) {
389-
remove_consumer(signal, dependency, false);
390+
remove_consumer(signal, dependency);
390391
}
391392
}
392393
}
@@ -415,7 +416,7 @@ function execute_signal_fn(signal) {
415416
}
416417
}
417418
} else if (dependencies !== null && current_dependencies_index < dependencies.length) {
418-
remove_consumers(signal, current_dependencies_index, false);
419+
remove_consumers(signal, current_dependencies_index);
419420
dependencies.length = current_dependencies_index;
420421
}
421422
return res;
@@ -435,10 +436,9 @@ function execute_signal_fn(signal) {
435436
* @template V
436437
* @param {import('./types.js').ComputationSignal<V>} signal
437438
* @param {import('./types.js').Signal<V>} dependency
438-
* @param {boolean} remove_unowned
439439
* @returns {void}
440440
*/
441-
function remove_consumer(signal, dependency, remove_unowned) {
441+
function remove_consumer(signal, dependency) {
442442
const consumers = dependency.c;
443443
let consumers_length = 0;
444444
if (consumers !== null) {
@@ -454,25 +454,20 @@ function remove_consumer(signal, dependency, remove_unowned) {
454454
}
455455
}
456456
}
457-
if (remove_unowned && consumers_length === 0 && (dependency.f & UNOWNED) !== 0) {
457+
if (consumers_length === 0 && (dependency.f & UNOWNED) !== 0) {
458458
// If the signal is unowned then we need to make sure to change it to dirty.
459459
set_signal_status(dependency, DIRTY);
460-
remove_consumers(
461-
/** @type {import('./types.js').ComputationSignal<V>} **/ (dependency),
462-
0,
463-
true
464-
);
460+
remove_consumers(/** @type {import('./types.js').ComputationSignal<V>} **/ (dependency), 0);
465461
}
466462
}
467463

468464
/**
469465
* @template V
470466
* @param {import('./types.js').ComputationSignal<V>} signal
471467
* @param {number} start_index
472-
* @param {boolean} remove_unowned
473468
* @returns {void}
474469
*/
475-
function remove_consumers(signal, start_index, remove_unowned) {
470+
function remove_consumers(signal, start_index) {
476471
const dependencies = signal.d;
477472
if (dependencies !== null) {
478473
const active_dependencies = start_index === 0 ? null : dependencies.slice(0, start_index);
@@ -481,7 +476,7 @@ function remove_consumers(signal, start_index, remove_unowned) {
481476
const dependency = dependencies[i];
482477
// Avoid removing a consumer if we know that it is active (start_index will not be 0)
483478
if (active_dependencies === null || !active_dependencies.includes(dependency)) {
484-
remove_consumer(signal, dependency, remove_unowned);
479+
remove_consumer(signal, dependency);
485480
}
486481
}
487482
}
@@ -502,7 +497,7 @@ function destroy_references(signal) {
502497
if ((reference.f & IS_EFFECT) !== 0) {
503498
destroy_signal(reference);
504499
} else {
505-
remove_consumers(reference, 0, true);
500+
remove_consumers(reference, 0);
506501
reference.d = null;
507502
}
508503
}
@@ -585,19 +580,26 @@ function flush_queued_effects(effects) {
585580
const length = effects.length;
586581
if (length > 0) {
587582
infinite_loop_guard();
588-
let i;
589-
for (i = 0; i < length; i++) {
590-
const signal = effects[i];
591-
const flags = signal.f;
592-
if ((flags & (DESTROYED | INERT)) === 0) {
593-
if (is_signal_dirty(signal)) {
594-
set_signal_status(signal, CLEAN);
595-
execute_effect(signal);
596-
} else if ((flags & MAYBE_DIRTY) !== 0) {
597-
set_signal_status(signal, CLEAN);
583+
const previously_flushing_effect = is_flushing_effect;
584+
is_flushing_effect = true;
585+
try {
586+
let i;
587+
for (i = 0; i < length; i++) {
588+
const signal = effects[i];
589+
const flags = signal.f;
590+
if ((flags & (DESTROYED | INERT)) === 0) {
591+
if (is_signal_dirty(signal)) {
592+
set_signal_status(signal, CLEAN);
593+
execute_effect(signal);
594+
} else if ((flags & MAYBE_DIRTY) !== 0) {
595+
set_signal_status(signal, CLEAN);
596+
}
598597
}
599598
}
599+
} finally {
600+
is_flushing_effect = previously_flushing_effect;
600601
}
602+
601603
effects.length = 0;
602604
}
603605
}
@@ -822,10 +824,7 @@ function update_derived(signal, force_schedule) {
822824
updating_derived = true;
823825
const value = execute_signal_fn(signal);
824826
updating_derived = previous_updating_derived;
825-
const status =
826-
current_skip_consumer || (current_effect === null && (signal.f & UNOWNED) !== 0)
827-
? DIRTY
828-
: CLEAN;
827+
const status = current_skip_consumer || (signal.f & UNOWNED) !== 0 ? DIRTY : CLEAN;
829828
set_signal_status(signal, status);
830829
const equals = /** @type {import('./types.js').EqualsFunctions} */ (signal.e);
831830
if (!equals(value, signal.v)) {
@@ -1083,7 +1082,10 @@ function mark_subtree_children_inert(signal, inert, visited_blocks) {
10831082
if (references !== null) {
10841083
let i;
10851084
for (i = 0; i < references.length; i++) {
1086-
mark_subtree_inert(references[i], inert, visited_blocks);
1085+
const reference = references[i];
1086+
if ((reference.f & IS_EFFECT) !== 0) {
1087+
mark_subtree_inert(references[i], inert, visited_blocks);
1088+
}
10871089
}
10881090
}
10891091
}
@@ -1262,7 +1264,7 @@ export function destroy_signal(signal) {
12621264
const destroy = signal.y;
12631265
const flags = signal.f;
12641266
destroy_references(signal);
1265-
remove_consumers(signal, 0, true);
1267+
remove_consumers(signal, 0);
12661268
signal.i =
12671269
signal.r =
12681270
signal.y =

packages/svelte/tests/signals/test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,61 @@ describe('signals', () => {
171171
}
172172
};
173173
});
174+
175+
test('effects correctly handle unowned derived values that do not change', () => {
176+
const log: number[] = [];
177+
178+
let count = $.source(0);
179+
const read = () => {
180+
const x = $.derived(() => ({ count: $.get(count) }));
181+
return $.get(x);
182+
};
183+
const derivedCount = $.derived(() => read().count);
184+
$.user_effect(() => {
185+
log.push($.get(derivedCount));
186+
});
187+
188+
return () => {
189+
$.flushSync(() => $.set(count, 1));
190+
// Ensure we're not leaking consumers
191+
assert.deepEqual(count.c?.length, 1);
192+
$.flushSync(() => $.set(count, 2));
193+
// Ensure we're not leaking consumers
194+
assert.deepEqual(count.c?.length, 1);
195+
$.flushSync(() => $.set(count, 3));
196+
// Ensure we're not leaking consumers
197+
assert.deepEqual(count.c?.length, 1);
198+
assert.deepEqual(log, [0, 1, 2, 3]);
199+
};
200+
});
201+
202+
let count = $.source(0);
203+
let calc = $.derived(() => {
204+
if ($.get(count) >= 2) {
205+
return 'limit';
206+
}
207+
return $.get(count) * 2;
208+
});
209+
210+
test('effect with derived using new derived every time', () => {
211+
const log: Array<number | string> = [];
212+
213+
const effect = $.user_effect(() => {
214+
log.push($.get(calc));
215+
});
216+
217+
return () => {
218+
$.flushSync(() => $.set(count, 1));
219+
$.flushSync(() => $.set(count, 2));
220+
$.flushSync(() => $.set(count, 3));
221+
$.flushSync(() => $.set(count, 4));
222+
$.flushSync(() => $.set(count, 0));
223+
// Ensure we're not leaking consumers
224+
assert.deepEqual(count.c?.length, 1);
225+
assert.deepEqual(log, [0, 2, 'limit', 0]);
226+
$.destroy_signal(effect);
227+
// Ensure we're not leaking consumers
228+
assert.deepEqual(count.c, null);
229+
};
230+
});
174231
});

0 commit comments

Comments
 (0)