diff --git a/.changeset/three-camels-sell.md b/.changeset/three-camels-sell.md new file mode 100644 index 0000000000..3bd3c67b9c --- /dev/null +++ b/.changeset/three-camels-sell.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: correctly cleanup unowned derived dependency memory diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 60183520a8..6fdd30e229 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -498,6 +498,7 @@ function destroy_references(signal) { if ((reference.f & IS_EFFECT) !== 0) { destroy_signal(reference); } else { + destroy_references(reference); remove_consumers(reference, 0); reference.d = null; } @@ -823,6 +824,7 @@ export async function tick() { function update_derived(signal, force_schedule) { const previous_updating_derived = updating_derived; updating_derived = true; + destroy_references(signal); const value = execute_signal_fn(signal); updating_derived = previous_updating_derived; const status = current_skip_consumer || (signal.f & UNOWNED) !== 0 ? DIRTY : CLEAN; @@ -1304,8 +1306,8 @@ export function derived(init) { signal.i = init; signal.x = current_component_context; signal.e = default_equals; - if (!is_unowned) { - push_reference(/** @type {import('./types.js').EffectSignal} */ (current_effect), signal); + if (current_consumer !== null) { + push_reference(current_consumer, signal); } return signal; } diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index f56c71086c..7ba75deb49 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1,5 +1,6 @@ import { describe, assert, it } from 'vitest'; import * as $ from '../../src/internal/client/runtime'; +import type { ComputationSignal } from '../../src/internal/client/types'; /** * @param runes runes mode @@ -199,6 +200,38 @@ describe('signals', () => { }; }); + test('correctly cleanup onowned nested derived values', () => { + return () => { + const nested: ComputationSignal[] = []; + + const a = $.source(0); + const b = $.source(0); + const c = $.derived(() => { + const a_2 = $.derived(() => $.get(a) + '!'); + const b_2 = $.derived(() => $.get(b) + '?'); + nested.push(a_2, b_2); + + return { a: $.get(a_2), b: $.get(b_2) }; + }); + + $.get(c); + + $.flushSync(() => $.set(a, 1)); + + $.get(c); + + $.flushSync(() => $.set(b, 1)); + + $.get(c); + + // Ensure we're not leaking dependencies + assert.deepEqual( + nested.slice(0, -2).map((s) => s.d), + [null, null, null, null] + ); + }; + }); + // outside of test function so that they are unowned signals let count = $.source(0); let calc = $.derived(() => {