From a371ed2c38ea8858f8682590192e7c1b0bdcb8d3 Mon Sep 17 00:00:00 2001 From: upstream Date: Tue, 29 Sep 2026 11:33:39 +0200 Subject: [PATCH] Upstream sveltejs/svelte state of the touched files before 147800f170 --- packages/svelte/tests/signals/test.ts | 1689 +++++++++++++++++++++++++ 1 file changed, 1689 insertions(+) create mode 100644 packages/svelte/tests/signals/test.ts diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts new file mode 100644 index 0000000..afc8669 --- /dev/null +++ b/packages/svelte/tests/signals/test.ts @@ -0,0 +1,1689 @@ +import { describe, assert, it } from 'vitest'; +import { flushSync } from '../../src/index-client'; +import * as $ from '../../src/internal/client/runtime'; +import { push, pop } from '../../src/internal/client/context'; +import { + effect, + effect_root, + render_effect, + user_effect, + user_pre_effect +} from '../../src/internal/client/reactivity/effects'; +import { state, set, update, update_pre } from '../../src/internal/client/reactivity/sources'; +import type { Derived, Effect, Source, Value } from '../../src/internal/client/types'; +import { proxy } from '../../src/internal/client/proxy'; +import { derived } from '../../src/internal/client/reactivity/deriveds'; +import { snapshot } from '../../src/internal/shared/clone.js'; +import { SvelteSet } from '../../src/reactivity/set'; +import { CONNECTED, DESTROYED } from '../../src/internal/client/constants'; +import { noop } from 'svelte/internal/client'; +import { disable_async_mode_flag, enable_async_mode_flag } from '../../src/internal/flags'; + +/** + * @param runes runes mode + * @param fn A function that returns a function because we first need to set up all the signals + * and then execute the test in order to simulate a real component + */ +function run_test(runes: boolean, fn: (runes: boolean) => () => void) { + return () => { + // Create a component context to test runes vs legacy mode + push({}, runes); + // Create a render context so that effect validations etc don't fail + let execute: any; + const destroy = effect_root(() => { + execute = fn(runes); + }); + pop(); + execute(); + destroy(); + }; +} + +function test(text: string, fn: (runes: boolean) => any) { + it(`${text} (legacy mode)`, run_test(false, fn)); + it(`${text} (runes mode)`, run_test(true, fn)); +} + +test.only = (text: string, fn: (runes: boolean) => any) => { + it.only(`${text} (legacy mode)`, run_test(false, fn)); + it.only(`${text} (runes mode)`, run_test(true, fn)); +}; + +test.skip = (text: string, fn: (runes: boolean) => any) => { + it.skip(`${text} (legacy mode)`, run_test(false, fn)); + it.skip(`${text} (runes mode)`, run_test(true, fn)); +}; + +describe('signals', () => { + test('effect with state and derived in it', () => { + const log: string[] = []; + + let count = state(0); + let double = derived(() => $.get(count) * 2); + effect(() => { + log.push(`${$.get(count)}:${$.get(double)}`); + }); + + return () => { + flushSync(() => set(count, 1)); + flushSync(() => set(count, 2)); + + assert.deepEqual(log, ['0:0', '1:2', '2:4']); + }; + }); + + test('multiple effects with state and derived in it #1', () => { + const log: string[] = []; + + let count = state(0); + let double = derived(() => $.get(count) * 2); + + effect(() => { + log.push(`A:${$.get(count)}:${$.get(double)}`); + }); + effect(() => { + log.push(`B:${$.get(double)}`); + }); + + return () => { + flushSync(() => set(count, 1)); + flushSync(() => set(count, 2)); + + assert.deepEqual(log, ['A:0:0', 'B:0', 'A:1:2', 'B:2', 'A:2:4', 'B:4']); + }; + }); + + test('multiple effects with state and derived in it #2', () => { + const log: string[] = []; + + let count = state(0); + let double = derived(() => $.get(count) * 2); + + effect(() => { + log.push(`A:${$.get(double)}`); + }); + effect(() => { + log.push(`B:${$.get(count)}:${$.get(double)}`); + }); + + return () => { + flushSync(() => set(count, 1)); + flushSync(() => set(count, 2)); + + assert.deepEqual(log, ['A:0', 'B:0:0', 'A:2', 'B:1:2', 'A:4', 'B:2:4']); + }; + }); + + test('unowned deriveds are not added as reactions but trigger effects', () => { + var obj = state(undefined); + + class C1 { + #v = state(0); + get v() { + return $.get(this.#v); + } + set v(v: number) { + set(this.#v, v); + } + } + + return () => { + let d = derived(() => $.get(obj)?.v || '-'); + + const log: number[] = []; + assert.equal($.get(d), '-'); + + let destroy = effect_root(() => { + render_effect(() => { + log.push($.get(d)); + }); + }); + + set(obj, new C1()); + flushSync(); + assert.equal($.get(d), '-'); + $.get(obj).v = 1; + flushSync(); + assert.equal($.get(d), 1); + assert.deepEqual(log, ['-', 1]); + destroy(); + // ensure we're not leaking reactions + assert.equal(obj.reactions, null); + assert.equal(d.reactions, null); + }; + }); + + test('derived from state', () => { + const log: number[] = []; + + let count = state(0); + let double = derived(() => $.get(count) * 2); + + effect(() => { + log.push($.get(double)); + }); + + return () => { + flushSync(() => set(count, 1)); + flushSync(() => set(count, 2)); + + assert.deepEqual(log, [0, 2, 4]); + }; + }); + + test('derived from derived', () => { + const log: number[] = []; + + let count = state(0); + let double = derived(() => $.get(count) * 2); + let quadruple = derived(() => $.get(double) * 2); + + effect(() => { + log.push($.get(quadruple)); + }); + + return () => { + flushSync(() => set(count, 1)); + flushSync(() => set(count, 2)); + + assert.deepEqual(log, [0, 4, 8]); + }; + }); + + test('state reset', () => { + const log: number[] = []; + + let count = state(0); + let double = derived(() => $.get(count) * 2); + + effect(() => { + log.push($.get(double)); + }); + + return () => { + flushSync(); + log.length = 0; + + set(count, 1); + set(count, 0); + + flushSync(); + + assert.deepEqual(log, []); + + set(count, 1); + $.get(double); + set(count, 0); + + flushSync(); + + // TODO: in an ideal world, the effect wouldn't fire here + assert.deepEqual(log, [0]); + }; + }); + + test('https://perf.js.hyoo.ru/#!bench=9h2as6_u0mfnn', () => { + let res: number[] = []; + + const numbers = Array.from({ length: 2 }, (_, i) => i); + const fib = (n: number): number => (n < 2 ? 1 : fib(n - 1) + fib(n - 2)); + const hard = (n: number, l: string) => n + fib(16); + + const A = state(0); + const B = state(0); + const C = derived(() => ($.get(A) % 2) + ($.get(B) % 2)); + const D = derived(() => numbers.map((i) => i + ($.get(A) % 2) - ($.get(B) % 2))); + const E = derived(() => hard($.get(C) + $.get(A) + $.get(D)[0]!, 'E')); + const F = derived(() => hard($.get(D)[0]! && $.get(B), 'F')); + const G = derived(() => $.get(C) + ($.get(C) || $.get(E) % 2) + $.get(D)[0]! + $.get(F)); + effect(() => { + res.push(hard($.get(G), 'H')); + }); + effect(() => { + res.push($.get(G)); + }); + effect(() => { + res.push(hard($.get(F), 'J')); + }); + + return () => { + flushSync(); + + let i = 2; + while (--i) { + res.length = 0; + set(B, 1); + set(A, 1 + i * 2); + flushSync(); + + set(A, 2 + i * 2); + set(B, 2); + flushSync(); + + assert.equal(res.length, 4); + assert.deepEqual(res, [3198, 1601, 3195, 1598]); + } + }; + }); + + test('https://perf.js.hyoo.ru/#!bench=9h2as6_u0mfnn #2', () => { + let res: number[] = []; + + const numbers = Array.from({ length: 2 }, (_, i) => i); + const fib = (n: number): number => (n < 2 ? 1 : fib(n - 1) + fib(n - 2)); + const hard = (n: number, l: string) => n + fib(16); + + const A = state(0); + const B = state(0); + + return () => { + const C = derived(() => ($.get(A) % 2) + ($.get(B) % 2)); + const D = derived(() => numbers.map((i) => i + ($.get(A) % 2) - ($.get(B) % 2))); + const E = derived(() => hard($.get(C) + $.get(A) + $.get(D)[0]!, 'E')); + const F = derived(() => hard($.get(D)[0]! && $.get(B), 'F')); + const G = derived(() => $.get(C) + ($.get(C) || $.get(E) % 2) + $.get(D)[0]! + $.get(F)); + + const destroy = effect_root(() => { + effect(() => { + res.push(hard($.get(G), 'H')); + }); + effect(() => { + res.push($.get(G)); + }); + effect(() => { + res.push(hard($.get(F), 'J')); + }); + }); + + flushSync(); + + let i = 2; + while (--i) { + res.length = 0; + set(B, 1); + set(A, 1 + i * 2); + flushSync(); + + set(A, 2 + i * 2); + set(B, 2); + flushSync(); + + assert.equal(res.length, 4); + assert.deepEqual(res, [3198, 1601, 3195, 1598]); + } + + destroy(); + assert(A.reactions === null); + assert(B.reactions === null); + }; + }); + + test('effects correctly handle unowned derived values that do not change', () => { + const log: number[] = []; + + return () => { + let count = state(0); + const read = () => { + const x = derived(() => ({ count: $.get(count) })); + return $.get(x); + }; + const derivedCount = derived(() => read().count); + + const destroy = effect_root(() => { + user_effect(() => { + log.push($.get(derivedCount)); + }); + }); + + flushSync(() => set(count, 1)); + // Ensure we're not leaking consumers + assert.deepEqual(count.reactions?.length, 1); + flushSync(() => set(count, 2)); + // Ensure we're not leaking consumers + assert.deepEqual(count.reactions?.length, 1); + flushSync(() => set(count, 3)); + // Ensure we're not leaking consumers + assert.deepEqual(count.reactions?.length, 1); + assert.deepEqual(log, [0, 1, 2, 3]); + + destroy(); + }; + }); + + test('correctly cleanup unowned nested derived values', () => { + return () => { + const nested: Derived[] = []; + + const a = state(0); + const b = state(0); + let c: any; + + const destroy = effect_root(() => { + 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); + + destroy(); + + assert.equal(a.reactions, null); + assert.equal(b.reactions, null); + }; + }); + + // outside of test function so that they are unowned signals + let count = state(0); + let calc = derived(() => { + if ($.get(count) >= 2) { + return 'limit'; + } + return $.get(count) * 2; + }); + + test('effect with derived using unowned derived every time', () => { + const log: Array = []; + + const destroy = effect_root(() => { + user_effect(() => { + log.push($.get(calc)); + $.get(calc); + }); + }); + + return () => { + flushSync(() => set(count, 1)); + flushSync(() => set(count, 2)); + flushSync(() => set(count, 3)); + flushSync(() => set(count, 4)); + flushSync(() => set(count, 0)); + // Ensure we're not leaking consumers + assert.deepEqual(count.reactions?.length, 1); + assert.deepEqual(calc.reactions?.length, 1); + assert.deepEqual(log, [0, 2, 'limit', 0]); + destroy(); + // Ensure we're not leaking consumers + assert.deepEqual(count.reactions, null); + assert.deepEqual(calc.reactions, null); + }; + }); + + let no_deps = derived(() => { + return []; + }); + + test('two effects with an unowned derived that has no dependencies', () => { + const log: Array> = []; + + render_effect(() => { + log.push($.get(no_deps)); + }); + + render_effect(() => { + log.push($.get(no_deps)); + }); + + return () => { + flushSync(); + assert.deepEqual(log, [[], []]); + }; + }); + + test('two effects with an unowned derived that has some dependencies', () => { + const log: Array> = []; + + return () => { + let some_state = state({}); + let some_deps = derived(() => { + return [$.get(some_state)]; + }); + let destroy2: any; + + const destroy = effect_root(() => { + render_effect(() => { + $.untrack(() => { + log.push($.get(some_deps)); + }); + }); + + destroy2 = effect_root(() => { + render_effect(() => { + log.push($.get(some_deps)); + }); + + render_effect(() => { + log.push($.get(some_deps)); + }); + }); + }); + + set(some_state, {}); + flushSync(); + + assert.deepEqual(log, [[{}], [{}], [{}], [{}], [{}]]); + + destroy2(); + + set(some_state, {}); + flushSync(); + + assert.deepEqual(log, [[{}], [{}], [{}], [{}], [{}]]); + + log.length = 0; + + const destroy3 = effect_root(() => { + render_effect(() => { + $.untrack(() => { + log.push($.get(some_deps)); + }); + log.push($.get(some_deps)); + }); + }); + + set(some_state, {}); + flushSync(); + + assert.deepEqual(log, [[{}], [{}], [{}], [{}]]); + + destroy3(); + + assert(some_state.reactions === null); + + destroy(); + + assert(some_state.reactions === null); + }; + }); + + test('schedules rerun when writing to signal before reading it', (runes) => { + if (!runes) return () => {}; + + const error = console.error; + console.error = noop; + + const value = state({ count: 0 }); + user_effect(() => { + set(value, { count: 0 }); + $.get(value); + }); + + return () => { + let errored = false; + try { + flushSync(); + } catch (e: any) { + assert.include(e.message, 'effect_update_depth_exceeded'); + errored = true; + } finally { + assert.equal(errored, true); + console.error = error; + } + }; + }); + + test('schedules rerun when updating deeply nested value', (runes) => { + if (!runes) return () => {}; + + const error = console.error; + console.error = noop; + + const value = proxy({ a: { b: { c: 0 } } }); + user_effect(() => { + value.a.b.c += 1; + }); + + return () => { + let errored = false; + try { + flushSync(); + } catch (e: any) { + assert.include(e.message, 'effect_update_depth_exceeded'); + errored = true; + } finally { + assert.equal(errored, true); + console.error = error; + } + }; + }); + + test.skip('schedules rerun when writing to signal before reading it', (runes) => { + if (!runes) return () => {}; + + const error = console.error; + console.error = noop; + + const value = proxy({ arr: [] }); + user_effect(() => { + value.arr = []; + value.arr; + }); + + return () => { + let errored = false; + try { + flushSync(); + } catch (e: any) { + assert.include(e.message, 'effect_update_depth_exceeded'); + errored = true; + } finally { + assert.equal(errored, true); + console.error = error; + } + }; + }); + + test('schedules rerun when writing to signal before reading it from derived', (runes) => { + if (!runes) return () => {}; + let log: any[] = []; + + const value = state(1); + const double = derived(() => $.get(value) * 2); + + user_effect(() => { + set(value, 10); + log.push($.get(double)); + }); + + return () => { + flushSync(); + assert.deepEqual(log, [20]); + }; + }); + + test('schedules rerun when writing to signal after reading it from derived', (runes) => { + if (!runes) return () => {}; + let log: any[] = []; + + const value = state(1); + const double = derived(() => $.get(value) * 2); + + user_effect(() => { + log.push($.get(double)); + set(value, 10); + }); + + return () => { + flushSync(); + assert.deepEqual(log, [2, 20]); + }; + }); + + test('effect teardown is removed on re-run', () => { + const count = state(0); + let first = true; + let teardown = 0; + + user_effect(() => { + $.get(count); + if (first) { + first = false; + return () => { + teardown += 1; + }; + } + }); + + return () => { + flushSync(() => set(count, 1)); + flushSync(() => set(count, 2)); + assert.equal(teardown, 1); + }; + }); + + test('creating effects within a derived correctly handles ownership', () => { + const log: Array = []; + let a: Value; + let inner: Value; + let outer: Value; + + const destroy = effect_root(() => { + inner = state(0); + outer = state(0); + + render_effect(() => { + a = derived(() => { + log.push('outer', $.get(outer)); + effect(() => { + log.push('inner', $.get(inner)); + }); + return $.get(outer); + }); + }); + }); + + return () => { + flushSync(() => { + $.get(a); + }); + assert.deepEqual(log, ['outer', 0, 'inner', 0]); + log.length = 0; + flushSync(() => { + set(inner, 1); + $.get(a); + }); + assert.deepEqual(log, ['inner', 1]); + log.length = 0; + flushSync(() => { + set(outer, 1); + $.get(a); + }); + assert.deepEqual(log, ['outer', 1, 'inner', 1]); + log.length = 0; + flushSync(() => { + set(inner, 2); + $.get(a); + }); + assert.deepEqual(log, ['inner', 2]); + destroy(); + }; + }); + + test('owned deriveds correctly cleanup when no longer connected to graph', () => { + let a: Derived; + let s = state(0); + + const destroy = effect_root(() => { + render_effect(() => { + a = derived(() => { + $.get(s); + }); + $.get(a); + }); + }); + + return () => { + flushSync(); + assert.equal(a?.deps?.length, 1); + assert.equal(s?.reactions?.length, 1); + destroy(); + assert.equal(s?.reactions, null); + }; + }); + + test('mixed nested deriveds correctly cleanup when no longer connected to graph #1', () => { + let a: Derived; + let b: Derived; + let s = state(0); + + const destroy = effect_root(() => { + render_effect(() => { + a = derived(() => { + b = derived(() => { + $.get(s); + }); + $.untrack(() => { + $.get(b); + }); + $.get(s); + }); + $.get(a); + }); + }); + + return () => { + flushSync(); + assert.equal(a?.deps?.length, 1); + assert.equal(s?.reactions?.length, 1); + destroy(); + assert.equal(s?.reactions, null); + }; + }); + + test('mixed nested deriveds correctly cleanup when no longer connected to graph #2', () => { + let a: Derived; + let b: Derived; + let s = state(0); + + const destroy = effect_root(() => { + render_effect(() => { + a = derived(() => { + b = derived(() => { + $.get(s); + }); + effect_root(() => { + $.get(b); + }); + $.get(s); + }); + $.get(a); + }); + }); + + return () => { + flushSync(); + assert.equal(a?.deps?.length, 1); + assert.equal(s?.reactions?.length, 1); + destroy(); + assert.equal(s?.reactions, null); + }; + }); + + test('mixed nested deriveds correctly cleanup when no longer connected to graph #3', () => { + let a: Derived; + let b: Derived; + let s = state(0); + let logs: any[] = []; + + const destroy = effect_root(() => { + render_effect(() => { + a = derived(() => { + b = derived(() => { + return $.get(s); + }); + effect_root(() => { + $.get(b); + }); + render_effect(() => { + logs.push($.get(b)); + }); + $.get(s); + }); + $.get(a); + }); + }); + + return () => { + flushSync(); + assert.equal(a?.deps?.length, 1); + assert.equal(s?.reactions?.length, 2); + + set(s, 1); + flushSync(); + + assert.deepEqual(logs, [0, 1]); + + destroy(); + assert.equal(s?.reactions, null); + }; + }); + + test('deriveds update upon reconnection #1', () => { + let a = state(false); + let b = state(false); + + let c = derived(() => $.get(a)); + let d = derived(() => $.get(c)); + + let last: Record = {}; + + render_effect(() => { + last = { + a: $.get(a), + b: $.get(b), + c: $.get(c), + d: $.get(a) || $.get(b) ? $.get(d) : null + }; + }); + + return () => { + assert.deepEqual(last, { a: false, b: false, c: false, d: null }); + + flushSync(() => set(a, true)); + flushSync(() => set(b, true)); + assert.deepEqual(last, { a: true, b: true, c: true, d: true }); + + flushSync(() => set(a, false)); + flushSync(() => set(b, false)); + assert.deepEqual(last, { a: false, b: false, c: false, d: null }); + + flushSync(() => set(a, true)); + flushSync(() => set(b, true)); + assert.deepEqual(last, { a: true, b: true, c: true, d: true }); + + flushSync(() => set(a, false)); + flushSync(() => set(b, false)); + assert.deepEqual(last, { a: false, b: false, c: false, d: null }); + + flushSync(() => set(b, true)); + assert.deepEqual(last, { a: false, b: true, c: false, d: false }); + }; + }); + + test('deriveds update upon reconnection #2', () => { + let a = state(false); + let b = state(false); + let c = state(false); + + let d = derived(() => $.get(a) || $.get(b)); + + let branch = ''; + + render_effect(() => { + if ($.get(c) && !$.get(d)) { + branch = 'if'; + } else { + branch = 'else'; + } + }); + + return () => { + assert.deepEqual(branch, 'else'); + + flushSync(() => set(c, true)); + assert.deepEqual(branch, 'if'); + + flushSync(() => set(a, true)); + assert.deepEqual(branch, 'else'); + + set(a, false); + set(b, false); + set(c, false); + flushSync(); + assert.deepEqual(branch, 'else'); + + flushSync(() => set(c, true)); + assert.deepEqual(branch, 'if'); + + flushSync(() => set(b, true)); + assert.deepEqual(branch, 'else'); + + set(a, false); + set(b, false); + set(c, false); + flushSync(); + assert.deepEqual(branch, 'else'); + + flushSync(() => set(c, true)); + assert.deepEqual(branch, 'if'); + }; + }); + + test('deriveds update upon reconnection #3', () => { + let a = state(false); + let b = state(false); + + let c = derived(() => $.get(a) || $.get(b)); + let d = derived(() => $.get(c)); + let e = derived(() => $.get(d)); + + return () => { + const log: string[] = []; + let destroy = effect_root(() => { + render_effect(() => { + $.get(e); + log.push('init'); + }); + }); + destroy(); + + destroy = effect_root(() => { + render_effect(() => { + $.get(e); + log.push('update'); + }); + }); + + assert.deepEqual(log, ['init', 'update']); + + set(a, true); + flushSync(); + + assert.deepEqual(log, ['init', 'update', 'update']); + }; + }); + + test('unowned deriveds are not added as reactions', () => { + var count = state(0); + + function create_derived() { + return derived(() => $.get(count) * 2); + } + + return () => { + let d = create_derived(); + assert.equal($.get(d), 0); + assert.equal(count.reactions, null); + assert.equal(d.deps?.length, 1); + + set(count, 1); + assert.equal($.get(d), 2); + assert.equal(count.reactions, null); + assert.equal(d.deps?.length, 1); + + d = create_derived(); + assert.equal($.get(d), 2); + assert.equal(count.reactions, null); + assert.equal(d.deps?.length, 1); + }; + }); + + test('unowned deriveds are correctly connected and disconnected from the graph', () => { + var count = state(0); + + function create_derived() { + return derived(() => $.get(count) * 2); + } + + return () => { + let d = create_derived(); + + const destroy = effect_root(() => { + render_effect(() => { + assert.equal($.get(d), 0); + }); + }); + + assert.equal($.get(d), 0); + assert.equal(count.reactions?.length, 1); + assert.equal(d.deps?.length, 1); + + set(count, 1); + assert.equal($.get(d), 2); + assert.equal(count.reactions?.length, 1); + assert.equal(d.deps?.length, 1); + + destroy(); + + assert.equal(count.reactions, null); + + set(count, 2); + assert.equal($.get(d), 4); + assert.equal(count.reactions, null); + assert.equal(d.deps?.length, 1); + }; + }); + + test('unowned deriveds dependencies are correctly de-duped', () => { + return () => { + let a = state(0); + let b = state(true); + let c = derived(() => $.get(a)); + let d = derived(() => ($.get(b) ? 1 : $.get(a) + $.get(c) + $.get(a))); + + $.get(d); + + assert.equal(d.deps?.length, 1); + + $.get(d); + + set(a, 1); + set(b, false); + + $.get(d); + + assert.equal(d.deps?.length, 3); + }; + }); + + test('unowned deriveds correctly update', () => { + return () => { + const arr1 = proxy<{ a: number }[]>([]); + const arr2 = proxy([]); + const combined = derived(() => [...arr1, ...arr2]); + const derived_length = derived(() => $.get(combined).length); + + assert.deepEqual($.get(combined), []); + assert.equal($.get(derived_length), 0); + + arr1.push({ a: 1 }); + + assert.deepEqual($.get(combined), [{ a: 1 }]); + assert.equal($.get(derived_length), 1); + }; + }); + + test('deriveds do not depend on state they own', () => { + return () => { + let s; + + const d = derived(() => { + s = state(0); + return $.get(s); + }); + + assert.equal($.get(d), 0); + + set(s!, 1); + assert.equal($.get(d), 0); + }; + }); + + test('effects do depend on state they own', (runes) => { + // This behavior is important for use cases like a Resource class + // which shares its instance between multiple effects and triggers + // rerenders by self-invalidating its state. + const log: number[] = []; + + let count: any; + + if (runes) { + // We will make this the new default behavior once it's stable but until then + // we need to keep the old behavior to not break existing code. + enable_async_mode_flag(); + } + + effect(() => { + if (!count || $.get(count) < 2) { + count ||= state(0); + log.push($.get(count)); + set(count, $.get(count) + 1); + } + }); + + return () => { + try { + flushSync(); + if (runes) { + assert.deepEqual(log, [0, 1]); + } else { + assert.deepEqual(log, [0]); + } + } finally { + disable_async_mode_flag(); + } + }; + }); + + test('nested effects depend on state of upper effects', () => { + const logs: number[] = []; + let raw: Source; + let proxied: { current: number }; + + user_effect(() => { + raw = state(0); + proxied = proxy({ current: 0 }); + + // We need those separate, else one working and rerunning the effect + // could mask the other one not rerunning + user_effect(() => { + logs.push($.get(raw)); + }); + + user_effect(() => { + logs.push(proxied.current); + }); + }); + + return () => { + flushSync(); + set(raw, $.get(raw) + 1); + proxied.current += 1; + flushSync(); + assert.deepEqual(logs, [0, 0, 1, 1]); + }; + }); + + test('nested effects depend on state of upper effects', () => { + const logs: number[] = []; + + user_pre_effect(() => { + const raw = state(0); + const proxied = proxy({ current: 0 }); + + // We need those separate, else one working and rerunning the effect + // could mask the other one not rerunning + user_pre_effect(() => { + logs.push($.get(raw)); + }); + + user_pre_effect(() => { + logs.push(proxied.current); + }); + + // Important so that the updating effect is not running + // together with the reading effects + flushSync(); + + user_pre_effect(() => { + $.untrack(() => { + set(raw, $.get(raw) + 1); + proxied.current += 1; + }); + }); + }); + + return () => { + flushSync(); + assert.deepEqual(logs, [0, 0, 1, 1]); + }; + }); + + test('proxy version state does not trigger self-dependency guard', () => { + return () => { + const s = proxy({ a: { b: 1 } }); + const d = derived(() => snapshot(s)); + + assert.deepEqual($.get(d), s); + }; + }); + + test('set version state does not trigger self-dependency guard', () => { + return () => { + const set = new SvelteSet(); + const d = derived(() => set.has('test')); + + set.add('test'); + assert.equal($.get(d), true); + }; + }); + + test('deriveds read inside the root/branches are cleaned up', () => { + return () => { + const a = state(0); + + const destroy = effect_root(() => { + const b = derived(() => $.get(a)); + $.get(b); + }); + + destroy(); + + assert.deepEqual(a.reactions, null); + }; + }); + + test('deriveds containing effects work correctly', () => { + return () => { + let a = render_effect(() => {}); + let b = state(0); + let c; + let effects: Effect[] = []; + + const destroy = effect_root(() => { + a = render_effect(() => { + c = derived(() => { + effects.push( + effect(() => { + $.get(b); + }) + ); + $.get(b); + }); + $.get(c); + }); + }); + + assert.equal(c!.effects?.length, 1); + assert.equal(a.first, a.last); + + set(b, 1); + + flushSync(); + + assert.equal(c!.effects?.length, 1); + assert.equal(a.first, a.last); + + destroy(); + + assert.equal(a.first, null); + + assert.equal(effects.length, 2); + assert.equal((effects[0].f & DESTROYED) !== 0, true); + assert.equal((effects[1].f & DESTROYED) !== 0, true); + }; + }); + + test("deriveds set after they are DIRTY doesn't get updated on get", () => { + return () => { + const a = state(0); + const b = derived(() => $.get(a)); + + set(b, 1); + assert.equal($.get(b), 1); + + set(a, 2); + assert.equal($.get(b), 2); + set(b, 3); + + assert.equal($.get(b), 3); + }; + }); + + test("unowned deriveds set after they are DIRTY doesn't get updated on get", () => { + return () => { + const a = state(0); + const b = derived(() => $.get(a)); + const c = derived(() => $.get(b)); + + set(b, 1); + assert.equal($.get(c), 1); + + set(a, 2); + + assert.equal($.get(b), 2); + assert.equal($.get(c), 2); + }; + }); + + test('deriveds containing effects work correctly when used with untrack', () => { + return () => { + let a = render_effect(() => {}); + let b = state(0); + let c; + let effects: Effect[] = []; + + const destroy = effect_root(() => { + a = render_effect(() => { + c = derived(() => { + $.untrack(() => { + effects.push( + effect(() => { + $.get(b); + }) + ); + }); + $.get(b); + }); + $.get(c); + }); + }); + + assert.equal(c!.effects?.length, 1); + assert.equal(a.first, a.last); + + set(b, 1); + + flushSync(); + + assert.equal(c!.effects?.length, 1); + assert.equal(a.first, a.last); + + destroy(); + + assert.equal(a.first, null); + + assert.equal(effects.length, 2); + assert.equal((effects[0].f & DESTROYED) !== 0, true); + assert.equal((effects[1].f & DESTROYED) !== 0, true); + }; + }); + + test('bigint states update correctly', () => { + return () => { + const count = state(0n); + + assert.doesNotThrow(() => update(count)); + assert.equal($.get(count), 1n); + assert.doesNotThrow(() => update(count, -1)); + assert.equal($.get(count), 0n); + + assert.doesNotThrow(() => update_pre(count)); + assert.equal($.get(count), 1n); + assert.doesNotThrow(() => update_pre(count, -1)); + assert.equal($.get(count), 0n); + }; + }); + + test('unowned deriveds correctly re-attach to their source', () => { + const log: any[] = []; + + return () => { + const a = state(0); + const b = state(0); + const c = derived(() => { + $.get(a); + return $.get(b); + }); + + $.get(c); + + set(a, 1); + + const destroy = effect_root(() => { + render_effect(() => { + log.push($.get(c)); + }); + }); + + assert.deepEqual(log, [0]); + + set(b, 1); + + flushSync(); + + assert.deepEqual(log, [0, 1]); + + destroy(); + }; + }); + + test('unowned deriveds correctly update', () => { + const log: any[] = []; + + return () => { + const a = state(0); + const b = state(0); + const c = derived(() => { + return $.get(a); + }); + const d = derived(() => { + return $.get(b); + }); + + const destroy = effect_root(() => { + const e = derived(() => { + return $.get(c) === 1 && $.get(d) === 1; + }); + render_effect(() => { + log.push($.get(e)); + }); + }); + + assert.deepEqual(log, [false]); + + set(a, 1); + set(b, 1); + + flushSync(); + + assert.deepEqual(log, [false, true]); + + set(b, 9); + + flushSync(); + + assert.deepEqual(log, [false, true, false]); + + destroy(); + }; + }); + + test('derived whose original parent effect has been destroyed no longer updates', () => { + return () => { + const warn = console.warn; + const warnings: string[] = []; + console.warn = (...args) => warnings.push(...args); + + let count: Source; + let double: Derived; + const destroy = effect_root(() => { + render_effect(() => { + count = state(0); + double = derived(() => $.get(count) * 2); + }); + }); + + flushSync(); + assert.equal($.get(double!), 0); + flushSync(); + + set(count!, 1); + flushSync(); + assert.equal($.get(double!), 2); + + destroy(); + + assert.equal($.get(double!), 2); + assert.equal(warnings.length, 0); // value is unchanged, no warning yet + + set(count!, 2); + flushSync(); + assert.equal($.get(double!), 2); + assert.ok(warnings.some((str) => str.includes('derived_inert'))); + + console.warn = warn; + }; + }); + + test('derived when connected should add new dependency to its reaction even when read outside effect', () => { + let count_a = state(0); + let count_b = state(0); + let which = state(true); + let double = derived(() => ($.get(which) ? $.get(count_a) * 2 : $.get(count_b) * 2)); + + render_effect(() => { + $.get(double); + }); + + return () => { + flushSync(); + assert.equal($.get(double!), 0); + + set(which, false); + $.get(double); // read before render effect has a chance to rerun + flushSync(); + assert.equal($.get(double!), 0); + + set(count_b, 1); + flushSync(); + assert.equal($.get(double!), 2); + }; + }); + + test('$effect.root inside deriveds stay alive independently', () => { + const log: any[] = []; + const c = state(0); + const cleanup: any[] = []; + const inner_states: any[] = []; + + const d = derived(() => { + const destroy = effect_root(() => { + const x = state(0); + inner_states.push(x); + + effect(() => { + log.push('inner ' + $.get(x)); + return () => { + log.push('inner destroyed'); + }; + }); + }); + + cleanup.push(destroy); + + return $.get(c); + }); + + return () => { + log.push($.get(d)); + flushSync(); + + assert.deepEqual(log, [0, 'inner 0']); + log.length = 0; + + set(inner_states[0], 1); + flushSync(); + + assert.deepEqual(log, ['inner destroyed', 'inner 1']); + log.length = 0; + + set(c, 1); + log.push($.get(d)); + flushSync(); + + assert.deepEqual(log, [1, 'inner 0']); + log.length = 0; + + cleanup.forEach((fn) => fn()); + flushSync(); + + assert.deepEqual(log, ['inner destroyed', 'inner destroyed']); + }; + }); + + test('derived read in an untracked context should not leak in deps reactions', () => { + return () => { + let s = state('hello'); + let a = derived(() => $.get(s)); + let b = derived(() => $.get(a)); + + let destroy = effect_root(() => { + $.get(b); + }); + + destroy(); + + // a was spuriously added to s.reactions + // even though the entire derived chain was read in an untracked context + assert.equal(s.reactions, null); + }; + }); + + test('untracked derived reads inside effects do not reconnect disconnected dependencies', () => { + return () => { + const source = state({ n: 1, items: [1] }); + const data = derived(() => $.get(source)); + const items = derived(() => $.get(data).items); + const count = derived(() => Math.max(1, $.get(items).length)); + const snapshot = derived(() => ({ n: $.get(data).n, count: $.get(count) })); + const show = state(true); + const trigger = state(0); + let rendered = -1; + let seen: { n: number; count: number } | undefined; + + const destroy = effect_root(() => { + render_effect(() => { + if ($.get(show)) { + render_effect(() => { + rendered = $.get(snapshot).count; + }); + } + }); + + render_effect(() => { + $.get(trigger); + seen = $.untrack(() => $.get(snapshot)); + }); + }); + + flushSync(); + assert.equal(rendered, 1); + + flushSync(() => set(show, false)); + assert.equal(source.reactions, null); + + flushSync(() => set(source, { n: 2, items: [1, 2] })); + flushSync(() => set(trigger, 1)); + + assert.deepEqual(seen, { n: 2, count: 2 }); + assert.equal(source.reactions, null); + assert.equal(items.reactions, null); + assert.equal(count.reactions, null); + assert.equal(items.f & CONNECTED, 0); + assert.equal(count.f & CONNECTED, 0); + + flushSync(() => set(show, true)); + assert.equal(rendered, 2); + assert.equal(source.reactions?.length, 1); + + flushSync(() => set(source, { n: 3, items: [1] })); + assert.equal(rendered, 1); + + destroy(); + flushSync(); + assert.equal(source.reactions, null); + }; + }); + + // https://github.com/sveltejs/svelte/issues/18414 + test('a reaction that throws after first-reading a fresh derived does not leak it', () => { + const src = state(0); + const pane = derived(() => $.get(src)); + const base = derived(() => $.get(pane) + ':base'); + const extra = derived(() => $.get(pane) + ':extra'); + const flag = state(false); + + const destroy = effect_root(() => { + render_effect(() => { + if ($.get(flag)) { + $.get(extra); + throw new Error('render boom'); + } else { + $.get(base); + } + }); + }); + + return () => { + try { + flushSync(() => set(flag, true)); + } catch {} + + destroy(); + + assert.equal(src.reactions, null); + }; + }); + + test('a derived that throws on its first run re-runs when its dependencies change', () => { + const s = state(0); + const fn = () => { + if ($.get(s) === 0) throw new Error('boom'); + return $.get(s); + }; + const owned = derived(fn); + const previous_effect = $.active_effect; + $.set_active_effect(null); + const unowned = derived(fn); + $.set_active_effect(previous_effect); + const log: any[] = []; + + const destroy = effect_root(() => { + render_effect(() => { + for (const d of [owned, unowned]) { + try { + log.push($.get(d)); + } catch { + log.push('error'); + } + } + }); + }); + + return () => { + assert.notEqual(owned.parent, null); + assert.equal(unowned.parent, null); + + flushSync(); + assert.deepEqual(log, ['error', 'error']); + + flushSync(() => set(s, 1)); + assert.deepEqual(log, ['error', 'error', 1, 1]); + + destroy(); + assert.equal(s.reactions, null); + }; + }); + + // https://github.com/sveltejs/svelte/issues/16814 + it('does not treat values created for inactive reactions as current', () => { + push({}, true); + + const trigger = state(false); + const value = state(0); + const log: number[] = []; + let inactive_reaction: Effect; + + const destroy = effect_root(() => { + render_effect(() => { + $.get(trigger); + inactive_reaction = $.active_reaction as Effect; + }); + + render_effect(() => { + $.get(trigger); + + const reaction = $.active_reaction; + $.set_active_reaction(inactive_reaction); + $.push_reaction_value(value); + $.set_active_reaction(reaction); + + log.push($.get(value)); + }); + }); + + try { + flushSync(); + flushSync(() => set(trigger, true)); + flushSync(() => set(value, 1)); + + assert.deepEqual(log, [0, 0, 1]); + } finally { + destroy(); + pop(); + } + }); +});