diff --git a/packages/svelte/src/internal/client/block.js b/packages/svelte/src/internal/client/block.js index 5ba278cf00..e71cdc7ec7 100644 --- a/packages/svelte/src/internal/client/block.js +++ b/packages/svelte/src/internal/client/block.js @@ -1,16 +1,12 @@ +import { + ROOT_BLOCK, + HEAD_BLOCK, + DYNAMIC_ELEMENT_BLOCK, + DYNAMIC_COMPONENT_BLOCK, + SNIPPET_BLOCK +} from './constants.js'; import { current_block } from './runtime.js'; -export const ROOT_BLOCK = 0; -export const IF_BLOCK = 1; -export const EACH_BLOCK = 2; -export const EACH_ITEM_BLOCK = 3; -export const AWAIT_BLOCK = 4; -export const KEY_BLOCK = 5; -export const HEAD_BLOCK = 6; -export const DYNAMIC_COMPONENT_BLOCK = 7; -export const DYNAMIC_ELEMENT_BLOCK = 8; -export const SNIPPET_BLOCK = 9; - /** * @param {boolean} intro * @returns {import('./types.js').RootBlock} diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js new file mode 100644 index 0000000000..7f3d7f1dba --- /dev/null +++ b/packages/svelte/src/internal/client/constants.js @@ -0,0 +1,26 @@ +export const SOURCE = 1; +export const DERIVED = 1 << 1; +export const EFFECT = 1 << 2; +export const PRE_EFFECT = 1 << 3; +export const RENDER_EFFECT = 1 << 4; +export const MANAGED = 1 << 6; +export const UNOWNED = 1 << 7; +export const CLEAN = 1 << 8; +export const DIRTY = 1 << 9; +export const MAYBE_DIRTY = 1 << 10; +export const INERT = 1 << 11; +export const DESTROYED = 1 << 12; + +export const ROOT_BLOCK = 0; +export const IF_BLOCK = 1; +export const EACH_BLOCK = 2; +export const EACH_ITEM_BLOCK = 3; +export const AWAIT_BLOCK = 4; +export const KEY_BLOCK = 5; +export const HEAD_BLOCK = 6; +export const DYNAMIC_COMPONENT_BLOCK = 7; +export const DYNAMIC_ELEMENT_BLOCK = 8; +export const SNIPPET_BLOCK = 9; + +export const UNINITIALIZED = Symbol(); +export const STATE_SYMBOL = Symbol('$state'); diff --git a/packages/svelte/src/internal/client/dev/ownership.js b/packages/svelte/src/internal/client/dev/ownership.js index 7d03e3c37f..000d6194ae 100644 --- a/packages/svelte/src/internal/client/dev/ownership.js +++ b/packages/svelte/src/internal/client/dev/ownership.js @@ -1,6 +1,6 @@ /** @typedef {{ file: string, line: number, column: number }} Location */ -import { STATE_SYMBOL } from '../proxy.js'; +import { STATE_SYMBOL } from '../constants.js'; import { untrack } from '../runtime.js'; /** @type {Record>} */ diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 19f4e61c96..ede2a759e9 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -1,9 +1,7 @@ import { is_promise } from '../../../common.js'; -import { AWAIT_BLOCK } from '../../block.js'; import { hydrate_block_anchor } from '../../hydration.js'; import { remove } from '../../reconciler.js'; import { - UNINITIALIZED, current_block, destroy_signal, execute_effect, @@ -12,6 +10,7 @@ import { } from '../../runtime.js'; import { render_effect } from '../../reactivity/computations.js'; import { trigger_transitions } from '../../transitions.js'; +import { AWAIT_BLOCK, UNINITIALIZED } from '../../constants.js'; /** @returns {import('../../types.js').AwaitBlock} */ export function create_await_block() { diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index b6ca2d402f..9f93b6db5a 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -7,7 +7,6 @@ import { EACH_KEYED } from '../../../../constants.js'; import { noop } from '../../../common.js'; -import { EACH_BLOCK, EACH_ITEM_BLOCK } from '../../block.js'; import { current_hydration_fragment, get_hydration_fragment, @@ -28,6 +27,7 @@ import { render_effect } from '../../reactivity/computations.js'; import { source, mutable_source } from '../../reactivity/sources.js'; import { trigger_transitions } from '../../transitions.js'; import { is_array } from '../../utils.js'; +import { EACH_BLOCK, EACH_ITEM_BLOCK } from '../../constants.js'; const NEW_BLOCK = -1; const MOVED_BLOCK = 99999999; diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index 076bc9c8d8..9499590817 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -1,13 +1,7 @@ -import { KEY_BLOCK } from '../../block.js'; +import { UNINITIALIZED, KEY_BLOCK } from '../../constants.js'; import { hydrate_block_anchor } from '../../hydration.js'; import { remove } from '../../reconciler.js'; -import { - UNINITIALIZED, - current_block, - destroy_signal, - execute_effect, - push_destroy_fn -} from '../../runtime.js'; +import { current_block, destroy_signal, execute_effect, push_destroy_fn } from '../../runtime.js'; import { render_effect } from '../../reactivity/computations.js'; import { trigger_transitions } from '../../transitions.js'; import { safe_not_equal } from '../../reactivity/equality.js'; diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js new file mode 100644 index 0000000000..6568000a36 --- /dev/null +++ b/packages/svelte/src/internal/client/dom/task.js @@ -0,0 +1,59 @@ +import { run_all } from '../../common.js'; + +let is_task_queued = false; +let is_raf_queued = false; + +/** @type {Array<() => void>} */ +let current_queued_tasks = []; +/** @type {Array<() => void>} */ +let current_raf_tasks = []; + +function process_task() { + is_task_queued = false; + const tasks = current_queued_tasks.slice(); + current_queued_tasks = []; + run_all(tasks); +} + +function process_raf_task() { + is_raf_queued = false; + const tasks = current_raf_tasks.slice(); + current_raf_tasks = []; + run_all(tasks); +} + +/** + * @param {() => void} fn + * @returns {void} + */ +export function schedule_task(fn) { + if (!is_task_queued) { + is_task_queued = true; + setTimeout(process_task, 0); + } + current_queued_tasks.push(fn); +} + +/** + * @param {() => void} fn + * @returns {void} + */ +export function schedule_raf_task(fn) { + if (!is_raf_queued) { + is_raf_queued = true; + requestAnimationFrame(process_raf_task); + } + current_raf_tasks.push(fn); +} + +/** + * Synchronously run any queued tasks. + */ +export function flush_tasks() { + if (is_task_queued) { + process_task(); + } + if (is_raf_queued) { + process_raf_task(); + } +} diff --git a/packages/svelte/src/internal/client/hydration.js b/packages/svelte/src/internal/client/hydration.js index 3d37f88ec1..d6912dfb59 100644 --- a/packages/svelte/src/internal/client/hydration.js +++ b/packages/svelte/src/internal/client/hydration.js @@ -1,7 +1,7 @@ // Handle hydration +import { schedule_task } from './dom/task.js'; import { empty } from './operations.js'; -import { schedule_task } from './runtime.js'; /** * Use this variable to guard everything related to hydration code so it can be treeshaken out diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 7d5e97a42b..36e24aa1b6 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -4,7 +4,6 @@ import { set, update, updating_derived, - UNINITIALIZED, batch_inspect, current_component_context } from './runtime.js'; @@ -21,8 +20,7 @@ import { } from './utils.js'; import { add_owner, check_ownership, strip_owner } from './dev/ownership.js'; import { mutable_source, source } from './reactivity/sources.js'; - -export const STATE_SYMBOL = Symbol('$state'); +import { STATE_SYMBOL, UNINITIALIZED } from './constants.js'; /** * @template T diff --git a/packages/svelte/src/internal/client/reactivity/computations.js b/packages/svelte/src/internal/client/reactivity/computations.js index 8c129eeb3f..9ae74cc731 100644 --- a/packages/svelte/src/internal/client/reactivity/computations.js +++ b/packages/svelte/src/internal/client/reactivity/computations.js @@ -1,5 +1,6 @@ import { DEV } from 'esm-env'; import { +<<<<<<< HEAD CLEAN, DERIVED, DIRTY, @@ -9,6 +10,8 @@ import { RENDER_EFFECT, UNINITIALIZED, UNOWNED, +======= +>>>>>>> main current_block, current_component_context, current_consumer, @@ -18,7 +21,21 @@ import { schedule_effect } from '../runtime.js'; import { default_equals, safe_equal } from './equality.js'; +<<<<<<< HEAD import { remove } from '../reconciler.js'; +======= +import { + DIRTY, + MANAGED, + RENDER_EFFECT, + EFFECT, + PRE_EFFECT, + DERIVED, + UNOWNED, + CLEAN, + UNINITIALIZED +} from '../constants.js'; +>>>>>>> main /** * @template V diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index a889e9c31e..d0b6de7cba 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -1,12 +1,7 @@ -/** - * @template V - * @param {V} initial_value - * @returns {import('../types.js').SourceSignal} - */ - import { DEV } from 'esm-env'; -import { CLEAN, SOURCE, current_component_context } from '../runtime.js'; +import { current_component_context } from '../runtime.js'; import { default_equals, safe_equal } from './equality.js'; +import { CLEAN, SOURCE } from '../constants.js'; /** * @template V diff --git a/packages/svelte/src/internal/client/reactivity/store.js b/packages/svelte/src/internal/client/reactivity/store.js new file mode 100644 index 0000000000..3680b95052 --- /dev/null +++ b/packages/svelte/src/internal/client/reactivity/store.js @@ -0,0 +1,153 @@ +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 { user_effect } from './computations.js'; +import { mutable_source } from './sources.js'; + +/** + * Gets the current value of a store. If the store isn't subscribed to yet, it will create a proxy + * signal that will be updated when the store is. The store references container is needed to + * track reassignments to stores and to track the correct component context. + * @template V + * @param {import('../types.js').Store | null | undefined} store + * @param {string} store_name + * @param {import('../types.js').StoreReferencesContainer} stores + * @returns {V} + */ +export function store_get(store, store_name, stores) { + /** @type {import('../types.js').StoreReferencesContainer[''] | undefined} */ + let entry = stores[store_name]; + const is_new = entry === undefined; + + if (is_new) { + entry = { + store: null, + last_value: null, + value: mutable_source(UNINITIALIZED), + unsubscribe: noop + }; + // TODO: can we remove this code? it was refactored out when we split up source/comptued signals + // push_destroy_fn(entry.value, () => { + // /** @type {import('../types.js').StoreReferencesContainer['']} */ (entry).last_value = + // /** @type {import('../types.js').StoreReferencesContainer['']} */ (entry).value.value; + // }); + stores[store_name] = entry; + } + + if (is_new || entry.store !== store) { + entry.unsubscribe(); + entry.store = store ?? null; + entry.unsubscribe = connect_store_to_signal(store, entry.value); + } + + const value = get(entry.value); + // This could happen if the store was cleaned up because the component was destroyed and there's a leak on the user side. + // In that case we don't want to fail with a cryptic Symbol error, but rather return the last value we got. + return value === UNINITIALIZED ? entry.last_value : value; +} + +/** + * @template V + * @param {import('../types.js').Store | null | undefined} store + * @param {import('../types.js').SourceSignal} source + */ +function connect_store_to_signal(store, source) { + if (store == null) { + set(source, undefined); + return noop; + } + + /** @param {V} v */ + const run = (v) => { + set_ignore_mutation_validation(true); + set(source, v); + set_ignore_mutation_validation(false); + }; + return subscribe_to_store(store, run); +} + +/** + * Sets the new value of a store and returns that value. + * @template V + * @param {import('../types.js').Store} store + * @param {V} value + * @returns {V} + */ +export function store_set(store, value) { + store.set(value); + return value; +} + +/** + * Unsubscribes from all auto-subscribed stores on destroy + * @param {import('../types.js').StoreReferencesContainer} stores + */ +export function unsubscribe_on_destroy(stores) { + on_destroy(() => { + let store_name; + for (store_name in stores) { + const ref = stores[store_name]; + ref.unsubscribe(); + // TODO: can we remove this code? it was refactored out when we split up source/comptued signals + // destroy_signal(ref.value); + } + }); +} + +/** + * Updates a store with a new value. + * @param {import('../types.js').Store} store the store to update + * @param {any} expression the expression that mutates the store + * @param {V} new_value the new store value + * @template V + */ +export function mutate_store(store, expression, new_value) { + store.set(new_value); + return expression; +} + +/** + * @template V + * @param {unknown} val + * @returns {val is import('../types.js').Store} + */ +export function is_store(val) { + return ( + typeof val === 'object' && + val !== null && + typeof (/** @type {import('../types.js').Store} */ (val).subscribe) === 'function' + ); +} + +/** + * @param {import('../types.js').Store} store + * @param {number} store_value + * @param {1 | -1} [d] + * @returns {number} + */ +export function update_store(store, store_value, d = 1) { + store.set(store_value + d); + return store_value; +} + +/** + * @param {import('../types.js').Store} store + * @param {number} store_value + * @param {1 | -1} [d] + * @returns {number} + */ +export function update_pre_store(store, store_value, d = 1) { + const value = store_value + d; + store.set(value); + return value; +} + +/** + * Schedules a callback to run immediately before the component is unmounted. + * @param {() => any} fn + * @returns {void} + */ +function on_destroy(fn) { + user_effect(() => () => untrack(fn)); +} diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 535e8deeee..3ad0d836a5 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -21,7 +21,11 @@ import { PassiveDelegatedEvents, DelegatedEvents, AttributeAliases, - namespace_svg + namespace_svg, + PROPS_IS_IMMUTABLE, + PROPS_IS_RUNES, + PROPS_IS_UPDATED, + PROPS_IS_LAZY_INITIAL } from '../../constants.js'; import { create_fragment_from_html, @@ -39,12 +43,23 @@ import { flush_sync, current_block, push, - current_component_context, pop, + current_component_context, deep_read, + get, + set, + is_signals_recorded, + inspect_fn, current_effect } from './runtime.js'; -import { render_effect, effect, managed_effect } from './reactivity/computations.js'; +import { + render_effect, + effect, + managed_effect, + derived, + pre_effect, + user_effect +} from './reactivity/computations.js'; import { current_hydration_fragment, get_hydration_fragment, @@ -61,9 +76,11 @@ import { is_function, object_assign } from './utils.js'; -import { is_promise } from '../common.js'; +import { run } from '../common.js'; import { bind_transition, trigger_transitions } from './transitions.js'; -import { STATE_SYMBOL } from './proxy.js'; +import { mutable_source, source } from './reactivity/sources.js'; +import { safe_equal, safe_not_equal } from './reactivity/equality.js'; +import { STATE_SYMBOL } from './constants.js'; /** @type {Set} */ const all_registerd_events = new Set(); @@ -2692,3 +2709,198 @@ function get_root_for_style(node) { } return /** @type {Document} */ (node.ownerDocument); } + +/** + * This function is responsible for synchronizing a possibly bound prop with the inner component state. + * It is used whenever the compiler sees that the component writes to the prop, or when it has a default prop_value. + * @template V + * @param {Record} props + * @param {string} key + * @param {number} flags + * @param {V | (() => V)} [initial] + * @returns {(() => V | ((arg: V) => V) | ((arg: V, mutation: boolean) => V))} + */ +export function prop(props, key, flags, initial) { + var immutable = (flags & PROPS_IS_IMMUTABLE) !== 0; + var runes = (flags & PROPS_IS_RUNES) !== 0; + var prop_value = /** @type {V} */ (props[key]); + var setter = get_descriptor(props, key)?.set; + + if (prop_value === undefined && initial !== undefined) { + if (setter && runes) { + // TODO consolidate all these random runtime errors + throw new Error( + 'ERR_SVELTE_BINDING_FALLBACK' + + (DEV + ? `: Cannot pass undefined to bind:${key} because the property contains a fallback value. Pass a different value than undefined to ${key}.` + : '') + ); + } + + // @ts-expect-error would need a cumbersome method overload to type this + if ((flags & PROPS_IS_LAZY_INITIAL) !== 0) initial = initial(); + + prop_value = /** @type {V} */ (initial); + + if (setter) setter(prop_value); + } + + var getter = () => { + var value = /** @type {V} */ (props[key]); + if (value !== undefined) initial = undefined; + return value === undefined ? /** @type {V} */ (initial) : value; + }; + + // easy mode — prop is never written to + if ((flags & PROPS_IS_UPDATED) === 0) { + return getter; + } + + // intermediate mode — prop is written to, but the parent component had + // `bind:foo` which means we can just call `$$props.foo = value` directly + if (setter) { + return function (/** @type {V} */ value) { + if (arguments.length === 1) { + /** @type {Function} */ (setter)(value); + return value; + } else { + return getter(); + } + }; + } + + // hard mode. this is where it gets ugly — the value in the child should + // synchronize with the parent, but it should also be possible to temporarily + // set the value to something else locally. + var from_child = false; + var was_from_child = false; + + // The derived returns the current value. The underlying mutable + // source is written to from various places to persist this value. + var inner_current_value = mutable_source(prop_value); + var current_value = derived(() => { + var parent_value = getter(); + var child_value = get(inner_current_value); + + if (from_child) { + from_child = false; + was_from_child = true; + return child_value; + } + + was_from_child = false; + return (inner_current_value.v = parent_value); + }); + + if (!immutable) current_value.e = safe_equal; + + return function (/** @type {V} */ value, mutation = false) { + var current = get(current_value); + + // legacy nonsense — need to ensure the source is invalidated when necessary + // also needed for when handling inspect logic so we can inspect the correct source signal + if (is_signals_recorded || (DEV && inspect_fn)) { + // set this so that we don't reset to the parent value if `d` + // is invalidated because of `invalidate_inner_signals` (rather + // than because the parent or child value changed) + from_child = was_from_child; + // invoke getters so that signals are picked up by `invalidate_inner_signals` + getter(); + get(inner_current_value); + } + + if (arguments.length > 0) { + if (mutation || (immutable ? value !== current : safe_not_equal(value, current))) { + from_child = true; + set(inner_current_value, mutation ? current : value); + get(current_value); // force a synchronisation immediately + } + + return value; + } + + return current; + }; +} + +/** + * Legacy-mode only: Call `onMount` callbacks and set up `beforeUpdate`/`afterUpdate` effects + */ +export function init() { + const context = /** @type {import('./types.js').ComponentContext} */ (current_component_context); + const callbacks = context.u; + + if (!callbacks) return; + + // beforeUpdate + pre_effect(() => { + observe_all(context); + callbacks.b.forEach(run); + }); + + // onMount (must run before afterUpdate) + user_effect(() => { + const fns = untrack(() => callbacks.m.map(run)); + return () => { + for (const fn of fns) { + if (typeof fn === 'function') { + fn(); + } + } + }; + }); + + // afterUpdate + user_effect(() => { + observe_all(context); + callbacks.a.forEach(run); + }); +} + +/** + * Invoke the getter of all signals associated with a component + * so they can be registered to the effect this function is called in. + * @param {import('./types.js').ComponentContext} context + */ +function observe_all(context) { + if (context.d) { + for (const signal of context.d) get(signal); + } + + deep_read(context.s); +} + +/** + * Under some circumstances, imports may be reactive in legacy mode. In that case, + * they should be using `reactive_import` as part of the transformation + * @param {() => any} fn + */ +export function reactive_import(fn) { + const s = source(0); + return function () { + if (arguments.length === 1) { + set(s, get(s) + 1); + return arguments[0]; + } else { + get(s); + return fn(); + } + }; +} + +/** + * @this {any} + * @param {Record} $$props + * @param {Event} event + * @returns {void} + */ +export function bubble_event($$props, event) { + var events = /** @type {Record} */ ($$props.$$events)?.[ + event.type + ]; + var callbacks = is_array(events) ? events.slice() : events == null ? [] : [events]; + for (var fn of callbacks) { + // Preserve "this" context + fn.call(this, event); + } +} diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 1ec8f16e19..5b28235830 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1,9 +1,7 @@ import { DEV } from 'esm-env'; -import { subscribe_to_store } from '../../store/utils.js'; -import { noop, run, run_all } from '../common.js'; +import { run_all } from '../common.js'; import { array_prototype, - get_descriptor, get_descriptors, get_prototype_of, is_array, @@ -11,44 +9,45 @@ import { object_freeze, object_prototype } from './utils.js'; +import { unstate } from './proxy.js'; import { PROPS_IS_LAZY_INITIAL, PROPS_IS_IMMUTABLE, PROPS_IS_RUNES, PROPS_IS_UPDATED } from '../../constants.js'; -import { STATE_SYMBOL, unstate } from './proxy.js'; -import { EACH_BLOCK, IF_BLOCK } from './block.js'; -import { derived, pre_effect, user_effect } from './reactivity/computations.js'; -import { mutable_source, source } from './reactivity/sources.js'; +import { derived, pre_effect } from './reactivity/computations.js'; +import { mutable_source } from './reactivity/sources.js'; import { safe_equal, safe_not_equal } from './reactivity/equality.js'; - -export const SOURCE = 1; -export const DERIVED = 1 << 1; -export const EFFECT = 1 << 2; -export const PRE_EFFECT = 1 << 3; -export const RENDER_EFFECT = 1 << 4; -export const MANAGED = 1 << 6; -export const UNOWNED = 1 << 7; -export const CLEAN = 1 << 8; -export const DIRTY = 1 << 9; -export const MAYBE_DIRTY = 1 << 10; -export const INERT = 1 << 11; -export const DESTROYED = 1 << 12; +import { + EACH_BLOCK, + IF_BLOCK, + EFFECT, + PRE_EFFECT, + RENDER_EFFECT, + DIRTY, + UNINITIALIZED, + MAYBE_DIRTY, + CLEAN, + DERIVED, + UNOWNED, + DESTROYED, + INERT, + MANAGED, + SOURCE, + STATE_SYMBOL +} from './constants.js'; +import { flush_tasks } from './dom/task.js'; const IS_EFFECT = EFFECT | PRE_EFFECT | RENDER_EFFECT; const FLUSH_MICROTASK = 0; const FLUSH_SYNC = 1; -export const UNINITIALIZED = Symbol(); - // Used for controlling the flush of effects. let current_scheduler_mode = FLUSH_MICROTASK; // Used for handling scheduling let is_micro_task_queued = false; -let is_task_queued = false; -let is_raf_queued = false; let is_flushing_effect = false; // Used for $inspect export let is_batching_effect = false; @@ -61,10 +60,6 @@ let current_queued_pre_and_render_effects = []; /** @type {import('./types.js').EffectSignal[]} */ let current_queued_effects = []; -/** @type {Array<() => void>} */ -let current_queued_tasks = []; -/** @type {Array<() => void>} */ -let current_raf_tasks = []; let flush_count = 0; // Handle signal reactivity tree dependencies and consumer @@ -89,16 +84,20 @@ let last_inspected_signal = null; 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; +/** @param {boolean} value */ +export function set_ignore_mutation_validation(value) { + ignore_mutation_validation = value; +} // If we are working with a get() chain that has no active container, // to prevent memory leaks, we skip adding the consumer. let current_skip_consumer = false; // Handle collecting all signals which are read during a specific time frame -let is_signals_recorded = false; +export let is_signals_recorded = false; let captured_signals = new Set(); /** @type {Function | null} */ -let inspect_fn = null; +export let inspect_fn = null; /** @type {Array} */ let inspect_captured_signals = []; @@ -570,44 +569,6 @@ export function schedule_effect(signal, sync) { } } -function process_task() { - is_task_queued = false; - const tasks = current_queued_tasks.slice(); - current_queued_tasks = []; - run_all(tasks); -} - -function process_raf_task() { - is_raf_queued = false; - const tasks = current_raf_tasks.slice(); - current_raf_tasks = []; - run_all(tasks); -} - -/** - * @param {() => void} fn - * @returns {void} - */ -export function schedule_task(fn) { - if (!is_task_queued) { - is_task_queued = true; - setTimeout(process_task, 0); - } - current_queued_tasks.push(fn); -} - -/** - * @param {() => void} fn - * @returns {void} - */ -export function schedule_raf_task(fn) { - if (!is_raf_queued) { - is_raf_queued = true; - requestAnimationFrame(process_raf_task); - } - current_raf_tasks.push(fn); -} - /** * @returns {void} */ @@ -683,12 +644,7 @@ export function flush_sync(fn, flush_previous = true) { if (current_queued_pre_and_render_effects.length > 0 || effects.length > 0) { flushSync(); } - if (is_raf_queued) { - process_raf_task(); - } - if (is_task_queued) { - process_task(); - } + flush_tasks(); flush_count = 0; } finally { current_scheduler_mode = previous_scheduler_mode; @@ -739,96 +695,6 @@ function update_derived(signal, force_schedule) { } } -/** - * Gets the current value of a store. If the store isn't subscribed to yet, it will create a proxy - * signal that will be updated when the store is. The store references container is needed to - * track reassignments to stores and to track the correct component context. - * @template V - * @param {import('./types.js').Store | null | undefined} store - * @param {string} store_name - * @param {import('./types.js').StoreReferencesContainer} stores - * @returns {V} - */ -export function store_get(store, store_name, stores) { - /** @type {import('./types.js').StoreReferencesContainer[''] | undefined} */ - let entry = stores[store_name]; - const is_new = entry === undefined; - - if (is_new) { - entry = { - store: null, - last_value: null, - value: mutable_source(UNINITIALIZED), - unsubscribe: noop - }; - // TODO: can we remove this code? it was refactored out when we split up source/comptued signals - // push_destroy_fn(entry.value, () => { - // /** @type {import('./types.js').StoreReferencesContainer['']} */ (entry).last_value = - // /** @type {import('./types.js').StoreReferencesContainer['']} */ (entry).value.value; - // }); - stores[store_name] = entry; - } - - if (is_new || entry.store !== store) { - entry.unsubscribe(); - entry.store = store ?? null; - entry.unsubscribe = connect_store_to_signal(store, entry.value); - } - - const value = get(entry.value); - // This could happen if the store was cleaned up because the component was destroyed and there's a leak on the user side. - // In that case we don't want to fail with a cryptic Symbol error, but rather return the last value we got. - return value === UNINITIALIZED ? entry.last_value : value; -} - -/** - * @template V - * @param {import('./types.js').Store | null | undefined} store - * @param {import('./types.js').SourceSignal} source - */ -function connect_store_to_signal(store, source) { - if (store == null) { - set(source, undefined); - return noop; - } - - /** @param {V} v */ - const run = (v) => { - ignore_mutation_validation = true; - set(source, v); - ignore_mutation_validation = false; - }; - return subscribe_to_store(store, run); -} - -/** - * Sets the new value of a store and returns that value. - * @template V - * @param {import('./types.js').Store} store - * @param {V} value - * @returns {V} - */ -export function store_set(store, value) { - store.set(value); - return value; -} - -/** - * Unsubscribes from all auto-subscribed stores on destroy - * @param {import('./types.js').StoreReferencesContainer} stores - */ -export function unsubscribe_on_destroy(stores) { - on_destroy(() => { - let store_name; - for (store_name in stores) { - const ref = stores[store_name]; - ref.unsubscribe(); - // TODO: can we remove this code? it was refactored out when we split up source/comptued signals - // destroy_signal(ref.value); - } - }); -} - /** * @template V * @param {import('./types.js').Signal} signal @@ -960,18 +826,6 @@ export function mutate(source, value) { return value; } -/** - * Updates a store with a new value. - * @param {import('./types.js').Store} store the store to update - * @param {any} expression the expression that mutates the store - * @param {V} new_value the new store value - * @template V - */ -export function mutate_store(store, expression, new_value) { - store.set(new_value); - return expression; -} - /** * @param {import('./types.js').ComputationSignal} signal * @param {boolean} inert @@ -1230,132 +1084,6 @@ export function is_signal(val) { ); } -/** - * @template V - * @param {unknown} val - * @returns {val is import('./types.js').Store} - */ -export function is_store(val) { - return ( - typeof val === 'object' && - val !== null && - typeof (/** @type {import('./types.js').Store} */ (val).subscribe) === 'function' - ); -} - -/** - * This function is responsible for synchronizing a possibly bound prop with the inner component state. - * It is used whenever the compiler sees that the component writes to the prop, or when it has a default prop_value. - * @template V - * @param {Record} props - * @param {string} key - * @param {number} flags - * @param {V | (() => V)} [initial] - * @returns {(() => V | ((arg: V) => V) | ((arg: V, mutation: boolean) => V))} - */ -export function prop(props, key, flags, initial) { - var immutable = (flags & PROPS_IS_IMMUTABLE) !== 0; - var runes = (flags & PROPS_IS_RUNES) !== 0; - var prop_value = /** @type {V} */ (props[key]); - var setter = get_descriptor(props, key)?.set; - - if (prop_value === undefined && initial !== undefined) { - if (setter && runes) { - // TODO consolidate all these random runtime errors - throw new Error( - 'ERR_SVELTE_BINDING_FALLBACK' + - (DEV - ? `: Cannot pass undefined to bind:${key} because the property contains a fallback value. Pass a different value than undefined to ${key}.` - : '') - ); - } - - // @ts-expect-error would need a cumbersome method overload to type this - if ((flags & PROPS_IS_LAZY_INITIAL) !== 0) initial = initial(); - - prop_value = /** @type {V} */ (initial); - - if (setter) setter(prop_value); - } - - var getter = () => { - var value = /** @type {V} */ (props[key]); - if (value !== undefined) initial = undefined; - return value === undefined ? /** @type {V} */ (initial) : value; - }; - - // easy mode — prop is never written to - if ((flags & PROPS_IS_UPDATED) === 0) { - return getter; - } - - // intermediate mode — prop is written to, but the parent component had - // `bind:foo` which means we can just call `$$props.foo = value` directly - if (setter) { - return function (/** @type {V} */ value) { - if (arguments.length === 1) { - /** @type {Function} */ (setter)(value); - return value; - } else { - return getter(); - } - }; - } - - // hard mode. this is where it gets ugly — the value in the child should - // synchronize with the parent, but it should also be possible to temporarily - // set the value to something else locally. - var from_child = false; - var was_from_child = false; - - // The derived returns the current value. The underlying mutable - // source is written to from various places to persist this value. - var inner_current_value = mutable_source(prop_value); - var current_value = derived(() => { - var parent_value = getter(); - var child_value = get(inner_current_value); - - if (from_child) { - from_child = false; - was_from_child = true; - return child_value; - } - - was_from_child = false; - return (inner_current_value.v = parent_value); - }); - - if (!immutable) current_value.e = safe_equal; - - return function (/** @type {V} */ value, mutation = false) { - var current = get(current_value); - - // legacy nonsense — need to ensure the source is invalidated when necessary - // also needed for when handling inspect logic so we can inspect the correct source signal - if (is_signals_recorded || (DEV && inspect_fn)) { - // set this so that we don't reset to the parent value if `d` - // is invalidated because of `invalidate_inner_signals` (rather - // than because the parent or child value changed) - from_child = was_from_child; - // invoke getters so that signals are picked up by `invalidate_inner_signals` - getter(); - get(inner_current_value); - } - - if (arguments.length > 0) { - if (mutation || (immutable ? value !== current : safe_not_equal(value, current))) { - from_child = true; - set(inner_current_value, mutation ? current : value); - get(current_value); // force a synchronisation immediately - } - - return value; - } - - return current; - }; -} - /** @returns {Map} */ export function get_or_init_context_map() { const component_context = current_component_context; @@ -1384,23 +1112,6 @@ function get_parent_context(component_context) { return null; } -/** - * @this {any} - * @param {Record} $$props - * @param {Event} event - * @returns {void} - */ -export function bubble_event($$props, event) { - var events = /** @type {Record} */ ($$props.$$events)?.[ - event.type - ]; - var callbacks = is_array(events) ? events.slice() : events == null ? [] : [events]; - for (var fn of callbacks) { - // Preserve "this" context - fn.call(this, event); - } -} - /** * @param {import('./types.js').Signal} signal * @param {1 | -1} [d] @@ -1423,17 +1134,6 @@ export function update_prop(fn, d = 1) { return value; } -/** - * @param {import('./types.js').Store} store - * @param {number} store_value - * @param {1 | -1} [d] - * @returns {number} - */ -export function update_store(store, store_value, d = 1) { - store.set(store_value + d); - return store_value; -} - /** * @param {import('./types.js').Signal} signal * @param {1 | -1} [d] @@ -1456,36 +1156,6 @@ export function update_pre_prop(fn, d = 1) { return value; } -/** - * @param {import('./types.js').Store} store - * @param {number} store_value - * @param {1 | -1} [d] - * @returns {number} - */ -export function update_pre_store(store, store_value, d = 1) { - const value = store_value + d; - store.set(value); - return value; -} - -/** - * Under some circumstances, imports may be reactive in legacy mode. In that case, - * they should be using `reactive_import` as part of the transformation - * @param {() => any} fn - */ -export function reactive_import(fn) { - const s = source(0); - return function () { - if (arguments.length === 1) { - set(s, get(s) + 1); - return arguments[0]; - } else { - get(s); - return fn(); - } - }; -} - /** * @param {Record} obj * @param {string[]} keys @@ -1510,15 +1180,6 @@ export function value_or_fallback(value, fallback) { return value === undefined ? fallback : value; } -/** - * Schedules a callback to run immediately before the component is unmounted. - * @param {() => any} fn - * @returns {void} - */ -function on_destroy(fn) { - user_effect(() => () => untrack(fn)); -} - /** * @param {Record} props * @param {any} runes @@ -1580,53 +1241,6 @@ export function pop(component) { return component || /** @type {T} */ ({}); } -/** - * Invoke the getter of all signals associated with a component - * so they can be registered to the effect this function is called in. - * @param {import('./types.js').ComponentContext} context - */ -function observe_all(context) { - if (context.d) { - for (const signal of context.d) get(signal); - } - - deep_read(context.s); -} - -/** - * Legacy-mode only: Call `onMount` callbacks and set up `beforeUpdate`/`afterUpdate` effects - */ -export function init() { - const context = /** @type {import('./types.js').ComponentContext} */ (current_component_context); - const callbacks = context.u; - - if (!callbacks) return; - - // beforeUpdate - pre_effect(() => { - observe_all(context); - callbacks.b.forEach(run); - }); - - // onMount (must run before afterUpdate) - user_effect(() => { - const fns = untrack(() => callbacks.m.map(run)); - return () => { - for (const fn of fns) { - if (typeof fn === 'function') { - fn(); - } - } - }; - }); - - // afterUpdate - user_effect(() => { - observe_all(context); - callbacks.a.forEach(run); - }); -} - /** * Deeply traverse an object and read all its properties * so that they're all reactive in case this is `$state` diff --git a/packages/svelte/src/internal/client/transitions.js b/packages/svelte/src/internal/client/transitions.js index 5d86a77d86..59eb3ad60d 100644 --- a/packages/svelte/src/internal/client/transitions.js +++ b/packages/svelte/src/internal/client/transitions.js @@ -8,8 +8,9 @@ import { IF_BLOCK, KEY_BLOCK, ROOT_BLOCK -} from './block.js'; +} from './constants.js'; import { destroy_each_item_block, get_first_element } from './dom/blocks/each.js'; +import { schedule_raf_task } from './dom/task.js'; import { append_child, empty } from './operations.js'; import { effect, managed_effect, managed_pre_effect } from './reactivity/computations.js'; import { @@ -18,7 +19,6 @@ import { destroy_signal, execute_effect, mark_subtree_inert, - schedule_raf_task, untrack } from './runtime.js'; import { raf } from './timing.js'; diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index cbdfa54591..028f81af43 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -1,4 +1,9 @@ import { + DERIVED, + EFFECT, + RENDER_EFFECT, + SOURCE, + PRE_EFFECT, ROOT_BLOCK, EACH_BLOCK, EACH_ITEM_BLOCK, @@ -8,10 +13,9 @@ import { HEAD_BLOCK, DYNAMIC_COMPONENT_BLOCK, DYNAMIC_ELEMENT_BLOCK, - SNIPPET_BLOCK -} from './block.js'; -import type { STATE_SYMBOL } from './proxy.js'; -import { DERIVED, EFFECT, RENDER_EFFECT, SOURCE, PRE_EFFECT } from './runtime.js'; + SNIPPET_BLOCK, + STATE_SYMBOL +} from './constants.js'; // Put all internal types in this file. Once we convert to JSDoc, we can make this a d.ts file diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js index 2752b8048e..09fec70828 100644 --- a/packages/svelte/src/internal/index.js +++ b/packages/svelte/src/internal/index.js @@ -1,33 +1,33 @@ export { - store_get, get, set, set_sync, invalidate_inner_signals, +<<<<<<< HEAD prop, flushSync, bubble_event, +======= + flushSync, +>>>>>>> main tick, untrack, update, update_prop, - update_store, update_pre, update_pre_prop, - update_pre_store, mutate, - mutate_store, value_or_fallback, exclude_from_object, - store_set, - unsubscribe_on_destroy, pop, push, +<<<<<<< HEAD reactive_import, +======= +>>>>>>> main inspect, unwrap, freeze, - init, deep_read } from './client/runtime.js'; export * from './client/dev/ownership.js'; @@ -38,6 +38,10 @@ export * from './client/dom/blocks/each.js'; export * from './client/reactivity/computations.js'; export * from './client/reactivity/sources.js'; export * from './client/reactivity/equality.js'; +<<<<<<< HEAD +======= +export * from './client/reactivity/store.js'; +>>>>>>> main export * from './client/render.js'; export * from './client/validate.js'; export { raf } from './client/timing.js';