diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 2788ac1927..4a1e6831d3 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 8c856b635f..f1f2baf6e3 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 6796655cc8..544868b17f 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']); + }; + }); });