From ed802760ed16329cf8ad8acdaa48e58823741048 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 19 Dec 2024 23:40:09 +0000 Subject: [PATCH 1/6] fix: make untrack behave correctly in relation to mutations --- .changeset/strong-cows-jump.md | 5 +++ packages/svelte/src/index-client.js | 3 +- packages/svelte/src/index-server.js | 3 +- .../src/internal/client/reactivity/effects.js | 5 ++- .../src/internal/client/reactivity/sources.js | 4 +- .../svelte/src/internal/client/runtime.js | 37 +++++++++++++++++-- packages/svelte/src/store/utils.js | 4 +- .../samples/derived-map/main.svelte | 6 +-- .../each-block-default-arg/main.svelte | 4 +- .../samples/snippet-default-arg/main.svelte | 4 +- packages/svelte/types/index.d.ts | 7 ++++ 11 files changed, 64 insertions(+), 18 deletions(-) create mode 100644 .changeset/strong-cows-jump.md diff --git a/.changeset/strong-cows-jump.md b/.changeset/strong-cows-jump.md new file mode 100644 index 000000000000..600352e584d4 --- /dev/null +++ b/.changeset/strong-cows-jump.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: make untrack behave correctly in relation to mutations diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 587d76623331..bd47c4398c7c 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -187,7 +187,8 @@ export { hasContext, setContext, tick, - untrack + untrack, + unsafe } from './internal/client/runtime.js'; export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index 0f1aff8f5aa7..9868120729fd 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -14,7 +14,8 @@ export { noop as afterUpdate, noop as onMount, noop as flushSync, - run as untrack + run as untrack, + run as unsafe } from './internal/shared/utils.js'; export function createEventDispatcher() { diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index bf890627f7e0..24b36ce90bc9 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -16,7 +16,8 @@ import { set_is_flushing_effect, set_signal_status, untrack, - skip_reaction + skip_reaction, + untracking } from '../runtime.js'; import { DIRTY, @@ -164,7 +165,7 @@ function create_effect(type, fn, sync, push = true) { * @returns {boolean} */ export function effect_tracking() { - if (active_reaction === null) { + if (active_reaction === null || untracking) { return false; } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 3e8c4a00c833..921151cfa9a6 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -18,7 +18,8 @@ import { set_derived_sources, check_dirtiness, set_is_flushing_effect, - is_flushing_effect + is_flushing_effect, + unsafe_mutations } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import { @@ -147,6 +148,7 @@ export function mutate(source, value) { export function set(source, value) { if ( active_reaction !== null && + !unsafe_mutations && is_runes() && (active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 && // If the source was created locally within the current derived, then diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 4a90a219712f..9dd18a69488b 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -78,6 +78,10 @@ let dev_effect_stack = []; /** @type {null | Reaction} */ export let active_reaction = null; +export let untracking = false; + +export let unsafe_mutations = false; + /** @param {null | Reaction} reaction */ export function set_active_reaction(reaction) { active_reaction = reaction; @@ -387,6 +391,8 @@ export function update_reaction(reaction) { var previous_skip_reaction = skip_reaction; var prev_derived_sources = derived_sources; var previous_component_context = component_context; + var previous_untracking = untracking; + var previous_unsafe_mutations = unsafe_mutations; var flags = reaction.f; new_deps = /** @type {null | Value[]} */ (null); @@ -396,6 +402,8 @@ export function update_reaction(reaction) { skip_reaction = !is_flushing_effect && (flags & UNOWNED) !== 0; derived_sources = null; component_context = reaction.ctx; + untracking = false; + unsafe_mutations = false; try { var result = /** @type {Function} */ (0, reaction.fn)(); @@ -434,6 +442,8 @@ export function update_reaction(reaction) { skip_reaction = previous_skip_reaction; derived_sources = prev_derived_sources; component_context = previous_component_context; + untracking = previous_untracking; + unsafe_mutations = previous_unsafe_mutations; } } @@ -856,7 +866,7 @@ export function get(signal) { } // Register the dependency on the current reaction signal. - if (active_reaction !== null) { + if (active_reaction !== null && !untracking) { if (derived_sources !== null && derived_sources.includes(signal)) { e.state_unsafe_local_read(); } @@ -1016,12 +1026,31 @@ export function invalidate_inner_signals(fn) { * @returns {T} */ export function untrack(fn) { - const previous_reaction = active_reaction; + var previous_untracking = untracking; try { - active_reaction = null; + untracking = true; return fn(); } finally { - active_reaction = previous_reaction; + untracking = previous_untracking; + } +} + +/** + * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived), + * any state updates to state is allowed. + * + * ``` + * @template T + * @param {() => T} fn + * @returns {T} + */ +export function unsafe(fn) { + var previous_unsafe_mutations = unsafe_mutations; + try { + unsafe_mutations = true; + return fn(); + } finally { + unsafe_mutations = previous_unsafe_mutations; } } diff --git a/packages/svelte/src/store/utils.js b/packages/svelte/src/store/utils.js index db2a62c68c01..4cdd49a002d2 100644 --- a/packages/svelte/src/store/utils.js +++ b/packages/svelte/src/store/utils.js @@ -1,5 +1,5 @@ /** @import { Readable } from './public' */ -import { untrack } from '../index-client.js'; +import { unsafe } from '../index-client.js'; import { noop } from '../internal/shared/utils.js'; /** @@ -22,7 +22,7 @@ export function subscribe_to_store(store, run, invalidate) { // Svelte store takes a private second argument // StartStopNotifier could mutate state, and we want to silence the corresponding validation error - const unsub = untrack(() => + const unsub = unsafe(() => store.subscribe( run, // @ts-expect-error diff --git a/packages/svelte/tests/runtime-runes/samples/derived-map/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-map/main.svelte index ea51f29dfb30..e5f3798948c3 100644 --- a/packages/svelte/tests/runtime-runes/samples/derived-map/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/derived-map/main.svelte @@ -1,5 +1,5 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/each-block-default-arg/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-block-default-arg/main.svelte index b9e77c47954b..76e287f28d76 100644 --- a/packages/svelte/tests/runtime-runes/samples/each-block-default-arg/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/each-block-default-arg/main.svelte @@ -1,9 +1,9 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-default-arg/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-default-arg/main.svelte index dc39503e3a94..3bd205b4c756 100644 --- a/packages/svelte/tests/runtime-runes/samples/snippet-default-arg/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/snippet-default-arg/main.svelte @@ -1,9 +1,9 @@ diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index d422abebbc0f..68cf4fde8bb5 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -484,6 +484,13 @@ declare module 'svelte' { * ``` * */ export function untrack(fn: () => T): T; + /** + * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived), + * any state updates to state is allowed. + * + * ``` + * */ + export function unsafe(fn: () => T): T; /** * Retrieves the context that belongs to the closest parent component with the specified `key`. * Must be called during component initialisation. From 1941e82238b650087f182bc0950019db78bdbc10 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 19 Dec 2024 23:46:28 +0000 Subject: [PATCH 2/6] format --- packages/svelte/src/internal/client/runtime.js | 1 - packages/svelte/types/index.d.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 9dd18a69488b..476f3220cc81 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1039,7 +1039,6 @@ export function untrack(fn) { * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived), * any state updates to state is allowed. * - * ``` * @template T * @param {() => T} fn * @returns {T} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 68cf4fde8bb5..d126945d910f 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -488,7 +488,6 @@ declare module 'svelte' { * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived), * any state updates to state is allowed. * - * ``` * */ export function unsafe(fn: () => T): T; /** From 3614f3717767cf27b25ad00ea6f060cbf209663b Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 20 Dec 2024 14:03:11 +0000 Subject: [PATCH 3/6] consistency fix --- .../svelte/src/internal/client/runtime.js | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 476f3220cc81..cbdbc26196cf 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -82,6 +82,8 @@ export let untracking = false; export let unsafe_mutations = false; +export let unsafe_mutation_inside_effect = false; + /** @param {null | Reaction} reaction */ export function set_active_reaction(reaction) { active_reaction = reaction; @@ -517,8 +519,10 @@ export function update_effect(effect) { var previous_effect = active_effect; var previous_component_context = component_context; + var previous_unsafe_mutation_inside_effect = unsafe_mutation_inside_effect; active_effect = effect; + unsafe_mutation_inside_effect = false; if (DEV) { var previous_component_fn = dev_current_component_function; @@ -536,6 +540,18 @@ export function update_effect(effect) { execute_effect_teardown(effect); var teardown = update_reaction(effect); effect.teardown = typeof teardown === 'function' ? teardown : null; + + // If unsafe() has been used within the effect, then we might need + // to schedule another update for this effect in case the unsafe mutation + // has caused a this effect to become invalidated again + if (unsafe_mutation_inside_effect && (effect.f & CLEAN) !== 0) { + set_signal_status(effect, MAYBE_DIRTY); + if (check_dirtiness(effect)) { + set_signal_status(effect, DIRTY); + schedule_effect(effect); + } + } + effect.version = current_version; if (DEV) { @@ -545,6 +561,7 @@ export function update_effect(effect) { handle_error(error, effect, previous_effect, previous_component_context || effect.ctx); } finally { active_effect = previous_effect; + unsafe_mutation_inside_effect = previous_unsafe_mutation_inside_effect; if (DEV) { dev_current_component_function = previous_component_fn; @@ -1047,7 +1064,13 @@ export function unsafe(fn) { var previous_unsafe_mutations = unsafe_mutations; try { unsafe_mutations = true; - return fn(); + var val = fn(); + + if (is_flushing_effect) { + unsafe_mutation_inside_effect = true; + } + + return val; } finally { unsafe_mutations = previous_unsafe_mutations; } From 8bf08ff422dc663c54eddbc84af276d20d23e52d Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 20 Dec 2024 14:30:53 +0000 Subject: [PATCH 4/6] consistency fix 2 --- packages/svelte/src/internal/client/runtime.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index cbdbc26196cf..1db9c4abac67 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -539,12 +539,13 @@ export function update_effect(effect) { execute_effect_teardown(effect); var teardown = update_reaction(effect); + var is_init = effect.version === 0; effect.teardown = typeof teardown === 'function' ? teardown : null; - // If unsafe() has been used within the effect, then we might need - // to schedule another update for this effect in case the unsafe mutation - // has caused a this effect to become invalidated again - if (unsafe_mutation_inside_effect && (effect.f & CLEAN) !== 0) { + // If unsafe() has been used within the effect on the first time + // it's run, then we might need to schedule the effect to run again + // if it has a dependency that has been invalidated + if (unsafe_mutation_inside_effect && is_init && (effect.f & CLEAN) !== 0) { set_signal_status(effect, MAYBE_DIRTY); if (check_dirtiness(effect)) { set_signal_status(effect, DIRTY); From 3221d652fdf2fc3ffeb1c1b88a828002ef9c0ff0 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 20 Dec 2024 20:47:17 +0000 Subject: [PATCH 5/6] consistency better fix --- .../src/internal/client/reactivity/sources.js | 16 ++++-- .../svelte/src/internal/client/runtime.js | 53 +++++++++---------- 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 921151cfa9a6..2788ac19278f 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -19,7 +19,9 @@ import { check_dirtiness, set_is_flushing_effect, is_flushing_effect, - unsafe_mutations + unsafe_mutating, + unsafe_sources, + set_unsafe_sources } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import { @@ -148,7 +150,7 @@ export function mutate(source, value) { export function set(source, value) { if ( active_reaction !== null && - !unsafe_mutations && + !unsafe_mutating && is_runes() && (active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 && // If the source was created locally within the current derived, then @@ -172,6 +174,14 @@ export function internal_set(source, value) { source.v = value; source.version = increment_version(); + if (unsafe_mutating) { + if (unsafe_sources === null) { + set_unsafe_sources([source]); + } else { + unsafe_sources.push(source); + } + } + if (DEV && tracing_mode_flag) { source.updated = get_stack('UpdatedAt'); } @@ -231,7 +241,7 @@ export function internal_set(source, value) { * @param {number} status should be DIRTY or MAYBE_DIRTY * @returns {void} */ -function mark_reactions(signal, status) { +export function mark_reactions(signal, status) { var reactions = signal.reactions; if (reactions === null) return; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 1db9c4abac67..8c856b635fef 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -29,7 +29,7 @@ import { } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { add_owner } from './dev/ownership.js'; -import { internal_set, set, source } from './reactivity/sources.js'; +import { internal_set, mark_reactions, set, source } from './reactivity/sources.js'; import { destroy_derived, execute_derived, update_derived } from './reactivity/deriveds.js'; import * as e from './errors.js'; import { lifecycle_outside_component } from '../shared/errors.js'; @@ -80,9 +80,15 @@ export let active_reaction = null; export let untracking = false; -export let unsafe_mutations = false; +export let unsafe_mutating = false; -export let unsafe_mutation_inside_effect = false; +/** @type {null | Source[]} */ +export let unsafe_sources = null; + +/** @param {Source[] | null} value */ +export function set_unsafe_sources(value) { + unsafe_sources = value; +} /** @param {null | Reaction} reaction */ export function set_active_reaction(reaction) { @@ -394,7 +400,7 @@ export function update_reaction(reaction) { var prev_derived_sources = derived_sources; var previous_component_context = component_context; var previous_untracking = untracking; - var previous_unsafe_mutations = unsafe_mutations; + var previous_unsafe_mutating = unsafe_mutating; var flags = reaction.f; new_deps = /** @type {null | Value[]} */ (null); @@ -405,7 +411,7 @@ export function update_reaction(reaction) { derived_sources = null; component_context = reaction.ctx; untracking = false; - unsafe_mutations = false; + unsafe_mutating = false; try { var result = /** @type {Function} */ (0, reaction.fn)(); @@ -445,7 +451,7 @@ export function update_reaction(reaction) { derived_sources = prev_derived_sources; component_context = previous_component_context; untracking = previous_untracking; - unsafe_mutations = previous_unsafe_mutations; + unsafe_mutating = previous_unsafe_mutating; } } @@ -519,10 +525,10 @@ export function update_effect(effect) { var previous_effect = active_effect; var previous_component_context = component_context; - var previous_unsafe_mutation_inside_effect = unsafe_mutation_inside_effect; + var previous_unsafe_sources = unsafe_sources; active_effect = effect; - unsafe_mutation_inside_effect = false; + unsafe_sources = null; if (DEV) { var previous_component_fn = dev_current_component_function; @@ -539,17 +545,14 @@ export function update_effect(effect) { execute_effect_teardown(effect); var teardown = update_reaction(effect); - var is_init = effect.version === 0; effect.teardown = typeof teardown === 'function' ? teardown : null; - // If unsafe() has been used within the effect on the first time - // it's run, then we might need to schedule the effect to run again - // if it has a dependency that has been invalidated - if (unsafe_mutation_inside_effect && is_init && (effect.f & CLEAN) !== 0) { - set_signal_status(effect, MAYBE_DIRTY); - if (check_dirtiness(effect)) { - set_signal_status(effect, DIRTY); - schedule_effect(effect); + // If unsafe() has been used within the effect then we will need + // to re-invalidate any unsafe sources that were already invalidated + // to ensure consistency of the graph + if (unsafe_sources !== null && (effect.f & CLEAN) !== 0) { + for (let i = 0; i < /** @type {Source[]} */ (unsafe_sources).length; i++) { + mark_reactions(unsafe_sources[i], DIRTY); } } @@ -562,7 +565,7 @@ export function update_effect(effect) { handle_error(error, effect, previous_effect, previous_component_context || effect.ctx); } finally { active_effect = previous_effect; - unsafe_mutation_inside_effect = previous_unsafe_mutation_inside_effect; + unsafe_sources = previous_unsafe_sources; if (DEV) { dev_current_component_function = previous_component_fn; @@ -1062,18 +1065,12 @@ export function untrack(fn) { * @returns {T} */ export function unsafe(fn) { - var previous_unsafe_mutations = unsafe_mutations; + var previous_unsafe_mutating = unsafe_mutating; try { - unsafe_mutations = true; - var val = fn(); - - if (is_flushing_effect) { - unsafe_mutation_inside_effect = true; - } - - return val; + unsafe_mutating = true; + return fn(); } finally { - unsafe_mutations = previous_unsafe_mutations; + unsafe_mutating = previous_unsafe_mutating; } } From 89780e38e8504640ed8ac9ff049f7ace085b371c Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 20 Dec 2024 21:31:11 +0000 Subject: [PATCH 6/6] add tests --- .../src/internal/client/reactivity/sources.js | 17 +-- .../svelte/src/internal/client/runtime.js | 2 +- packages/svelte/tests/signals/test.ts | 111 ++++++++++++++++++ 3 files changed, 122 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 2788ac19278f..4a1e6831d367 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -239,9 +239,10 @@ export function internal_set(source, value) { /** * @param {Value} signal * @param {number} status should be DIRTY or MAYBE_DIRTY + * @param {boolean} [unsafe] mark all reactions for unsafe mutations * @returns {void} */ -export function mark_reactions(signal, status) { +export function mark_reactions(signal, status, unsafe = false) { var reactions = signal.reactions; if (reactions === null) return; @@ -253,23 +254,25 @@ export function mark_reactions(signal, status) { var flags = reaction.f; // Skip any effects that are already dirty - if ((flags & DIRTY) !== 0) continue; + if ((flags & DIRTY) !== 0 && !unsafe) continue; // In legacy mode, skip the current effect to prevent infinite loops if (!runes && reaction === active_effect) continue; - // Inspect effects need to run immediately, so that the stack trace makes sense - if (DEV && (flags & INSPECT_EFFECT) !== 0) { + // Inspect effects need to run immediately, so that the stack trace makes sense. + // Skip doing this for the unsafe mutations as they will have already been added + // in the unsafe() wrapper + if (DEV && !unsafe && (flags & INSPECT_EFFECT) !== 0) { inspect_effects.add(reaction); continue; } set_signal_status(reaction, status); - // If the signal a) was previously clean or b) is an unowned derived, then mark it - if ((flags & (CLEAN | UNOWNED)) !== 0) { + // If the signal a) was previously clean or b) is an unowned derived then mark it + if ((flags & (CLEAN | UNOWNED)) !== 0 || unsafe) { if ((flags & DERIVED) !== 0) { - mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); + mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY, unsafe); } else { schedule_effect(/** @type {Effect} */ (reaction)); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 8c856b635fef..f1f2baf6e35b 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -552,7 +552,7 @@ export function update_effect(effect) { // to ensure consistency of the graph if (unsafe_sources !== null && (effect.f & CLEAN) !== 0) { for (let i = 0; i < /** @type {Source[]} */ (unsafe_sources).length; i++) { - mark_reactions(unsafe_sources[i], DIRTY); + mark_reactions(unsafe_sources[i], DIRTY, true); } } diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 6796655cc8d2..544868b17f04 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -781,4 +781,115 @@ describe('signals', () => { assert.equal($.get(count), 0n); }; }); + + test('unsafe() correctly ensures graph consistency', () => { + return () => { + const output: any[] = []; + + const destroy = effect_root(() => { + const a = state(0); + const b = state(0); + const c = derived(() => { + $.unsafe(() => { + set(b, $.get(a) + 1); + }); + return $.get(a); + }); + + render_effect(() => { + output.push('b' + $.get(b)); + }); + + render_effect(() => { + output.push('b' + $.get(b), 'c' + $.get(c)); + }); + + flushSync(); + + set(a, 1); + + flushSync(); + }); + + destroy(); + + assert.deepEqual(output, ['b0', 'b0', 'c0', 'b1', 'b1', 'c0', 'b2', 'c1', 'b2']); + }; + }); + + test('unsafe() correctly ensures graph consistency #2', () => { + return () => { + const output: any[] = []; + + const destroy = effect_root(() => { + const a = state(0); + const b = state(0); + const c = derived(() => { + $.unsafe(() => { + set(b, $.get(a) + 1); + }); + return $.get(a); + }); + let d = derived(() => $.get(b)); + + render_effect(() => { + output.push('d' + $.get(d)); + }); + + render_effect(() => { + output.push('d' + $.get(d), 'c' + $.get(c)); + }); + + flushSync(); + + set(a, 1); + + flushSync(); + }); + + destroy(); + + assert.deepEqual(output, ['d0', 'd0', 'c0', 'd1', 'd1', 'c0', 'd2', 'c1', 'd2']); + }; + }); + + test('unsafe() correctly ensures graph consistency #3', () => { + return () => { + const output: any[] = []; + + const destroy = effect_root(() => { + const a = state(0); + const b = state(0); + const c = derived(() => { + $.unsafe(() => { + set(b, $.get(a) + 1); + }); + return $.get(a); + }); + let d = state(true); + let e = derived(() => $.get(b)); + + render_effect(() => { + if ($.get(d)) { + return; + } + output.push('e' + $.get(e), 'c' + $.get(c)); + }); + + flushSync(); + + set(d, false); + + flushSync(); + + set(a, 1); + + flushSync(); + }); + + destroy(); + + assert.deepEqual(output, ['e0', 'c0', 'e1', 'c0', 'e2', 'c1']); + }; + }); });