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/custom-element.js b/packages/svelte/src/internal/client/custom-element.js index 5374d6ba77..92d1a46573 100644 --- a/packages/svelte/src/internal/client/custom-element.js +++ b/packages/svelte/src/internal/client/custom-element.js @@ -1,5 +1,6 @@ import { createClassComponent } from '../../legacy/legacy-client.js'; -import { render_effect, destroy_signal } from './runtime.js'; +import { destroy_signal } from './runtime.js'; +import { render_effect } from './reactivity/computations.js'; import { open, close } from './render.js'; import { define_property } from './utils.js'; 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 39946f63f2..ede2a759e9 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -1,17 +1,16 @@ 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, flushSync, - push_destroy_fn, - render_effect + push_destroy_fn } 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 d6367509b7..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, @@ -21,14 +20,14 @@ import { current_block, destroy_signal, execute_effect, - mutable_source, push_destroy_fn, - render_effect, - set_signal_value, - source + set_signal_value } from '../../runtime.js'; +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/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 75cfd7faed..1aa742fd43 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -1,4 +1,4 @@ -import { IF_BLOCK } from '../../block.js'; +import { IF_BLOCK } from '../../constants.js'; import { current_hydration_fragment, hydrate_block_anchor, @@ -6,13 +6,8 @@ import { set_current_hydration_fragment } from '../../hydration.js'; import { remove } from '../../reconciler.js'; -import { - current_block, - destroy_signal, - execute_effect, - push_destroy_fn, - render_effect -} 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'; /** @returns {import('../../types.js').IfBlock} */ diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index fda8828475..9499590817 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -1,16 +1,10 @@ -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, - render_effect, - safe_not_equal -} 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'; /** @returns {import('../../types.js').KeyBlock} */ function create_key_block() { 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 8f26e67806..36e24aa1b6 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -1,16 +1,13 @@ import { DEV } from 'esm-env'; import { - effect_active, get, set, update, - source, updating_derived, - UNINITIALIZED, - mutable_source, batch_inspect, current_component_context } from './runtime.js'; +import { effect_active } from './reactivity/computations.js'; import { array_prototype, define_property, @@ -22,8 +19,8 @@ import { object_prototype } from './utils.js'; import { add_owner, check_ownership, strip_owner } from './dev/ownership.js'; - -export const STATE_SYMBOL = Symbol('$state'); +import { mutable_source, source } from './reactivity/sources.js'; +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 new file mode 100644 index 0000000000..13052e410a --- /dev/null +++ b/packages/svelte/src/internal/client/reactivity/computations.js @@ -0,0 +1,258 @@ +import { DEV } from 'esm-env'; +import { + current_block, + current_component_context, + current_consumer, + current_effect, + destroy_signal, + flush_local_render_effects, + schedule_effect +} from '../runtime.js'; +import { default_equals, safe_equal } from './equality.js'; +import { + DIRTY, + MANAGED, + RENDER_EFFECT, + EFFECT, + PRE_EFFECT, + DERIVED, + UNOWNED, + CLEAN, + UNINITIALIZED +} from '../constants.js'; + +/** + * @template V + * @param {import('../types.js').SignalFlags} flags + * @param {V} value + * @param {import('../types.js').Block | null} block + */ +function create_computation_signal(flags, value, block) { + /** @type {import('../types.js').ComputationSignal} */ + const signal = { + b: block, + c: null, + d: null, + e: null, + f: flags, + l: 0, + i: null, + r: null, + v: value, + w: 0, + x: null, + y: null + }; + + if (DEV) { + // @ts-expect-error + signal.inspect = new Set(); + } + + return signal; +} + +/** + * @param {import('../types.js').ComputationSignal} target_signal + * @param {import('../types.js').ComputationSignal} ref_signal + * @returns {void} + */ +export function push_reference(target_signal, ref_signal) { + const references = target_signal.r; + if (references === null) { + target_signal.r = [ref_signal]; + } else { + references.push(ref_signal); + } +} + +/** + * @param {import('../types.js').EffectType} type + * @param {(() => void | (() => void)) | ((b: import('../types.js').Block) => void | (() => void))} fn + * @param {boolean} sync + * @param {null | import('../types.js').Block} block + * @param {boolean} schedule + * @returns {import('../types.js').EffectSignal} + */ +function internal_create_effect(type, fn, sync, block, schedule) { + const signal = create_computation_signal(type | DIRTY, null, block); + signal.i = fn; + signal.x = current_component_context; + if (current_effect !== null) { + signal.l = current_effect.l + 1; + if ((type & MANAGED) === 0) { + push_reference(current_effect, signal); + } + } + if (schedule) { + schedule_effect(signal, sync); + } + return signal; +} + +/** + * @returns {boolean} + */ +export function effect_active() { + return current_effect ? (current_effect.f & MANAGED) === 0 : false; +} + +/** + * Internal representation of `$effect(...)` + * @param {() => void | (() => void)} fn + * @returns {import('../types.js').EffectSignal} + */ +export function user_effect(fn) { + if (current_effect === null) { + throw new Error( + 'ERR_SVELTE_ORPHAN_EFFECT' + + (DEV ? ': The Svelte $effect rune can only be used during component initialisation.' : '') + ); + } + + const apply_component_effect_heuristics = + current_effect.f & RENDER_EFFECT && + current_component_context !== null && + !current_component_context.m; + + const effect = internal_create_effect( + EFFECT, + fn, + false, + current_block, + !apply_component_effect_heuristics + ); + + if (apply_component_effect_heuristics) { + const context = /** @type {import('../types.js').ComponentContext} */ ( + current_component_context + ); + (context.e ??= []).push(effect); + } + + return effect; +} + +/** + * Internal representation of `$effect.root(...)` + * @param {() => void | (() => void)} fn + * @returns {() => void} + */ +export function user_root_effect(fn) { + const effect = render_effect(fn, current_block, true); + return () => { + destroy_signal(effect); + }; +} + +/** + * @param {() => void | (() => void)} fn + * @returns {import('../types.js').EffectSignal} + */ +export function effect(fn) { + return internal_create_effect(EFFECT, fn, false, current_block, true); +} + +/** + * @param {() => void | (() => void)} fn + * @returns {import('../types.js').EffectSignal} + */ +export function managed_effect(fn) { + return internal_create_effect(EFFECT | MANAGED, fn, false, current_block, true); +} + +/** + * @param {() => void | (() => void)} fn + * @param {boolean} sync + * @returns {import('../types.js').EffectSignal} + */ +export function managed_pre_effect(fn, sync) { + return internal_create_effect(PRE_EFFECT | MANAGED, fn, sync, current_block, true); +} + +/** + * Internal representation of `$effect.pre(...)` + * @param {() => void | (() => void)} fn + * @returns {import('../types.js').EffectSignal} + */ +export function pre_effect(fn) { + if (current_effect === null) { + throw new Error( + 'ERR_SVELTE_ORPHAN_EFFECT' + + (DEV + ? ': The Svelte $effect.pre rune can only be used during component initialisation.' + : '') + ); + } + const sync = current_effect !== null && (current_effect.f & RENDER_EFFECT) !== 0; + return internal_create_effect( + PRE_EFFECT, + () => { + const val = fn(); + flush_local_render_effects(); + return val; + }, + sync, + current_block, + true + ); +} + +/** + * This effect is used to ensure binding are kept in sync. We use a pre effect to ensure we run before the + * bindings which are in later effects. However, we don't use a pre_effect directly as we don't want to flush anything. + * + * @param {() => void | (() => void)} fn + * @returns {import('../types.js').EffectSignal} + */ +export function invalidate_effect(fn) { + return internal_create_effect(PRE_EFFECT, fn, true, current_block, true); +} + +/** + * @template {import('../types.js').Block} B + * @param {(block: B) => void | (() => void)} fn + * @param {any} block + * @param {any} managed + * @param {any} sync + * @returns {import('../types.js').EffectSignal} + */ +export function render_effect(fn, block = current_block, managed = false, sync = true) { + let flags = RENDER_EFFECT; + if (managed) { + flags |= MANAGED; + } + return internal_create_effect(flags, /** @type {any} */ (fn), sync, block, true); +} + +/** + * @template V + * @param {() => V} fn + * @returns {import('../types.js').ComputationSignal} + */ +/*#__NO_SIDE_EFFECTS__*/ +export function derived(fn) { + const is_unowned = current_effect === null; + const flags = is_unowned ? DERIVED | UNOWNED : DERIVED; + const signal = /** @type {import('../types.js').ComputationSignal} */ ( + create_computation_signal(flags | CLEAN, UNINITIALIZED, current_block) + ); + signal.i = fn; + signal.e = default_equals; + if (current_consumer !== null) { + push_reference(current_consumer, signal); + } + return signal; +} + +/** + * @template V + * @param {() => V} fn + * @returns {import('../types.js').ComputationSignal} + */ +/*#__NO_SIDE_EFFECTS__*/ +export function derived_safe_equal(fn) { + const signal = derived(fn); + signal.e = safe_equal; + return signal; +} diff --git a/packages/svelte/src/internal/client/reactivity/equality.js b/packages/svelte/src/internal/client/reactivity/equality.js new file mode 100644 index 0000000000..34673b2dda --- /dev/null +++ b/packages/svelte/src/internal/client/reactivity/equality.js @@ -0,0 +1,30 @@ +/** + * @param {unknown} a + * @param {unknown} b + * @returns {boolean} + */ +export function default_equals(a, b) { + return a === b; +} + +/** + * @param {unknown} a + * @param {unknown} b + * @returns {boolean} + */ +export function safe_not_equal(a, b) { + // eslint-disable-next-line eqeqeq + return a != a + ? // eslint-disable-next-line eqeqeq + b == b + : a !== b || (a !== null && typeof a === 'object') || typeof a === 'function'; +} + +/** + * @param {unknown} a + * @param {unknown} b + * @returns {boolean} + */ +export function safe_equal(a, b) { + return !safe_not_equal(a, b); +} diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js new file mode 100644 index 0000000000..d0b6de7cba --- /dev/null +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -0,0 +1,70 @@ +import { DEV } from 'esm-env'; +import { current_component_context } from '../runtime.js'; +import { default_equals, safe_equal } from './equality.js'; +import { CLEAN, SOURCE } from '../constants.js'; + +/** + * @template V + * @param {V} initial_value + * @returns {import('../types.js').SourceSignal} + */ +/*#__NO_SIDE_EFFECTS__*/ +export function source(initial_value) { + return create_source_signal(SOURCE | CLEAN, initial_value); +} + +/** + * @template V + * @param {V} initial_value + * @returns {import('../types.js').SourceSignal} + */ +/*#__NO_SIDE_EFFECTS__*/ +export function mutable_source(initial_value) { + const s = source(initial_value); + s.e = safe_equal; + + // bind the signal to the component context, in case we need to + // track updates to trigger beforeUpdate/afterUpdate callbacks + if (current_component_context) { + (current_component_context.d ??= []).push(s); + } + + return s; +} + +/** + * @template V + * @param {import('../types.js').SignalFlags} flags + * @param {V} value + * @returns {import('../types.js').SourceSignal | import('../types.js').SourceSignal & import('../types.js').SourceSignalDebug} + */ +function create_source_signal(flags, value) { + if (DEV) { + return { + // consumers + c: null, + // equals + e: default_equals, + // flags + f: flags, + // value + v: value, + // write version + w: 0, + // this is for DEV only + inspect: new Set() + }; + } + return { + // consumers + c: null, + // equals + e: default_equals, + // flags + f: flags, + // value + v: value, + // write version + w: 0 + }; +} 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 8a76d2d98d..a6e5f80d5f 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, @@ -31,21 +35,30 @@ import { remove } from './reconciler.js'; import { - render_effect, destroy_signal, is_signal, push_destroy_fn, execute_effect, untrack, - effect, flush_sync, current_block, - managed_effect, push, - current_component_context, pop, - deep_read + current_component_context, + deep_read, + get, + set, + is_signals_recorded, + inspect_fn } from './runtime.js'; +import { + render_effect, + effect, + managed_effect, + derived, + pre_effect, + user_effect +} from './reactivity/computations.js'; import { current_hydration_fragment, get_hydration_fragment, @@ -62,9 +75,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(); @@ -2691,3 +2706,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 47547636e1..8c93d1c158 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,41 +9,37 @@ import { object_freeze, object_prototype } from './utils.js'; +import { unstate } from './proxy.js'; +import { pre_effect } from './reactivity/computations.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'; - -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; -const MANAGED = 1 << 6; -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; + 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; @@ -58,15 +52,11 @@ 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 /** @type {null | import('./types.js').ComputationSignal} */ -let current_consumer = null; +export let current_consumer = null; /** @type {null | import('./types.js').EffectSignal} */ export let current_effect = null; @@ -86,16 +76,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 = []; @@ -144,133 +138,6 @@ export function batch_inspect(target, prop, receiver) { }; } -/** - * @param {unknown} a - * @param {unknown} b - * @returns {boolean} - */ -export function default_equals(a, b) { - return a === b; -} - -/** - * @template V - * @param {import('./types.js').SignalFlags} flags - * @param {V} value - * @returns {import('./types.js').SourceSignal | import('./types.js').SourceSignal & import('./types.js').SourceSignalDebug} - */ -function create_source_signal(flags, value) { - if (DEV) { - return { - // consumers - c: null, - // equals - e: default_equals, - // flags - f: flags, - // value - v: value, - // write version - w: 0, - // this is for DEV only - inspect: new Set() - }; - } - return { - // consumers - c: null, - // equals - e: default_equals, - // flags - f: flags, - // value - v: value, - // write version - w: 0 - }; -} - -/** - * @template V - * @param {import('./types.js').SignalFlags} flags - * @param {V} value - * @param {import('./types.js').Block | null} block - * @returns {import('./types.js').ComputationSignal | import('./types.js').ComputationSignal & import('./types.js').SourceSignalDebug} - */ -function create_computation_signal(flags, value, block) { - if (DEV) { - return { - // block - b: block, - // consumers - c: null, - // destroy - d: null, - // equals - e: null, - // flags - f: flags, - // init - i: null, - // level - l: 0, - // references - r: null, - // value - v: value, - // write version - w: 0, - // context: We can remove this if we get rid of beforeUpdate/afterUpdate - x: null, - // destroy - y: null, - // this is for DEV only - inspect: new Set() - }; - } - - return { - // block - b: block, - // consumers - c: null, - // destroy - d: null, - // equals - e: null, - // flags - f: flags, - // level - l: 0, - // init - i: null, - // references - r: null, - // value - v: value, - // write version - w: 0, - // context: We can remove this if we get rid of beforeUpdate/afterUpdate - x: null, - // destroy - y: null - }; -} - -/** - * @param {import('./types.js').ComputationSignal} target_signal - * @param {import('./types.js').ComputationSignal} ref_signal - * @returns {void} - */ -function push_reference(target_signal, ref_signal) { - const references = target_signal.r; - if (references === null) { - target_signal.r = [ref_signal]; - } else { - references.push(ref_signal); - } -} - /** * @template V * @param {import('./types.js').Signal} signal @@ -694,44 +561,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} */ @@ -807,12 +636,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; @@ -863,96 +687,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 @@ -1084,18 +818,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 @@ -1295,67 +1017,6 @@ export function destroy_signal(signal) { } } -/** - * @template V - * @param {() => V} init - * @returns {import('./types.js').ComputationSignal} - */ -/*#__NO_SIDE_EFFECTS__*/ -export function derived(init) { - const is_unowned = current_effect === null; - const flags = is_unowned ? DERIVED | UNOWNED : DERIVED; - const signal = /** @type {import('./types.js').ComputationSignal} */ ( - create_computation_signal(flags | CLEAN, UNINITIALIZED, current_block) - ); - signal.i = init; - signal.e = default_equals; - if (current_consumer !== null) { - push_reference(current_consumer, signal); - } - return signal; -} - -/** - * @template V - * @param {() => V} init - * @returns {import('./types.js').ComputationSignal} - */ -/*#__NO_SIDE_EFFECTS__*/ -export function derived_safe_equal(init) { - const signal = derived(init); - signal.e = safe_equal; - return signal; -} - -/** - * @template V - * @param {V} initial_value - * @returns {import('./types.js').SourceSignal} - */ -/*#__NO_SIDE_EFFECTS__*/ -export function source(initial_value) { - return create_source_signal(SOURCE | CLEAN, initial_value); -} - -/** - * @template V - * @param {V} initial_value - * @returns {import('./types.js').SourceSignal} - */ -/*#__NO_SIDE_EFFECTS__*/ -export function mutable_source(initial_value) { - const s = source(initial_value); - s.e = safe_equal; - - // bind the signal to the component context, in case we need to - // track updates to trigger beforeUpdate/afterUpdate callbacks - if (current_component_context) { - (current_component_context.d ??= []).push(s); - } - - return s; -} - /** * Use `untrack` to prevent something from being treated as an `$effect`/`$derived` dependency. * @@ -1374,170 +1035,6 @@ export function untrack(fn) { } } -/** - * @param {import('./types.js').EffectType} type - * @param {(() => void | (() => void)) | ((b: import('./types.js').Block) => void | (() => void))} init - * @param {boolean} sync - * @param {null | import('./types.js').Block} block - * @param {boolean} schedule - * @returns {import('./types.js').EffectSignal} - */ -function internal_create_effect(type, init, sync, block, schedule) { - const signal = create_computation_signal(type | DIRTY, null, block); - signal.i = init; - signal.x = current_component_context; - if (current_effect !== null) { - signal.l = current_effect.l + 1; - if ((type & MANAGED) === 0) { - push_reference(current_effect, signal); - } - } - if (schedule) { - schedule_effect(signal, sync); - } - return signal; -} - -/** - * @returns {boolean} - */ -export function effect_active() { - return current_effect ? (current_effect.f & MANAGED) === 0 : false; -} - -/** - * @param {() => void | (() => void)} init - * @returns {import('./types.js').EffectSignal} - */ -export function user_effect(init) { - if (current_effect === null) { - throw new Error( - 'ERR_SVELTE_ORPHAN_EFFECT' + - (DEV ? ': The Svelte $effect rune can only be used during component initialisation.' : '') - ); - } - const apply_component_effect_heuristics = - current_effect.f & RENDER_EFFECT && - current_component_context !== null && - !current_component_context.m; - const effect = internal_create_effect( - EFFECT, - init, - false, - current_block, - !apply_component_effect_heuristics - ); - if (apply_component_effect_heuristics) { - const context = /** @type {import('./types.js').ComponentContext} */ ( - current_component_context - ); - (context.e ??= []).push(effect); - } - return effect; -} - -/** - * @param {() => void | (() => void)} init - * @returns {() => void} - */ -export function user_root_effect(init) { - const effect = managed_render_effect(init); - return () => { - destroy_signal(effect); - }; -} - -/** - * @param {() => void | (() => void)} init - * @returns {import('./types.js').EffectSignal} - */ -export function effect(init) { - return internal_create_effect(EFFECT, init, false, current_block, true); -} - -/** - * @param {() => void | (() => void)} init - * @returns {import('./types.js').EffectSignal} - */ -export function managed_effect(init) { - return internal_create_effect(EFFECT | MANAGED, init, false, current_block, true); -} - -/** - * @param {() => void | (() => void)} init - * @param {boolean} sync - * @returns {import('./types.js').EffectSignal} - */ -export function managed_pre_effect(init, sync) { - return internal_create_effect(PRE_EFFECT | MANAGED, init, sync, current_block, true); -} - -/** - * @param {() => void | (() => void)} init - * @returns {import('./types.js').EffectSignal} - */ -export function pre_effect(init) { - if (current_effect === null) { - throw new Error( - 'ERR_SVELTE_ORPHAN_EFFECT' + - (DEV - ? ': The Svelte $effect.pre rune can only be used during component initialisation.' - : '') - ); - } - const sync = current_effect !== null && (current_effect.f & RENDER_EFFECT) !== 0; - return internal_create_effect( - PRE_EFFECT, - () => { - const val = init(); - flush_local_render_effects(); - return val; - }, - sync, - current_block, - true - ); -} - -/** - * This effect is used to ensure binding are kept in sync. We use a pre effect to ensure we run before the - * bindings which are in later effects. However, we don't use a pre_effect directly as we don't want to flush anything. - * - * @param {() => void | (() => void)} init - * @returns {import('./types.js').EffectSignal} - */ -export function invalidate_effect(init) { - return internal_create_effect(PRE_EFFECT, init, true, current_block, true); -} - -/** - * @template {import('./types.js').Block} B - * @param {(block: B) => void | (() => void)} init - * @param {any} block - * @param {any} managed - * @param {any} sync - * @returns {import('./types.js').EffectSignal} - */ -export function render_effect(init, block = current_block, managed = false, sync = true) { - let flags = RENDER_EFFECT; - if (managed) { - flags |= MANAGED; - } - return internal_create_effect(flags, /** @type {any} */ (init), sync, block, true); -} - -/** - * @template {import('./types.js').Block} B - * @param {(block: B) => void | (() => void)} init - * @param {any} block - * @param {any} sync - * @returns {import('./types.js').EffectSignal} - */ -export function managed_render_effect(init, block = current_block, sync = true) { - const flags = RENDER_EFFECT | MANAGED; - return internal_create_effect(flags, /** @type {any} */ (init), sync, block, true); -} - /** * @template V * @param {import('./types.js').ComputationSignal} signal @@ -1562,7 +1059,7 @@ const STATUS_MASK = ~(DIRTY | MAYBE_DIRTY | CLEAN); * @param {number} status * @returns {void} */ -export function set_signal_status(signal, status) { +function set_signal_status(signal, status) { signal.f = (signal.f & STATUS_MASK) | status; } @@ -1579,154 +1076,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; - }; -} - -/** - * @param {unknown} a - * @param {unknown} b - * @returns {boolean} - */ -export function safe_not_equal(a, b) { - // eslint-disable-next-line eqeqeq - return a != a - ? // eslint-disable-next-line eqeqeq - b == b - : a !== b || (a !== null && typeof a === 'object') || typeof a === 'function'; -} - -/** - * @param {unknown} a - * @param {unknown} b - * @returns {boolean} - */ -export function safe_equal(a, b) { - return !safe_not_equal(a, b); -} - /** @returns {Map} */ export function get_or_init_context_map() { const component_context = current_component_context; @@ -1755,23 +1104,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] @@ -1794,17 +1126,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] @@ -1827,36 +1148,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 @@ -1881,15 +1172,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 @@ -1951,53 +1233,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 01350350e9..59eb3ad60d 100644 --- a/packages/svelte/src/internal/client/transitions.js +++ b/packages/svelte/src/internal/client/transitions.js @@ -8,19 +8,17 @@ 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 { current_block, current_effect, destroy_signal, - effect, execute_effect, - managed_effect, - managed_pre_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/client/validate.js b/packages/svelte/src/internal/client/validate.js index c6f590a420..83124697a8 100644 --- a/packages/svelte/src/internal/client/validate.js +++ b/packages/svelte/src/internal/client/validate.js @@ -1,5 +1,4 @@ -import { EACH_INDEX_REACTIVE } from '../../constants.js'; -import { source, untrack } from './runtime.js'; +import { untrack } from './runtime.js'; import { is_array } from './utils.js'; /** regex of all html void element names */ diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js index 72026e9389..741391ebc6 100644 --- a/packages/svelte/src/internal/index.js +++ b/packages/svelte/src/internal/index.js @@ -1,44 +1,23 @@ export { - store_get, get, set, set_sync, invalidate_inner_signals, - source, - mutable_source, - derived, - derived_safe_equal, - prop, - user_effect, - render_effect, - pre_effect, - invalidate_effect, flushSync, - bubble_event, - safe_equal, 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, - reactive_import, - effect_active, - user_root_effect, inspect, unwrap, freeze, - init, deep_read } from './client/runtime.js'; export * from './client/dev/ownership.js'; @@ -46,6 +25,10 @@ export { await_block as await } from './client/dom/blocks/await.js'; export { if_block as if } from './client/dom/blocks/if.js'; export { key_block as key } from './client/dom/blocks/key.js'; 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'; +export * from './client/reactivity/store.js'; export * from './client/render.js'; export * from './client/validate.js'; export { raf } from './client/timing.js'; diff --git a/packages/svelte/src/main/main-client.js b/packages/svelte/src/main/main-client.js index 1b2ba77725..690f6946a2 100644 --- a/packages/svelte/src/main/main-client.js +++ b/packages/svelte/src/main/main-client.js @@ -1,10 +1,10 @@ import { current_component_context, get_or_init_context_map, - untrack, - user_effect + untrack } from '../internal/client/runtime.js'; import { is_array } from '../internal/client/utils.js'; +import { user_effect } from '../internal/index.js'; /** * The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM. diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 9cecf3d5ea..8b91601a3b 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1,5 +1,12 @@ import { describe, assert, it } from 'vitest'; import * as $ from '../../src/internal/client/runtime'; +import { + derived, + effect, + render_effect, + user_effect +} from '../../src/internal/client/reactivity/computations'; +import { source } from '../../src/internal/client/reactivity/sources'; import type { ComputationSignal } from '../../src/internal/client/types'; /** @@ -13,7 +20,7 @@ function run_test(runes: boolean, fn: (runes: boolean) => () => void) { $.push({}, runes); // Create a render context so that effect validations etc don't fail let execute: any; - const signal = $.render_effect( + const signal = render_effect( () => { execute = fn(runes); }, @@ -36,9 +43,9 @@ describe('signals', () => { test('effect with state and derived in it', () => { const log: string[] = []; - let count = $.source(0); - let double = $.derived(() => $.get(count) * 2); - $.effect(() => { + let count = source(0); + let double = derived(() => $.get(count) * 2); + effect(() => { log.push(`${$.get(count)}:${$.get(double)}`); }); @@ -53,13 +60,13 @@ describe('signals', () => { test('multiple effects with state and derived in it#1', () => { const log: string[] = []; - let count = $.source(0); - let double = $.derived(() => $.get(count) * 2); + let count = source(0); + let double = derived(() => $.get(count) * 2); - $.effect(() => { + effect(() => { log.push(`A:${$.get(count)}:${$.get(double)}`); }); - $.effect(() => { + effect(() => { log.push(`B:${$.get(double)}`); }); @@ -74,13 +81,13 @@ describe('signals', () => { test('multiple effects with state and derived in it#2', () => { const log: string[] = []; - let count = $.source(0); - let double = $.derived(() => $.get(count) * 2); + let count = source(0); + let double = derived(() => $.get(count) * 2); - $.effect(() => { + effect(() => { log.push(`A:${$.get(double)}`); }); - $.effect(() => { + effect(() => { log.push(`B:${$.get(count)}:${$.get(double)}`); }); @@ -95,10 +102,10 @@ describe('signals', () => { test('derived from state', () => { const log: number[] = []; - let count = $.source(0); - let double = $.derived(() => $.get(count) * 2); + let count = source(0); + let double = derived(() => $.get(count) * 2); - $.effect(() => { + effect(() => { log.push($.get(double)); }); @@ -113,11 +120,11 @@ describe('signals', () => { test('derived from derived', () => { const log: number[] = []; - let count = $.source(0); - let double = $.derived(() => $.get(count) * 2); - let quadruple = $.derived(() => $.get(double) * 2); + let count = source(0); + let double = derived(() => $.get(count) * 2); + let quadruple = derived(() => $.get(double) * 2); - $.effect(() => { + effect(() => { log.push($.get(quadruple)); }); @@ -136,20 +143,20 @@ describe('signals', () => { 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 = $.source(0); - const B = $.source(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(() => { + const A = source(0); + const B = source(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(() => { + effect(() => { res.push($.get(G)); }); - $.effect(() => { + effect(() => { res.push(hard($.get(F), 'J')); }); @@ -176,13 +183,13 @@ describe('signals', () => { test('effects correctly handle unowned derived values that do not change', () => { const log: number[] = []; - let count = $.source(0); + let count = source(0); const read = () => { - const x = $.derived(() => ({ count: $.get(count) })); + const x = derived(() => ({ count: $.get(count) })); return $.get(x); }; - const derivedCount = $.derived(() => read().count); - $.user_effect(() => { + const derivedCount = derived(() => read().count); + user_effect(() => { log.push($.get(derivedCount)); }); @@ -204,11 +211,11 @@ describe('signals', () => { return () => { const nested: ComputationSignal[] = []; - const a = $.source(0); - const b = $.source(0); - const c = $.derived(() => { - const a_2 = $.derived(() => $.get(a) + '!'); - const b_2 = $.derived(() => $.get(b) + '?'); + const a = source(0); + const b = source(0); + const 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) }; @@ -233,8 +240,8 @@ describe('signals', () => { }); // outside of test function so that they are unowned signals - let count = $.source(0); - let calc = $.derived(() => { + let count = source(0); + let calc = derived(() => { if ($.get(count) >= 2) { return 'limit'; } @@ -244,7 +251,7 @@ describe('signals', () => { test('effect with derived using unowned derived every time', () => { const log: Array = []; - const effect = $.user_effect(() => { + const effect = user_effect(() => { log.push($.get(calc)); }); @@ -263,18 +270,18 @@ describe('signals', () => { }; }); - let no_deps = $.derived(() => { + let no_deps = derived(() => { return []; }); test('two effects with an unowned derived that has no depedencies', () => { const log: Array> = []; - $.render_effect(() => { + render_effect(() => { log.push($.get(no_deps)); }); - $.render_effect(() => { + render_effect(() => { log.push($.get(no_deps)); }); @@ -284,19 +291,19 @@ describe('signals', () => { }; }); - let some_state = $.source({}); - let some_deps = $.derived(() => { + let some_state = source({}); + let some_deps = derived(() => { return [$.get(some_state)]; }); test('two effects with an unowned derived that has some depedencies', () => { const log: Array> = []; - $.render_effect(() => { + render_effect(() => { log.push($.get(some_deps)); }); - $.render_effect(() => { + render_effect(() => { log.push($.get(some_deps)); }); @@ -309,8 +316,8 @@ describe('signals', () => { test('schedules rerun when writing to signal before reading it', (runes) => { if (!runes) return () => {}; - const value = $.source({ count: 0 }); - $.user_effect(() => { + const value = source({ count: 0 }); + user_effect(() => { $.set(value, { count: 0 }); $.get(value); });