You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte/packages/svelte/src/internal/client/runtime.js

2133 lines
57 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { DEV } from 'esm-env';
import { subscribe_to_store } from '../../store/utils.js';
import { EMPTY_FUNC, run_all } from '../common.js';
import {
array_prototype,
get_descriptor,
get_descriptors,
get_prototype_of,
is_array,
is_frozen,
object_freeze,
object_prototype
} from './utils.js';
import {
PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE,
PROPS_IS_RUNES,
PROPS_IS_UPDATED
} from '../../constants.js';
import { READONLY_SYMBOL, STATE_SYMBOL, proxy, readonly, 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;
const IS_EFFECT = EFFECT | PRE_EFFECT | RENDER_EFFECT;
const FLUSH_MICROTASK = 0;
const FLUSH_SYNC = 1;
export const UNINITIALIZED = Symbol();
export const LAZY_PROPERTY = 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;
// Handle effect queues
/** @type {import('./types.js').EffectSignal[]} */
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;
/** @type {null | import('./types.js').EffectSignal} */
export let current_effect = null;
/** @type {null | import('./types.js').Signal[]} */
let current_dependencies = null;
let current_dependencies_index = 0;
/** @type {null | import('./types.js').Signal[]} */
let current_untracked_writes = null;
/** @type {null | import('./types.js').SignalDebug} */
let last_inspected_signal = null;
/** If `true`, `get`ting the signal should not register it as a dependency */
export let current_untracking = false;
/** Exists to opt out of the mutation validation for stores which may be set for the first time during a derivation */
let ignore_mutation_validation = false;
// 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;
let captured_signals = new Set();
/** @type {Function | null} */
let inspect_fn = null;
/** @type {Array<import('./types.js').SignalDebug>} */
let inspect_captured_signals = [];
// Handle rendering tree blocks and anchors
/** @type {null | import('./types.js').Block} */
export let current_block = null;
// Handling runtime component context
/** @type {import('./types.js').ComponentContext | null} */
export let current_component_context = null;
export let is_ssr = false;
export let updating_derived = false;
/**
* @param {boolean} ssr
* @returns {void}
*/
export function set_is_ssr(ssr) {
is_ssr = ssr;
}
/**
* @param {null | import('./types.js').ComponentContext} context
* @returns {boolean}
*/
function is_runes(context) {
const component_context = context || current_component_context;
return component_context !== null && component_context.r;
}
/**
* @param {import('./types.js').ProxyStateObject} target
* @param {string | symbol} prop
* @param {any} receiver
*/
export function batch_inspect(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
return function () {
const previously_batching_effect = is_batching_effect;
is_batching_effect = true;
try {
return Reflect.apply(value, receiver, arguments);
} finally {
is_batching_effect = previously_batching_effect;
if (last_inspected_signal !== null) {
for (const fn of last_inspected_signal.inspect) fn();
last_inspected_signal = null;
}
}
};
}
/**
* @param {null | import('./types.js').ComponentContext} context_stack_item
* @returns {void}
*/
export function set_current_component_context(context_stack_item) {
current_component_context = context_stack_item;
}
/**
* @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<V> | import('./types.js').SourceSignal<V> & 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,
// context: We can remove this if we get rid of beforeUpdate/afterUpdate
x: null,
// this is for DEV only
inspect: new Set()
};
}
return {
// consumers
c: null,
// equals
e: default_equals,
// flags
f: flags,
// value
v: value,
// context: We can remove this if we get rid of beforeUpdate/afterUpdate
x: null
};
}
/**
* @template V
* @param {import('./types.js').SignalFlags} flags
* @param {V} value
* @param {import('./types.js').Block | null} block
* @returns {import('./types.js').ComputationSignal<V> | import('./types.js').ComputationSignal<V> & 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,
// 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,
// 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<V>} signal
* @returns {boolean}
*/
function is_signal_dirty(signal) {
const flags = signal.f;
if ((flags & DIRTY) !== 0 || signal.v === UNINITIALIZED) {
return true;
}
if ((flags & MAYBE_DIRTY) !== 0) {
const dependencies = /** @type {import('./types.js').ComputationSignal<V>} **/ (signal).d;
if (dependencies !== null) {
const length = dependencies.length;
let i;
for (i = 0; i < length; i++) {
const dependency = dependencies[i];
if ((dependency.f & MAYBE_DIRTY) !== 0 && !is_signal_dirty(dependency)) {
set_signal_status(dependency, CLEAN);
continue;
}
// The flags can be marked as dirty from the above is_signal_dirty call.
if ((dependency.f & DIRTY) !== 0) {
if ((dependency.f & DERIVED) !== 0) {
update_derived(
/** @type {import('./types.js').ComputationSignal<V>} **/ (dependency),
true
);
// Might have been mutated from above get.
if ((signal.f & DIRTY) !== 0) {
return true;
}
} else {
return true;
}
}
}
}
}
return false;
}
/**
* @template V
* @param {import('./types.js').ComputationSignal<V>} signal
* @returns {V}
*/
function execute_signal_fn(signal) {
const init = signal.i;
const previous_dependencies = current_dependencies;
const previous_dependencies_index = current_dependencies_index;
const previous_untracked_writes = current_untracked_writes;
const previous_consumer = current_consumer;
const previous_block = current_block;
const previous_component_context = current_component_context;
const previous_skip_consumer = current_skip_consumer;
const is_render_effect = (signal.f & RENDER_EFFECT) !== 0;
const previous_untracking = current_untracking;
current_dependencies = /** @type {null | import('./types.js').Signal[]} */ (null);
current_dependencies_index = 0;
current_untracked_writes = null;
current_consumer = signal;
current_block = signal.b;
current_component_context = signal.x;
current_skip_consumer = !is_flushing_effect && (signal.f & UNOWNED) !== 0;
current_untracking = false;
// Render effects are invoked when the UI is about to be updated - run beforeUpdate at that point
if (is_render_effect && current_component_context?.u != null) {
// update_callbacks.execute()
current_component_context.u.e();
}
try {
let res;
if (is_render_effect) {
res =
/** @type {(block: import('./types.js').Block, signal: import('./types.js').Signal) => V} */ (
init
)(
/** @type {import('./types.js').Block} */ (signal.b),
/** @type {import('./types.js').Signal} */ (signal)
);
} else {
res = /** @type {() => V} */ (init)();
}
let dependencies = /** @type {import('./types.js').Signal<unknown>[]} **/ (signal.d);
if (current_dependencies !== null) {
let i;
if (dependencies !== null) {
const deps_length = dependencies.length;
// Include any dependencies up until the current_dependencies_index.
const full_current_dependencies =
current_dependencies_index === 0
? current_dependencies
: dependencies.slice(0, current_dependencies_index).concat(current_dependencies);
const current_dep_length = full_current_dependencies.length;
// If we have more than 16 elements in the array then use a Set for faster performance
// TODO: evaluate if we should always just use a Set or not here?
const full_current_dependencies_set =
current_dep_length > 16 && deps_length - current_dependencies_index > 1
? new Set(full_current_dependencies)
: null;
for (i = current_dependencies_index; i < deps_length; i++) {
const dependency = dependencies[i];
if (
full_current_dependencies_set !== null
? !full_current_dependencies_set.has(dependency)
: !full_current_dependencies.includes(dependency)
) {
remove_consumer(signal, dependency);
}
}
}
if (dependencies !== null && current_dependencies_index > 0) {
dependencies.length = current_dependencies_index + current_dependencies.length;
for (i = 0; i < current_dependencies.length; i++) {
dependencies[current_dependencies_index + i] = current_dependencies[i];
}
} else {
signal.d = /** @type {import('./types.js').Signal<V>[]} **/ (
dependencies = current_dependencies
);
}
if (!current_skip_consumer) {
for (i = current_dependencies_index; i < dependencies.length; i++) {
const dependency = dependencies[i];
const consumers = dependency.c;
if (consumers === null) {
dependency.c = [signal];
} else if (consumers[consumers.length - 1] !== signal) {
consumers.push(signal);
}
}
}
} else if (dependencies !== null && current_dependencies_index < dependencies.length) {
remove_consumers(signal, current_dependencies_index);
dependencies.length = current_dependencies_index;
}
return res;
} finally {
current_dependencies = previous_dependencies;
current_dependencies_index = previous_dependencies_index;
current_untracked_writes = previous_untracked_writes;
current_consumer = previous_consumer;
current_block = previous_block;
current_component_context = previous_component_context;
current_skip_consumer = previous_skip_consumer;
current_untracking = previous_untracking;
}
}
/**
* @template V
* @param {import('./types.js').ComputationSignal<V>} signal
* @param {import('./types.js').Signal<V>} dependency
* @returns {void}
*/
function remove_consumer(signal, dependency) {
const consumers = dependency.c;
let consumers_length = 0;
if (consumers !== null) {
consumers_length = consumers.length - 1;
const index = consumers.indexOf(signal);
if (index !== -1) {
if (consumers_length === 0) {
dependency.c = null;
} else {
// Swap with last element and then remove.
consumers[index] = consumers[consumers_length];
consumers.pop();
}
}
}
if (consumers_length === 0 && (dependency.f & UNOWNED) !== 0) {
// If the signal is unowned then we need to make sure to change it to dirty.
set_signal_status(dependency, DIRTY);
remove_consumers(/** @type {import('./types.js').ComputationSignal<V>} **/ (dependency), 0);
}
}
/**
* @template V
* @param {import('./types.js').ComputationSignal<V>} signal
* @param {number} start_index
* @returns {void}
*/
function remove_consumers(signal, start_index) {
const dependencies = signal.d;
if (dependencies !== null) {
const active_dependencies = start_index === 0 ? null : dependencies.slice(0, start_index);
let i;
for (i = start_index; i < dependencies.length; i++) {
const dependency = dependencies[i];
// Avoid removing a consumer if we know that it is active (start_index will not be 0)
if (active_dependencies === null || !active_dependencies.includes(dependency)) {
remove_consumer(signal, dependency);
}
}
}
}
/**
* @template V
* @param {import('./types.js').ComputationSignal<V>} signal
* @returns {void}
*/
function destroy_references(signal) {
const references = signal.r;
signal.r = null;
if (references !== null) {
let i;
for (i = 0; i < references.length; i++) {
destroy_signal(references[i]);
}
}
}
/**
* @param {import('./types.js').Block} block
* @param {unknown} error
* @returns {void}
*/
function report_error(block, error) {
/** @type {import('./types.js').Block | null} */
let current_block = block;
if (current_block !== null) {
throw error;
}
}
/**
* @param {import('./types.js').EffectSignal} signal
* @returns {void}
*/
export function execute_effect(signal) {
if ((signal.f & DESTROYED) !== 0) {
return;
}
const teardown = signal.v;
const previous_effect = current_effect;
current_effect = signal;
try {
destroy_references(signal);
if (teardown !== null) {
teardown();
}
const possible_teardown = execute_signal_fn(signal);
if (typeof possible_teardown === 'function') {
signal.v = possible_teardown;
}
} catch (error) {
const block = signal.b;
if (block !== null) {
report_error(block, error);
} else {
throw error;
}
} finally {
current_effect = previous_effect;
}
const component_context = signal.x;
if (
is_runes(component_context) && // Don't rerun pre effects more than once to accomodate for "$: only runs once" behavior
(signal.f & PRE_EFFECT) !== 0 &&
current_queued_pre_and_render_effects.length > 0
) {
flush_local_pre_effects(component_context);
}
}
function infinite_loop_guard() {
if (flush_count > 100) {
throw new Error(
'ERR_SVELTE_TOO_MANY_UPDATES' +
(DEV
? ': Maximum update depth exceeded. This can happen when a reactive block or effect ' +
'repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops.'
: '')
);
}
flush_count++;
}
/**
* @param {Array<import('./types.js').EffectSignal>} effects
* @returns {void}
*/
function flush_queued_effects(effects) {
const length = effects.length;
if (length > 0) {
infinite_loop_guard();
const previously_flushing_effect = is_flushing_effect;
is_flushing_effect = true;
try {
let i;
for (i = 0; i < length; i++) {
const signal = effects[i];
const flags = signal.f;
if ((flags & (DESTROYED | INERT)) === 0) {
if (is_signal_dirty(signal)) {
set_signal_status(signal, CLEAN);
execute_effect(signal);
} else if ((flags & MAYBE_DIRTY) !== 0) {
set_signal_status(signal, CLEAN);
}
}
}
} finally {
is_flushing_effect = previously_flushing_effect;
}
effects.length = 0;
}
}
function process_microtask() {
is_micro_task_queued = false;
if (flush_count > 101) {
return;
}
const previous_queued_pre_and_render_effects = current_queued_pre_and_render_effects;
const previous_queued_effects = current_queued_effects;
current_queued_pre_and_render_effects = [];
current_queued_effects = [];
flush_queued_effects(previous_queued_pre_and_render_effects);
flush_queued_effects(previous_queued_effects);
if (!is_micro_task_queued) {
flush_count = 0;
}
}
/**
* @param {import('./types.js').EffectSignal} signal
* @param {boolean} sync
* @returns {void}
*/
export function schedule_effect(signal, sync) {
const flags = signal.f;
if (sync) {
const previously_flushing_effect = is_flushing_effect;
try {
is_flushing_effect = true;
execute_effect(signal);
set_signal_status(signal, CLEAN);
} finally {
is_flushing_effect = previously_flushing_effect;
}
} else {
if (current_scheduler_mode === FLUSH_MICROTASK) {
if (!is_micro_task_queued) {
is_micro_task_queued = true;
queueMicrotask(process_microtask);
}
}
if ((flags & EFFECT) !== 0) {
current_queued_effects.push(signal);
// Prevent any nested user effects from potentially triggering
// before this effect is scheduled. We know they will be destroyed
// so we can make them inert to avoid having to find them in the
// queue and remove them.
if ((flags & MANAGED) === 0) {
mark_subtree_children_inert(signal, true);
}
} else {
// We need to ensure we insert the signal in the right topological order. In other words,
// we need to evaluate where to insert the signal based off its level and whether or not it's
// a pre-effect and within the same block. By checking the signals in the queue in reverse order
// we can find the right place quickly. TODO: maybe opt to use a linked list rather than an array
// for these operations.
const length = current_queued_pre_and_render_effects.length;
let should_append = length === 0;
if (!should_append) {
const target_level = signal.l;
const target_block = signal.b;
const is_pre_effect = (flags & PRE_EFFECT) !== 0;
let target_signal;
let is_target_pre_effect;
let i = length;
while (true) {
target_signal = current_queued_pre_and_render_effects[--i];
if (target_signal.l <= target_level) {
if (i + 1 === length) {
should_append = true;
} else {
is_target_pre_effect = (target_signal.f & PRE_EFFECT) !== 0;
if (target_signal.b !== target_block || (is_target_pre_effect && !is_pre_effect)) {
i++;
}
current_queued_pre_and_render_effects.splice(i, 0, signal);
}
break;
}
if (i === 0) {
current_queued_pre_and_render_effects.unshift(signal);
break;
}
}
}
if (should_append) {
current_queued_pre_and_render_effects.push(signal);
}
}
}
}
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}
*/
export function flush_local_render_effects() {
const effects = [];
for (let i = 0; i < current_queued_pre_and_render_effects.length; i++) {
const effect = current_queued_pre_and_render_effects[i];
if ((effect.f & RENDER_EFFECT) !== 0 && effect.x === current_component_context) {
effects.push(effect);
current_queued_pre_and_render_effects.splice(i, 1);
i--;
}
}
flush_queued_effects(effects);
}
/**
* @param {null | import('./types.js').ComponentContext} context
* @returns {void}
*/
export function flush_local_pre_effects(context) {
const effects = [];
for (let i = 0; i < current_queued_pre_and_render_effects.length; i++) {
const effect = current_queued_pre_and_render_effects[i];
if ((effect.f & PRE_EFFECT) !== 0 && effect.x === context) {
effects.push(effect);
current_queued_pre_and_render_effects.splice(i, 1);
i--;
}
}
flush_queued_effects(effects);
}
/**
* Synchronously flushes any pending state changes and those that result from it.
* @param {() => void} [fn]
* @returns {void}
*/
export function flushSync(fn) {
const previous_scheduler_mode = current_scheduler_mode;
const previous_queued_pre_and_render_effects = current_queued_pre_and_render_effects;
const previous_queued_effects = current_queued_effects;
try {
infinite_loop_guard();
/** @type {import('./types.js').EffectSignal[]} */
const pre_and_render_effects = [];
/** @type {import('./types.js').EffectSignal[]} */
const effects = [];
current_scheduler_mode = FLUSH_SYNC;
current_queued_pre_and_render_effects = pre_and_render_effects;
current_queued_effects = effects;
flush_queued_effects(previous_queued_pre_and_render_effects);
flush_queued_effects(previous_queued_effects);
if (fn !== undefined) {
fn();
}
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_count = 0;
} finally {
current_scheduler_mode = previous_scheduler_mode;
current_queued_pre_and_render_effects = previous_queued_pre_and_render_effects;
current_queued_effects = previous_queued_effects;
}
}
/**
* Returns a promise that resolves once any pending state changes have been applied.
* @returns {Promise<void>}
*/
export async function tick() {
await Promise.resolve();
// By calling flushSync we guarantee that any pending state changes are applied after one tick.
// TODO look into whether we can make flushing subsequent updates synchronously in the future.
flushSync();
}
/**
* @template V
* @param {import('./types.js').ComputationSignal<V>} signal
* @param {boolean} force_schedule
* @returns {void}
*/
function update_derived(signal, force_schedule) {
const previous_updating_derived = updating_derived;
updating_derived = true;
destroy_references(signal);
const value = execute_signal_fn(signal);
updating_derived = previous_updating_derived;
const status = current_skip_consumer || (signal.f & UNOWNED) !== 0 ? DIRTY : CLEAN;
set_signal_status(signal, status);
const equals = /** @type {import('./types.js').EqualsFunctions} */ (signal.e);
if (!equals(value, signal.v)) {
signal.v = value;
mark_signal_consumers(signal, DIRTY, force_schedule);
// @ts-expect-error
if (DEV && signal.inspect && force_schedule) {
for (const fn of /** @type {import('./types.js').SignalDebug} */ (signal).inspect) fn();
}
}
}
/**
* 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<V> | 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: EMPTY_FUNC
};
// 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<V> | null | undefined} store
* @param {import('./types.js').SourceSignal<V>} source
*/
function connect_store_to_signal(store, source) {
if (store == null) {
set(source, undefined);
return EMPTY_FUNC;
}
/** @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<V>} 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) {
onDestroy(() => {
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<V>} signal
* @returns {V}
*/
export function get(signal) {
// @ts-expect-error
if (DEV && signal.inspect && inspect_fn) {
/** @type {import('./types.js').SignalDebug} */ (signal).inspect.add(inspect_fn);
// @ts-expect-error
inspect_captured_signals.push(signal);
}
const flags = signal.f;
if ((flags & DESTROYED) !== 0) {
return signal.v;
}
if (is_signals_recorded) {
captured_signals.add(signal);
}
// Register the dependency on the current consumer signal.
if (current_consumer !== null && (current_consumer.f & MANAGED) === 0 && !current_untracking) {
const unowned = (current_consumer.f & UNOWNED) !== 0;
const dependencies = current_consumer.d;
if (
current_dependencies === null &&
dependencies !== null &&
dependencies[current_dependencies_index] === signal &&
!(unowned && current_effect !== null)
) {
current_dependencies_index++;
} else if (
dependencies === null ||
current_dependencies_index === 0 ||
dependencies[current_dependencies_index - 1] !== signal
) {
if (current_dependencies === null) {
current_dependencies = [signal];
} else if (signal !== current_dependencies[current_dependencies.length - 1]) {
current_dependencies.push(signal);
}
}
if (
current_untracked_writes !== null &&
current_effect !== null &&
(current_effect.f & CLEAN) !== 0 &&
current_untracked_writes.includes(signal)
) {
set_signal_status(current_effect, DIRTY);
schedule_effect(current_effect, false);
}
}
if ((flags & DERIVED) !== 0 && is_signal_dirty(signal)) {
if (DEV) {
// we want to avoid tracking indirect dependencies
const previous_inspect_fn = inspect_fn;
inspect_fn = null;
update_derived(/** @type {import('./types.js').ComputationSignal<V>} **/ (signal), false);
inspect_fn = previous_inspect_fn;
} else {
update_derived(/** @type {import('./types.js').ComputationSignal<V>} **/ (signal), false);
}
}
return signal.v;
}
/**
* @template V
* @param {import('./types.js').Signal<V>} signal
* @param {V} value
* @returns {V}
*/
export function set(signal, value) {
set_signal_value(signal, value);
return value;
}
/**
* @template V
* @param {import('./types.js').Signal<V>} signal
* @param {V} value
* @returns {void}
*/
export function set_sync(signal, value) {
flushSync(() => set(signal, value));
}
/**
* Invokes a function and captures all signals that are read during the invocation,
* then invalidates them.
* @param {() => any} fn
*/
export function invalidate_inner_signals(fn) {
var previous_is_signals_recorded = is_signals_recorded;
var previous_captured_signals = captured_signals;
is_signals_recorded = true;
captured_signals = new Set();
var captured = captured_signals;
var signal;
try {
untrack(fn);
} finally {
is_signals_recorded = previous_is_signals_recorded;
if (is_signals_recorded) {
for (signal of captured_signals) {
previous_captured_signals.add(signal);
}
}
captured_signals = previous_captured_signals;
}
for (signal of captured) {
mutate(signal, null /* doesnt matter */);
}
}
/**
* @template V
* @param {import('./types.js').Signal<V>} source
* @param {V} value
*/
export function mutate(source, value) {
set_signal_value(
source,
untrack(() => get(source))
);
return value;
}
/**
* Updates a store with a new value.
* @param {import('./types.js').Store<V>} 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
* @param {Set<import('./types.js').Block>} [visited_blocks]
* @returns {void}
*/
function mark_subtree_children_inert(signal, inert, visited_blocks) {
const references = signal.r;
if (references !== null) {
let i;
for (i = 0; i < references.length; i++) {
const reference = references[i];
if ((reference.f & IS_EFFECT) !== 0) {
mark_subtree_inert(reference, inert, visited_blocks);
}
}
}
}
/**
* @param {import('./types.js').ComputationSignal} signal
* @param {boolean} inert
* @param {Set<import('./types.js').Block>} [visited_blocks]
* @returns {void}
*/
export function mark_subtree_inert(signal, inert, visited_blocks = new Set()) {
const flags = signal.f;
const is_already_inert = (flags & INERT) !== 0;
if (is_already_inert !== inert) {
signal.f ^= INERT;
if (!inert && (flags & IS_EFFECT) !== 0 && (flags & CLEAN) === 0) {
schedule_effect(/** @type {import('./types.js').EffectSignal} */ (signal), false);
}
// Nested if block effects
const block = signal.b;
if (block !== null && !visited_blocks.has(block)) {
visited_blocks.add(block);
const type = block.t;
if (type === IF_BLOCK) {
const condition_effect = block.e;
if (condition_effect !== null && block !== current_block) {
mark_subtree_inert(condition_effect, inert);
}
const consequent_effect = block.ce;
if (consequent_effect !== null && block.v) {
mark_subtree_inert(consequent_effect, inert, visited_blocks);
}
const alternate_effect = block.ae;
if (alternate_effect !== null && !block.v) {
mark_subtree_inert(alternate_effect, inert, visited_blocks);
}
} else if (type === EACH_BLOCK) {
const items = block.v;
for (let { e: each_item_effect } of items) {
if (each_item_effect !== null) {
mark_subtree_inert(each_item_effect, inert, visited_blocks);
}
}
}
}
}
mark_subtree_children_inert(signal, inert, visited_blocks);
}
/**
* @template V
* @param {import('./types.js').Signal<V>} signal
* @param {number} to_status
* @param {boolean} force_schedule
* @returns {void}
*/
function mark_signal_consumers(signal, to_status, force_schedule) {
const runes = is_runes(signal.x);
const consumers = signal.c;
if (consumers !== null) {
const length = consumers.length;
let i;
for (i = 0; i < length; i++) {
const consumer = consumers[i];
const flags = consumer.f;
const unowned = (flags & UNOWNED) !== 0;
const dirty = (flags & DIRTY) !== 0;
// We skip any effects that are already dirty (but not unowned). Additionally, we also
// skip if the consumer is the same as the current effect (except if we're not in runes or we
// are in force schedule mode).
if ((dirty && !unowned) || ((!force_schedule || !runes) && consumer === current_effect)) {
continue;
}
set_signal_status(consumer, to_status);
// If the signal is not clean, then skip over it with the exception of unowned signals that
// are already dirty. Unowned signals might be dirty because they are not captured as part of an
// effect.
if ((flags & CLEAN) !== 0 || (dirty && unowned)) {
if ((consumer.f & IS_EFFECT) !== 0) {
schedule_effect(/** @type {import('./types.js').EffectSignal} */ (consumer), false);
} else {
mark_signal_consumers(consumer, MAYBE_DIRTY, force_schedule);
}
}
}
}
}
/**
* @template V
* @param {import('./types.js').Signal<V>} signal
* @param {V} value
* @returns {void}
*/
export function set_signal_value(signal, value) {
if (
!current_untracking &&
!ignore_mutation_validation &&
current_consumer !== null &&
is_runes(signal.x) &&
(current_consumer.f & DERIVED) !== 0
) {
throw new Error(
'ERR_SVELTE_UNSAFE_MUTATION' +
(DEV
? ": Unsafe mutations during Svelte's render or derived phase are not permitted in runes mode. " +
'This can lead to unexpected errors and possibly cause infinite loops.\n\nIf this mutation is not meant ' +
'to be reactive do not use the "$state" rune for that declaration.'
: '')
);
}
if (
(signal.f & SOURCE) !== 0 &&
!(/** @type {import('./types.js').EqualsFunctions} */ (signal.e)(value, signal.v))
) {
const component_context = signal.x;
signal.v = value;
// If the current signal is running for the first time, it won't have any
// consumers as we only allocate and assign the consumers after the signal
// has fully executed. So in the case of ensuring it registers the consumer
// properly for itself, we need to ensure the current effect actually gets
// scheduled. i.e:
//
// $effect(() => x++)
//
if (
is_runes(component_context) &&
current_effect !== null &&
current_effect.c === null &&
(current_effect.f & CLEAN) !== 0
) {
if (current_dependencies !== null && current_dependencies.includes(signal)) {
set_signal_status(current_effect, DIRTY);
schedule_effect(current_effect, false);
} else {
if (current_untracked_writes === null) {
current_untracked_writes = [signal];
} else {
current_untracked_writes.push(signal);
}
}
}
mark_signal_consumers(signal, DIRTY, true);
// If we have afterUpdates locally on the component, but we're within a render effect
// then we will need to manually invoke the beforeUpdate/afterUpdate logic.
// TODO: should we put this being a is_runes check and only run it in non-runes mode?
if (current_effect === null && current_queued_pre_and_render_effects.length === 0) {
const update_callbacks = component_context?.u;
if (update_callbacks != null) {
run_all(update_callbacks.b);
const managed = managed_effect(() => {
destroy_signal(managed);
run_all(update_callbacks.a);
});
}
}
// @ts-expect-error
if (DEV && signal.inspect) {
if (is_batching_effect) {
last_inspected_signal = /** @type {import('./types.js').SignalDebug} */ (signal);
} else {
for (const fn of /** @type {import('./types.js').SignalDebug} */ (signal).inspect) fn();
}
}
}
}
/**
* @template V
* @param {import('./types.js').ComputationSignal<V>} signal
* @returns {void}
*/
export function destroy_signal(signal) {
const teardown = /** @type {null | (() => void)} */ (signal.v);
const destroy = signal.y;
const flags = signal.f;
destroy_references(signal);
remove_consumers(signal, 0);
signal.i = signal.r = signal.y = signal.x = signal.b = signal.d = signal.c = null;
set_signal_status(signal, DESTROYED);
if (destroy !== null) {
if (is_array(destroy)) {
run_all(destroy);
} else {
destroy();
}
}
if (teardown !== null && (flags & IS_EFFECT) !== 0) {
teardown();
}
}
/**
* @template V
* @param {() => V} init
* @returns {import('./types.js').ComputationSignal<V>}
*/
/*#__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<V>} */ (
create_computation_signal(flags | CLEAN, UNINITIALIZED, current_block)
);
signal.i = init;
signal.x = current_component_context;
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<V>}
*/
/*#__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<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function source(initial_value) {
const source = create_source_signal(SOURCE | CLEAN, initial_value);
source.x = current_component_context;
return source;
}
/**
* @template V
* @param {V} initial_value
* @returns {import('./types.js').SourceSignal<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function mutable_source(initial_value) {
const s = source(initial_value);
s.e = safe_equal;
return s;
}
/**
* Use `untrack` to prevent something from being treated as an `$effect`/`$derived` dependency.
*
* https://svelte-5-preview.vercel.app/docs/functions#untrack
* @template T
* @param {() => T} fn
* @returns {T}
*/
export function untrack(fn) {
const previous_untracking = current_untracking;
try {
current_untracking = true;
return fn();
} finally {
current_untracking = previous_untracking;
}
}
/**
* @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<V>} signal
* @param {() => void} destroy_fn
* @returns {void}
*/
export function push_destroy_fn(signal, destroy_fn) {
let destroy = signal.y;
if (destroy === null) {
signal.y = destroy_fn;
} else if (is_array(destroy)) {
destroy.push(destroy_fn);
} else {
signal.y = [destroy, destroy_fn];
}
}
const STATUS_MASK = ~(DIRTY | MAYBE_DIRTY | CLEAN);
/**
* @template V
* @param {import('./types.js').Signal<V>} signal
* @param {number} status
* @returns {void}
*/
export function set_signal_status(signal, status) {
signal.f = (signal.f & STATUS_MASK) | status;
}
/**
* @template V
* @param {V | import('./types.js').Signal<V>} val
* @returns {val is import('./types.js').Signal<V>}
*/
export function is_signal(val) {
return (
typeof val === 'object' &&
val !== null &&
typeof (/** @type {import('./types.js').Signal<V>} */ (val).f) === 'number'
);
}
/**
* @template O
* @template P
* @param {any} val
* @returns {val is import('./types.js').LazyProperty<O, P>}
*/
export function is_lazy_property(val) {
return (
typeof val === 'object' &&
val !== null &&
/** @type {import('./types.js').LazyProperty<O, P>} */ (val).t === LAZY_PROPERTY
);
}
/**
* @template V
* @param {unknown} val
* @returns {val is import('./types.js').Store<V>}
*/
export function is_store(val) {
return (
typeof val === 'object' &&
val !== null &&
typeof (/** @type {import('./types.js').Store<V>} */ (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<string, unknown>} 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 setter = get_descriptor(props, key)?.set;
if (DEV && setter && runes && initial !== undefined) {
// TODO consolidate all these random runtime errors
throw new Error('Cannot use fallback values with bind:');
}
var prop_value = /** @type {V} */ (props[key]);
if (prop_value === undefined && initial !== undefined) {
// @ts-expect-error would need a cumbersome method overload to type this
if ((flags & PROPS_IS_LAZY_INITIAL) !== 0) initial = initial();
if (DEV && runes) {
initial = readonly(proxy(/** @type {any} */ (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
if (is_signals_recorded) {
// 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<unknown, unknown>} */
export function get_or_init_context_map() {
const component_context = current_component_context;
if (component_context === null) {
throw new Error(
'ERR_SVELTE_ORPHAN_CONTEXT' +
(DEV ? 'Context can only be used during component initialisation.' : '')
);
}
return (component_context.c ??= new Map(get_parent_context(component_context) || undefined));
}
/**
* @param {import('./types.js').ComponentContext} component_context
* @returns {Map<unknown, unknown> | null}
*/
function get_parent_context(component_context) {
let parent = component_context.p;
while (parent !== null) {
const context_map = parent.c;
if (context_map !== null) {
return context_map;
}
parent = parent.p;
}
return null;
}
/**
* @this {any}
* @param {Record<string, unknown>} $$props
* @param {Event} event
* @returns {void}
*/
export function bubble_event($$props, event) {
var events = /** @type {Record<string, Function[] | Function>} */ ($$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<number>} signal
* @param {1 | -1} [d]
* @returns {number}
*/
export function update(signal, d = 1) {
const value = get(signal);
set_signal_value(signal, value + d);
return value;
}
/**
* @param {((value?: number) => number)} fn
* @param {1 | -1} [d]
* @returns {number}
*/
export function update_prop(fn, d = 1) {
const value = fn();
fn(value + d);
return value;
}
/**
* @param {import('./types.js').Store<number>} 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<number>} signal
* @param {1 | -1} [d]
* @returns {number}
*/
export function update_pre(signal, d = 1) {
const value = get(signal) + d;
set_signal_value(signal, value);
return value;
}
/**
* @param {((value?: number) => number)} fn
* @param {1 | -1} [d]
* @returns {number}
*/
export function update_pre_prop(fn, d = 1) {
const value = fn() + d;
fn(value);
return value;
}
/**
* @param {import('./types.js').Store<number>} 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<string, unknown>} obj
* @param {string[]} keys
* @returns {Record<string, unknown>}
*/
export function exclude_from_object(obj, keys) {
obj = { ...obj };
let key;
for (key of keys) {
delete obj[key];
}
return obj;
}
/**
* @template V
* @param {V} value
* @param {V} fallback
* @returns {V}
*/
export function value_or_fallback(value, fallback) {
return value === undefined ? fallback : value;
}
/**
* Schedules a callback to run immediately before the component is unmounted.
*
* Out of `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`, this is the
* only one that runs inside a server-side component.
*
* https://svelte.dev/docs/svelte#ondestroy
* @param {() => any} fn
* @returns {void}
*/
export function onDestroy(fn) {
if (!is_ssr) {
user_effect(() => () => untrack(fn));
}
}
/**
* @param {Record<string, unknown>} props
* @param {any} runes
* @returns {void}
*/
export function push(props, runes = false) {
current_component_context = {
// accessors
a: null,
// context
c: null,
// effects
e: null,
// mounted
m: false,
// parent
p: current_component_context,
// props
s: props,
// runes
r: runes,
// update_callbacks
u: null
};
}
/**
* @param {Record<string, any>} [accessors]
* @returns {void}
*/
export function pop(accessors) {
const context_stack_item = current_component_context;
if (context_stack_item !== null) {
if (accessors !== undefined) {
context_stack_item.a = accessors;
}
const effects = context_stack_item.e;
if (effects !== null) {
context_stack_item.e = null;
for (let i = 0; i < effects.length; i++) {
schedule_effect(effects[i], false);
}
}
current_component_context = context_stack_item.p;
context_stack_item.m = true;
}
}
/**
* @param {any} value
* @param {Set<any>} visited
* @returns {void}
*/
function deep_read(value, visited = new Set()) {
if (typeof value === 'object' && value !== null && !visited.has(value)) {
visited.add(value);
for (let key in value) {
try {
deep_read(value[key], visited);
} catch (e) {
// continue
}
}
const proto = Object.getPrototypeOf(value);
if (
proto !== Object.prototype &&
proto !== Array.prototype &&
proto !== Map.prototype &&
proto !== Set.prototype &&
proto !== Date.prototype
) {
const descriptors = get_descriptors(proto);
for (let key in descriptors) {
const get = descriptors[key].get;
if (get) {
try {
get.call(value);
} catch (e) {
// continue
}
}
}
}
}
}
/**
* Like `unstate`, but recursively traverses into normal arrays/objects to find potential states in them.
* @param {any} value
* @param {Map<any, any>} visited
* @returns {any}
*/
function deep_unstate(value, visited = new Map()) {
if (typeof value === 'object' && value !== null && !visited.has(value)) {
const unstated = unstate(value);
if (unstated !== value) {
visited.set(value, unstated);
return unstated;
}
const prototype = get_prototype_of(value);
// Only deeply unstate plain objects and arrays
if (prototype === object_prototype || prototype === array_prototype) {
let contains_unstated = false;
/** @type {any} */
const nested_unstated = Array.isArray(value) ? [] : {};
for (let key in value) {
const result = deep_unstate(value[key], visited);
nested_unstated[key] = result;
if (result !== value[key]) {
contains_unstated = true;
}
}
visited.set(value, contains_unstated ? nested_unstated : value);
} else {
visited.set(value, value);
}
}
return visited.get(value) ?? value;
}
// TODO remove in a few versions, before 5.0 at the latest
let warned_inspect_changed = false;
/**
* @param {() => any[]} get_value
* @param {Function} [inspect]
*/
// eslint-disable-next-line no-console
export function inspect(get_value, inspect = console.log) {
let initial = true;
pre_effect(() => {
const fn = () => {
const value = get_value().map((v) => deep_unstate(v));
if (value.length === 2 && typeof value[1] === 'function' && !warned_inspect_changed) {
// eslint-disable-next-line no-console
console.warn(
'$inspect() API has changed. See https://svelte-5-preview.vercel.app/docs/runes#$inspect for more information.'
);
warned_inspect_changed = true;
}
inspect(initial ? 'init' : 'update', ...value);
};
inspect_fn = fn;
const value = get_value();
deep_read(value);
inspect_fn = null;
const signals = inspect_captured_signals.slice();
inspect_captured_signals = [];
if (initial) {
fn();
initial = false;
}
return () => {
for (const s of signals) {
s.inspect.delete(fn);
}
};
});
}
/**
* @template O
* @template P
* @param {O} o
* @param {P} p
* @returns {import('./types.js').LazyProperty<O, P>}
*/
export function lazy_property(o, p) {
return {
o,
p,
t: LAZY_PROPERTY
};
}
/**
* @template V
* @param {V} value
* @returns {import('./types.js').UnwrappedSignal<V>}
*/
export function unwrap(value) {
if (is_signal(value)) {
// @ts-ignore
return get(value);
}
if (is_lazy_property(value)) {
return value.o[value.p];
}
// @ts-ignore
return value;
}
if (DEV) {
/** @param {string} rune */
function throw_rune_error(rune) {
if (!(rune in globalThis)) {
// @ts-ignore
globalThis[rune] = () => {
// TODO if people start adjusting the "this can contain runes" config through v-p-s more, adjust this message
throw new Error(`${rune} is only available inside .svelte and .svelte.js/ts files`);
};
}
}
throw_rune_error('$state');
throw_rune_error('$effect');
throw_rune_error('$derived');
throw_rune_error('$inspect');
throw_rune_error('$props');
}
/**
* Expects a value that was wrapped with `freeze` and makes it frozen.
* @template T
* @param {T} value
* @returns {Readonly<T>}
*/
export function freeze(value) {
if (typeof value === 'object' && value != null && !is_frozen(value)) {
// If the object is already proxified, then unstate the value
if (STATE_SYMBOL in value) {
return object_freeze(unstate(value));
}
// If the value is already read-only then just use that
if (DEV && READONLY_SYMBOL in value) {
return value;
}
// Otherwise freeze the object
object_freeze(value);
}
return value;
}