diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 9b80aa2cea..afe2e403ce 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -15,14 +15,13 @@ import { } from '../../hydration.js'; import { empty, map_get, map_set } from '../../operations.js'; import { insert, remove } from '../../reconciler.js'; -import { set } from '../../runtime.js'; import { destroy_effect, pause_effect, render_effect, resume_effect } from '../../reactivity/effects.js'; -import { source, mutable_source } from '../../reactivity/sources.js'; +import { source, mutable_source, set } from '../../reactivity/sources.js'; import { is_array } from '../../utils.js'; import { BRANCH_EFFECT } from '../../constants.js'; diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index cbe981ef66..5fc57ae280 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -1,7 +1,6 @@ import { DEV } from 'esm-env'; import { get, - set, updating_derived, batch_inspect, current_component_context, @@ -19,7 +18,7 @@ import { object_prototype } from './utils.js'; import { add_owner, check_ownership, strip_owner } from './dev/ownership.js'; -import { mutable_source, source } from './reactivity/sources.js'; +import { mutable_source, source, set } from './reactivity/sources.js'; import { STATE_SYMBOL, UNINITIALIZED } from './constants.js'; /** diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index cc4b9e5b57..0afc654880 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -1,7 +1,26 @@ import { DEV } from 'esm-env'; -import { current_component_context } from '../runtime.js'; +import { + current_component_context, + current_consumer, + current_dependencies, + current_effect, + current_untracked_writes, + current_untracking, + flushSync, + get, + ignore_mutation_validation, + is_batching_effect, + is_runes, + last_inspected_signal, + mark_signal_consumers, + schedule_effect, + set_current_untracked_writes, + set_last_inspected_signal, + set_signal_status, + untrack +} from '../runtime.js'; import { default_equals, safe_equal } from './equality.js'; -import { CLEAN, SOURCE } from '../constants.js'; +import { CLEAN, DERIVED, DIRTY, MANAGED, SOURCE } from '../constants.js'; /** * @template V @@ -44,3 +63,100 @@ export function mutable_source(initial_value) { return s; } + +/** + * @template V + * @param {import('#client').Source} signal + * @param {V} value + * @returns {V} + */ +export function set(signal, value) { + if ( + !current_untracking && + !ignore_mutation_validation && + current_consumer !== null && + is_runes(null) && + (current_consumer.f & DERIVED) !== 0 + ) { + throw new Error( + 'ERR_SVELTE_UNSAFE_MUTATION' + + (DEV + ? ": Unsafe mutations during Svelte's render or derived phase are not permitted in runes mode. " + + 'This can lead to unexpected errors and possibly cause infinite loops.\n\nIf this mutation is not meant ' + + 'to be reactive do not use the "$state" rune for that declaration.' + : '') + ); + } + + if (!signal.eq(value, signal.v)) { + signal.v = value; + + // Increment write version so that unowned signals can properly track dirtyness + signal.w++; + + // If the current signal is running for the first time, it won't have any + // consumers as we only allocate and assign the consumers after the signal + // has fully executed. So in the case of ensuring it registers the consumer + // properly for itself, we need to ensure the current effect actually gets + // scheduled. i.e: + // + // $effect(() => x++) + // + // We additionally want to skip this logic for when ignore_mutation_validation is + // true, as stores write to source signal on initialization. + if ( + is_runes(null) && + !ignore_mutation_validation && + current_effect !== null && + (current_effect.f & CLEAN) !== 0 && + (current_effect.f & MANAGED) === 0 + ) { + if (current_dependencies !== null && current_dependencies.includes(signal)) { + set_signal_status(current_effect, DIRTY); + schedule_effect(current_effect, false); + } else { + if (current_untracked_writes === null) { + set_current_untracked_writes([signal]); + } else { + current_untracked_writes.push(signal); + } + } + } + + mark_signal_consumers(signal, DIRTY, true); + + // @ts-expect-error + if (DEV && signal.inspect) { + if (is_batching_effect) { + set_last_inspected_signal(/** @type {import('#client').SourceDebug} */ (signal)); + } else { + for (const fn of /** @type {import('#client').SourceDebug} */ (signal).inspect) fn(); + } + } + } + + return value; +} + +/** + * @template V + * @param {import('#client').Source} signal + * @param {V} value + * @returns {void} + */ +export function set_sync(signal, value) { + flushSync(() => set(signal, value)); +} + +/** + * @template V + * @param {import('#client').Source} source + * @param {V} value + */ +export function mutate(source, value) { + set( + source, + untrack(() => get(source)) + ); + return value; +} diff --git a/packages/svelte/src/internal/client/reactivity/store.js b/packages/svelte/src/internal/client/reactivity/store.js index 9e0999053b..44b25ce636 100644 --- a/packages/svelte/src/internal/client/reactivity/store.js +++ b/packages/svelte/src/internal/client/reactivity/store.js @@ -1,9 +1,9 @@ import { subscribe_to_store } from '../../../store/utils.js'; import { noop } from '../../common.js'; import { UNINITIALIZED } from '../constants.js'; -import { get, set, set_ignore_mutation_validation, untrack } from '../runtime.js'; +import { get, set_ignore_mutation_validation, untrack } from '../runtime.js'; import { user_effect } from './effects.js'; -import { mutable_source } from './sources.js'; +import { mutable_source, set } from './sources.js'; /** * Gets the current value of a store. If the store isn't subscribed to yet, it will create a proxy diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 707a642ea3..fee89913fd 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -35,7 +35,6 @@ import { current_component_context, deep_read, get, - set, is_signals_recorded, inspect_fn, current_effect @@ -66,7 +65,7 @@ import { } from './utils.js'; import { run } from '../common.js'; import { bind_transition } from './transitions.js'; -import { mutable_source, source } from './reactivity/sources.js'; +import { mutable_source, source, set } from './reactivity/sources.js'; import { safe_not_equal } from './reactivity/equality.js'; import { derived, derived_safe_equal } from './reactivity/deriveds.js'; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 47b9dac584..ee303b2078 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -28,6 +28,7 @@ import { BRANCH_EFFECT } from './constants.js'; import { flush_tasks } from './dom/task.js'; +import { mutate, set } from './reactivity/sources.js'; const IS_EFFECT = EFFECT | PRE_EFFECT | RENDER_EFFECT; @@ -66,20 +67,33 @@ export function set_current_effect(effect) { } /** @type {null | import('#client').ValueSignal[]} */ -let current_dependencies = null; +export let current_dependencies = null; let current_dependencies_index = 0; + /** * Tracks writes that the effect it's executed in doesn't listen to yet, * so that the dependency can be added to the effect later on if it then reads it * @type {null | import('#client').Source[]} */ -let current_untracked_writes = null; +export let current_untracked_writes = null; + +/** @param {null | import('#client').Source[]} writes */ +export function set_current_untracked_writes(writes) { + current_untracked_writes = writes; +} + /** @type {null | import('#client').SourceDebug} */ -let last_inspected_signal = null; +export let last_inspected_signal = null; + +/** @param {import('#client').SourceDebug} signal */ +export function set_last_inspected_signal(signal) { + last_inspected_signal = signal; +} + /** If `true`, `get`ting the signal should not register it as a dependency */ export let current_untracking = false; /** Exists to opt out of the mutation validation for stores which may be set for the first time during a derivation */ -let ignore_mutation_validation = false; +export let ignore_mutation_validation = false; /** @param {boolean} value */ export function set_ignore_mutation_validation(value) { ignore_mutation_validation = value; @@ -113,7 +127,7 @@ export let updating_derived = false; * @param {null | import('#client').ComponentContext} context * @returns {boolean} */ -function is_runes(context) { +export function is_runes(context) { const component_context = context || current_component_context; return component_context !== null && component_context.r; } @@ -751,90 +765,6 @@ export function get(signal) { return signal.v; } -/** - * @template V - * @param {import('#client').Source} signal - * @param {V} value - * @returns {V} - */ -export function set(signal, value) { - if ( - !current_untracking && - !ignore_mutation_validation && - current_consumer !== null && - is_runes(null) && - (current_consumer.f & DERIVED) !== 0 - ) { - throw new Error( - 'ERR_SVELTE_UNSAFE_MUTATION' + - (DEV - ? ": Unsafe mutations during Svelte's render or derived phase are not permitted in runes mode. " + - 'This can lead to unexpected errors and possibly cause infinite loops.\n\nIf this mutation is not meant ' + - 'to be reactive do not use the "$state" rune for that declaration.' - : '') - ); - } - - if (!signal.eq(value, signal.v)) { - signal.v = value; - - // Increment write version so that unowned signals can properly track dirtyness - signal.w++; - - // If the current signal is running for the first time, it won't have any - // consumers as we only allocate and assign the consumers after the signal - // has fully executed. So in the case of ensuring it registers the consumer - // properly for itself, we need to ensure the current effect actually gets - // scheduled. i.e: - // - // $effect(() => x++) - // - // We additionally want to skip this logic for when ignore_mutation_validation is - // true, as stores write to source signal on initialization. - if ( - is_runes(null) && - !ignore_mutation_validation && - current_effect !== null && - (current_effect.f & CLEAN) !== 0 && - (current_effect.f & MANAGED) === 0 - ) { - if (current_dependencies !== null && current_dependencies.includes(signal)) { - set_signal_status(current_effect, DIRTY); - schedule_effect(current_effect, false); - } else { - if (current_untracked_writes === null) { - current_untracked_writes = [signal]; - } else { - current_untracked_writes.push(signal); - } - } - } - - mark_signal_consumers(signal, DIRTY, true); - - // @ts-expect-error - if (DEV && signal.inspect) { - if (is_batching_effect) { - last_inspected_signal = /** @type {import('#client').SourceDebug} */ (signal); - } else { - for (const fn of /** @type {import('#client').SourceDebug} */ (signal).inspect) fn(); - } - } - } - - return value; -} - -/** - * @template V - * @param {import('#client').Source} signal - * @param {V} value - * @returns {void} - */ -export function set_sync(signal, value) { - flushSync(() => set(signal, value)); -} - /** * Invokes a function and captures all signals that are read during the invocation, * then invalidates them. @@ -863,26 +793,13 @@ export function invalidate_inner_signals(fn) { } } -/** - * @template V - * @param {import('#client').Source} source - * @param {V} value - */ -export function mutate(source, value) { - set( - source, - untrack(() => get(source)) - ); - return value; -} - /** * @param {import('#client').ValueSignal} signal * @param {number} to_status * @param {boolean} force_schedule * @returns {void} */ -function mark_signal_consumers(signal, to_status, force_schedule) { +export function mark_signal_consumers(signal, to_status, force_schedule) { const runes = is_runes(null); const consumers = signal.consumers; @@ -974,7 +891,7 @@ const STATUS_MASK = ~(DIRTY | MAYBE_DIRTY | CLEAN); * @param {number} status * @returns {void} */ -function set_signal_status(signal, status) { +export function set_signal_status(signal, status) { signal.f = (signal.f & STATUS_MASK) | status; } diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js index 67663e6cc2..a63b1bcf31 100644 --- a/packages/svelte/src/internal/index.js +++ b/packages/svelte/src/internal/index.js @@ -1,7 +1,5 @@ export { get, - set, - set_sync, invalidate_inner_signals, flushSync, tick, @@ -10,7 +8,6 @@ export { update_prop, update_pre, update_pre_prop, - mutate, value_or_fallback, exclude_from_object, pop, diff --git a/packages/svelte/src/reactivity/index.js b/packages/svelte/src/reactivity/index.js index b68cef7be3..f802121a2e 100644 --- a/packages/svelte/src/reactivity/index.js +++ b/packages/svelte/src/reactivity/index.js @@ -1,5 +1,5 @@ -import { source } from '../internal/client/reactivity/sources.js'; -import { get, set } from '../internal/client/runtime.js'; +import { source, set } from '../internal/client/reactivity/sources.js'; +import { get } from '../internal/client/runtime.js'; /** @type {Array} */ const read = [ diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index de5ca18fde..be93d77959 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -5,6 +5,7 @@ import { source } from '../../src/internal/client/reactivity/sources'; import type { Derived } from '../../src/internal/client/reactivity/types'; import { proxy } from '../../src/internal/client/proxy'; import { derived } from '../../src/internal/client/reactivity/deriveds'; +import { set } from '../../src/internal/client/reactivity/sources'; /** * @param runes runes mode @@ -46,8 +47,8 @@ describe('signals', () => { }); return () => { - $.flushSync(() => $.set(count, 1)); - $.flushSync(() => $.set(count, 2)); + $.flushSync(() => set(count, 1)); + $.flushSync(() => set(count, 2)); assert.deepEqual(log, ['0:0', '1:2', '2:4']); }; @@ -67,8 +68,8 @@ describe('signals', () => { }); return () => { - $.flushSync(() => $.set(count, 1)); - $.flushSync(() => $.set(count, 2)); + $.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']); }; @@ -88,8 +89,8 @@ describe('signals', () => { }); return () => { - $.flushSync(() => $.set(count, 1)); - $.flushSync(() => $.set(count, 2)); + $.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']); }; @@ -106,8 +107,8 @@ describe('signals', () => { }); return () => { - $.flushSync(() => $.set(count, 1)); - $.flushSync(() => $.set(count, 2)); + $.flushSync(() => set(count, 1)); + $.flushSync(() => set(count, 2)); assert.deepEqual(log, [0, 2, 4]); }; @@ -125,8 +126,8 @@ describe('signals', () => { }); return () => { - $.flushSync(() => $.set(count, 1)); - $.flushSync(() => $.set(count, 2)); + $.flushSync(() => set(count, 1)); + $.flushSync(() => set(count, 2)); assert.deepEqual(log, [0, 4, 8]); }; @@ -162,12 +163,12 @@ describe('signals', () => { let i = 2; while (--i) { res.length = 0; - $.set(B, 1); - $.set(A, 1 + i * 2); + set(B, 1); + set(A, 1 + i * 2); $.flushSync(); - $.set(A, 2 + i * 2); - $.set(B, 2); + set(A, 2 + i * 2); + set(B, 2); $.flushSync(); assert.equal(res.length, 4); @@ -190,13 +191,13 @@ describe('signals', () => { }); return () => { - $.flushSync(() => $.set(count, 1)); + $.flushSync(() => set(count, 1)); // Ensure we're not leaking consumers assert.deepEqual(count.consumers?.length, 1); - $.flushSync(() => $.set(count, 2)); + $.flushSync(() => set(count, 2)); // Ensure we're not leaking consumers assert.deepEqual(count.consumers?.length, 1); - $.flushSync(() => $.set(count, 3)); + $.flushSync(() => set(count, 3)); // Ensure we're not leaking consumers assert.deepEqual(count.consumers?.length, 1); assert.deepEqual(log, [0, 1, 2, 3]); @@ -219,11 +220,11 @@ describe('signals', () => { $.get(c); - $.flushSync(() => $.set(a, 1)); + $.flushSync(() => set(a, 1)); $.get(c); - $.flushSync(() => $.set(b, 1)); + $.flushSync(() => set(b, 1)); $.get(c); @@ -252,11 +253,11 @@ describe('signals', () => { }); return () => { - $.flushSync(() => $.set(count, 1)); - $.flushSync(() => $.set(count, 2)); - $.flushSync(() => $.set(count, 3)); - $.flushSync(() => $.set(count, 4)); - $.flushSync(() => $.set(count, 0)); + $.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.consumers?.length, 1); assert.deepEqual(log, [0, 2, 'limit', 0]); @@ -314,7 +315,7 @@ describe('signals', () => { const value = source({ count: 0 }); user_effect(() => { - $.set(value, { count: 0 }); + set(value, { count: 0 }); $.get(value); });