svelte/packages/svelte/src/internal/client/runtime.js

1362 lines
36 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 {
define_property,
get_descriptors,
get_prototype_of,
is_frozen,
object_freeze
} from './utils.js';
import { snapshot } from './proxy.js';
import { destroy_effect, effect, execute_effect_teardown } from './reactivity/effects.js';
import {
EFFECT,
RENDER_EFFECT,
DIRTY,
MAYBE_DIRTY,
CLEAN,
DERIVED,
UNOWNED,
DESTROYED,
INERT,
BRANCH_EFFECT,
STATE_SYMBOL,
BLOCK_EFFECT,
ROOT_EFFECT,
LEGACY_DERIVED_PROP,
DISCONNECTED,
STATE_FROZEN_SYMBOL
} from './constants.js';
import { flush_tasks } from './dom/task.js';
import { add_owner } from './dev/ownership.js';
import { mutate, set, source } from './reactivity/sources.js';
import { update_derived } from './reactivity/deriveds.js';
import { inspect_captured_signals, inspect_fn, set_inspect_fn } from './dev/inspect.js';
import * as e from './errors.js';
import { lifecycle_outside_component } from '../shared/errors.js';
const FLUSH_MICROTASK = 0;
const FLUSH_SYNC = 1;
// Used for DEV time error handling
/** @param {WeakSet<Error>} value */
const handled_errors = new WeakSet();
// Used for controlling the flush of effects.
let current_scheduler_mode = FLUSH_MICROTASK;
// Used for handling scheduling
let is_micro_task_queued = false;
export let is_flushing_effect = false;
export let is_destroying_effect = false;
/** @param {boolean} value */
export function set_is_flushing_effect(value) {
is_flushing_effect = value;
}
/** @param {boolean} value */
export function set_is_destroying_effect(value) {
is_destroying_effect = value;
}
/** @param {boolean} value */
export function set_untracking(value) {
current_untracking = value;
}
// Used for $inspect
export let is_batching_effect = false;
let is_inspecting_signal = false;
// Handle effect queues
/** @type {import('#client').Effect[]} */
let current_queued_root_effects = [];
let flush_count = 0;
// Handle signal reactivity tree dependencies and reactions
/** @type {null | import('#client').Reaction} */
export let current_reaction = null;
/** @param {null | import('#client').Reaction} reaction */
export function set_current_reaction(reaction) {
current_reaction = reaction;
}
/** @type {null | import('#client').Effect} */
export let current_effect = null;
/** @param {null | import('#client').Effect} effect */
export function set_current_effect(effect) {
current_effect = effect;
}
/** @type {null | import('#client').Value[]} */
export let current_dependencies = null;
let current_dependencies_index = 0;
/**
* Tracks writes that the effect it's executed in doesn't listen to yet,
* so that the dependency can be added to the effect later on if it then reads it
* @type {null | import('#client').Source[]}
*/
export let current_untracked_writes = null;
/** @param {null | import('#client').Source[]} value */
export function set_current_untracked_writes(value) {
current_untracked_writes = value;
}
/** @type {null | import('#client').ValueDebug} */
export let last_inspected_signal = null;
/** @param {null | import('#client').ValueDebug} signal */
export function set_last_inspected_signal(signal) {
last_inspected_signal = signal;
}
/** If `true`, `get`ting the signal should not register it as a dependency */
export let current_untracking = false;
// If we are working with a get() chain that has no active container,
// to prevent memory leaks, we skip adding the reaction.
export let current_skip_reaction = false;
// Handle collecting all signals which are read during a specific time frame
export let is_signals_recorded = false;
let captured_signals = new Set();
// Handling runtime component context
/** @type {import('#client').ComponentContext | null} */
export let current_component_context = null;
/** @param {import('#client').ComponentContext | null} context */
export function set_current_component_context(context) {
current_component_context = context;
}
/**
* The current component function. Different from current component context:
* ```html
* <!-- App.svelte -->
* <Foo>
* <Bar /> <!-- context == Foo.svelte, function == App.svelte -->
* </Foo>
* ```
* @type {import('#client').ComponentContext['function']}
*/
export let dev_current_component_function = null;
/** @param {import('#client').ComponentContext['function']} fn */
export function set_dev_current_component_function(fn) {
dev_current_component_function = fn;
}
/** @returns {boolean} */
export function is_runes() {
return current_component_context !== null && current_component_context.l === null;
}
/**
* @param {import('#client').ProxyStateObject} target
* @param {string | symbol} prop
* @param {any} receiver
*/
export function batch_inspect(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
/**
* @this {any}
*/
return function () {
const previously_batching_effect = is_batching_effect;
is_batching_effect = true;
try {
return Reflect.apply(value, this, arguments);
} finally {
is_batching_effect = previously_batching_effect;
if (last_inspected_signal !== null && !is_inspecting_signal) {
is_inspecting_signal = true;
try {
for (const fn of last_inspected_signal.inspect) {
fn();
}
} finally {
is_inspecting_signal = false;
}
last_inspected_signal = null;
}
}
};
}
/**
* Determines whether a derived or effect is dirty.
* If it is MAYBE_DIRTY, will set the status to CLEAN
* @param {import('#client').Reaction} reaction
* @returns {boolean}
*/
export function check_dirtiness(reaction) {
var flags = reaction.f;
var is_dirty = (flags & DIRTY) !== 0;
var is_unowned = (flags & UNOWNED) !== 0;
// If we are unowned, we still need to ensure that we update our version to that
// of our dependencies.
if (is_dirty && !is_unowned) {
return true;
}
var is_disconnected = (flags & DISCONNECTED) !== 0;
if ((flags & MAYBE_DIRTY) !== 0 || (is_dirty && is_unowned)) {
var dependencies = reaction.deps;
if (dependencies !== null) {
var length = dependencies.length;
var is_equal;
var reactions;
for (var i = 0; i < length; i++) {
var dependency = dependencies[i];
if (!is_dirty && check_dirtiness(/** @type {import('#client').Derived} */ (dependency))) {
is_equal = update_derived(/** @type {import('#client').Derived} **/ (dependency), true);
}
var version = dependency.version;
if (is_unowned) {
// If we're working with an unowned derived signal, then we need to check
// if our dependency write version is higher. If it is then we can assume
// that state has changed to a newer version and thus this unowned signal
// is also dirty.
if (version > /** @type {import('#client').Derived} */ (reaction).version) {
/** @type {import('#client').Derived} */ (reaction).version = version;
return !is_equal;
}
if (!current_skip_reaction && !dependency?.reactions?.includes(reaction)) {
// If we are working with an unowned signal as part of an effect (due to !current_skip_reaction)
// and the version hasn't changed, we still need to check that this reaction
// if linked to the dependency source otherwise future updates will not be caught.
reactions = dependency.reactions;
if (reactions === null) {
dependency.reactions = [reaction];
} else {
reactions.push(reaction);
}
}
} else if ((reaction.f & DIRTY) !== 0) {
// `signal` might now be dirty, as a result of calling `check_dirtiness` and/or `update_derived`
return true;
} else if (is_disconnected) {
// It might be that the derived was was dereferenced from its dependencies but has now come alive again.
// In thise case, we need to re-attach it to the graph and mark it dirty if any of its dependencies have
// changed since.
if (version > /** @type {import('#client').Derived} */ (reaction).version) {
/** @type {import('#client').Derived} */ (reaction).version = version;
is_dirty = true;
}
reactions = dependency.reactions;
if (reactions === null) {
dependency.reactions = [reaction];
} else if (!reactions.includes(reaction)) {
reactions.push(reaction);
}
}
}
}
// Unowned signals are always maybe dirty, as we instead check their dependency versions.
if (!is_unowned) {
set_signal_status(reaction, CLEAN);
}
if (is_disconnected) {
reaction.f ^= DISCONNECTED;
}
}
return is_dirty;
}
/**
* @param {Error} error
* @param {import("#client").Effect} effect
* @param {import("#client").ComponentContext | null} component_context
*/
function handle_error(error, effect, component_context) {
// Given we don't yet have error boundaries, we will just always throw.
if (!DEV || handled_errors.has(error) || component_context === null) {
throw error;
}
const component_stack = [];
const effect_name = effect.fn?.name;
if (effect_name) {
component_stack.push(effect_name);
}
/** @type {import("#client").ComponentContext | null} */
let current_context = component_context;
while (current_context !== null) {
var filename = current_context.function?.filename;
if (filename) {
const file = filename.split('/').at(-1);
component_stack.push(file);
}
current_context = current_context.p;
}
const indent = /Firefox/.test(navigator.userAgent) ? ' ' : '\t';
define_property(error, 'message', {
value: error.message + `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n`
});
const stack = error.stack;
// Filter out internal files from callstack
if (stack) {
const lines = stack.split('\n');
const new_lines = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.includes('svelte/src/internal')) {
continue;
}
new_lines.push(line);
}
define_property(error, 'stack', {
value: error.stack + new_lines.join('\n')
});
}
handled_errors.add(error);
throw error;
}
/**
* @template V
* @param {import('#client').Reaction} signal
* @returns {V}
*/
export function execute_reaction_fn(signal) {
const previous_dependencies = current_dependencies;
const previous_dependencies_index = current_dependencies_index;
const previous_untracked_writes = current_untracked_writes;
const previous_reaction = current_reaction;
const previous_skip_reaction = current_skip_reaction;
const previous_untracking = current_untracking;
current_dependencies = /** @type {null | import('#client').Value[]} */ (null);
current_dependencies_index = 0;
current_untracked_writes = null;
current_reaction = signal;
current_skip_reaction = !is_flushing_effect && (signal.f & UNOWNED) !== 0;
current_untracking = false;
try {
let res = /** @type {Function} */ (0, signal.fn)();
let dependencies = /** @type {import('#client').Value<unknown>[]} **/ (signal.deps);
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_reaction(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.deps = /** @type {import('#client').Value<V>[]} **/ (
dependencies = current_dependencies
);
}
if (!current_skip_reaction) {
for (i = current_dependencies_index; i < dependencies.length; i++) {
const dependency = dependencies[i];
const reactions = dependency.reactions;
if (reactions === null) {
dependency.reactions = [signal];
} else if (reactions[reactions.length - 1] !== signal) {
// TODO: should this be:
//
// } else if (!reactions.includes(signal)) {
//
reactions.push(signal);
}
}
}
} else if (dependencies !== null && current_dependencies_index < dependencies.length) {
remove_reactions(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_reaction = previous_reaction;
current_skip_reaction = previous_skip_reaction;
current_untracking = previous_untracking;
}
}
/**
* @template V
* @param {import('#client').Reaction} signal
* @param {import('#client').Value<V>} dependency
* @returns {void}
*/
function remove_reaction(signal, dependency) {
const reactions = dependency.reactions;
let reactions_length = 0;
if (reactions !== null) {
reactions_length = reactions.length - 1;
const index = reactions.indexOf(signal);
if (index !== -1) {
if (reactions_length === 0) {
dependency.reactions = null;
} else {
// Swap with last element and then remove.
reactions[index] = reactions[reactions_length];
reactions.pop();
}
}
}
// If the derived has no reactions, then we can disconnect it from the graph,
// allowing it to either reconnect in the future, or be GC'd by the VM.
if (reactions_length === 0 && (dependency.f & DERIVED) !== 0) {
set_signal_status(dependency, MAYBE_DIRTY);
// If we are working with a derived that is owned by an effect, then mark it as being
// disconnected.
if ((dependency.f & (UNOWNED | DISCONNECTED)) === 0) {
dependency.f ^= DISCONNECTED;
}
remove_reactions(/** @type {import('#client').Derived} **/ (dependency), 0);
}
}
/**
* @param {import('#client').Reaction} signal
* @param {number} start_index
* @returns {void}
*/
export function remove_reactions(signal, start_index) {
const dependencies = signal.deps;
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 reaction if we know that it is active (start_index will not be 0)
if (active_dependencies === null || !active_dependencies.includes(dependency)) {
remove_reaction(signal, dependency);
}
}
}
}
/**
* @param {import('#client').Reaction} signal
* @param {boolean} [remove_dom]
* @returns {void}
*/
export function destroy_effect_children(signal, remove_dom = true) {
let effect = signal.first;
signal.first = null;
signal.last = null;
var sibling;
while (effect !== null) {
sibling = effect.next;
destroy_effect(effect, remove_dom);
effect = sibling;
}
}
/**
* @param {import('#client').Effect} effect
* @returns {void}
*/
export function execute_effect(effect) {
var flags = effect.f;
if ((flags & DESTROYED) !== 0) {
return;
}
set_signal_status(effect, CLEAN);
var component_context = effect.ctx;
var previous_effect = current_effect;
var previous_component_context = current_component_context;
current_effect = effect;
current_component_context = component_context;
if (DEV) {
var previous_component_fn = dev_current_component_function;
dev_current_component_function = effect.component_function;
}
try {
if ((flags & BLOCK_EFFECT) === 0) {
destroy_effect_children(effect);
}
execute_effect_teardown(effect);
var teardown = execute_reaction_fn(effect);
effect.teardown = typeof teardown === 'function' ? teardown : null;
} catch (error) {
handle_error(/** @type {Error} */ (error), effect, current_component_context);
} finally {
current_effect = previous_effect;
current_component_context = previous_component_context;
if (DEV) {
dev_current_component_function = previous_component_fn;
}
}
}
function infinite_loop_guard() {
if (flush_count > 1000) {
flush_count = 0;
e.effect_update_depth_exceeded();
}
flush_count++;
}
/**
* @param {Array<import('#client').Effect>} root_effects
* @returns {void}
*/
function flush_queued_root_effects(root_effects) {
const length = root_effects.length;
if (length === 0) {
return;
}
infinite_loop_guard();
var previously_flushing_effect = is_flushing_effect;
is_flushing_effect = true;
try {
for (var i = 0; i < length; i++) {
var effect = root_effects[i];
// When working with custom elements, the root effects might not have a root
if (effect.first === null && (effect.f & BRANCH_EFFECT) === 0) {
flush_queued_effects([effect]);
} else {
/** @type {import('#client').Effect[]} */
var collected_effects = [];
process_effects(effect, collected_effects);
flush_queued_effects(collected_effects);
}
}
} finally {
is_flushing_effect = previously_flushing_effect;
}
}
/**
* @param {Array<import('#client').Effect>} effects
* @returns {void}
*/
function flush_queued_effects(effects) {
var length = effects.length;
if (length === 0) return;
for (var i = 0; i < length; i++) {
var effect = effects[i];
if ((effect.f & (DESTROYED | INERT)) === 0 && check_dirtiness(effect)) {
execute_effect(effect);
}
}
}
function process_deferred() {
is_micro_task_queued = false;
if (flush_count > 1001) {
return;
}
const previous_queued_root_effects = current_queued_root_effects;
current_queued_root_effects = [];
flush_queued_root_effects(previous_queued_root_effects);
if (!is_micro_task_queued) {
flush_count = 0;
}
}
/**
* @param {import('#client').Effect} signal
* @returns {void}
*/
export function schedule_effect(signal) {
if (current_scheduler_mode === FLUSH_MICROTASK) {
if (!is_micro_task_queued) {
is_micro_task_queued = true;
queueMicrotask(process_deferred);
}
}
var effect = signal;
while (effect.parent !== null) {
effect = effect.parent;
var flags = effect.f;
if ((flags & BRANCH_EFFECT) !== 0) {
if ((flags & CLEAN) === 0) return;
set_signal_status(effect, MAYBE_DIRTY);
}
}
current_queued_root_effects.push(effect);
}
/**
*
* This function both runs render effects and collects user effects in topological order
* from the starting effect passed in. Effects will be collected when they match the filtered
* bitwise flag passed in only. The collected effects array will be populated with all the user
* effects to be flushed.
*
* @param {import('#client').Effect} effect
* @param {import('#client').Effect[]} collected_effects
* @returns {void}
*/
function process_effects(effect, collected_effects) {
var current_effect = effect.first;
var effects = [];
main_loop: while (current_effect !== null) {
var flags = current_effect.f;
// TODO: we probably don't need to check for destroyed as it shouldn't be encountered?
var is_active = (flags & (DESTROYED | INERT)) === 0;
var is_branch = flags & BRANCH_EFFECT;
var is_clean = (flags & CLEAN) !== 0;
var child = current_effect.first;
// Skip this branch if it's clean
if (is_active && (!is_branch || !is_clean)) {
if (is_branch) {
set_signal_status(current_effect, CLEAN);
}
if ((flags & RENDER_EFFECT) !== 0) {
if (!is_branch && check_dirtiness(current_effect)) {
execute_effect(current_effect);
// Child might have been mutated since running the effect
child = current_effect.first;
}
if (child !== null) {
current_effect = child;
continue;
}
} else if ((flags & EFFECT) !== 0) {
if (is_branch || is_clean) {
if (child !== null) {
current_effect = child;
continue;
}
} else {
effects.push(current_effect);
}
}
}
var sibling = current_effect.next;
if (sibling === null) {
let parent = current_effect.parent;
while (parent !== null) {
if (effect === parent) {
break main_loop;
}
var parent_sibling = parent.next;
if (parent_sibling !== null) {
current_effect = parent_sibling;
continue main_loop;
}
parent = parent.parent;
}
}
current_effect = sibling;
}
// We might be dealing with many effects here, far more than can be spread into
// an array push call (callstack overflow). So let's deal with each effect in a loop.
for (var i = 0; i < effects.length; i++) {
child = effects[i];
collected_effects.push(child);
process_effects(child, collected_effects);
}
}
/**
* Internal version of `flushSync` with the option to not flush previous effects.
* Returns the result of the passed function, if given.
* @param {() => any} [fn]
* @param {boolean} [flush_previous]
* @returns {any}
*/
export function flush_sync(fn, flush_previous = true) {
var previous_scheduler_mode = current_scheduler_mode;
var previous_queued_root_effects = current_queued_root_effects;
try {
infinite_loop_guard();
/** @type {import('#client').Effect[]} */
const root_effects = [];
current_scheduler_mode = FLUSH_SYNC;
current_queued_root_effects = root_effects;
is_micro_task_queued = false;
if (flush_previous) {
flush_queued_root_effects(previous_queued_root_effects);
}
var result = fn?.();
flush_tasks();
if (current_queued_root_effects.length > 0 || root_effects.length > 0) {
flush_sync();
}
flush_count = 0;
return result;
} finally {
current_scheduler_mode = previous_scheduler_mode;
current_queued_root_effects = previous_queued_root_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 flush_sync 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.
flush_sync();
}
/**
* @template V
* @param {import('#client').Value<V>} signal
* @returns {V}
*/
export function get(signal) {
if (DEV && inspect_fn) {
var s = /** @type {import('#client').ValueDebug} */ (signal);
s.inspect.add(inspect_fn);
inspect_captured_signals.push(s);
}
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 reaction signal.
if (
current_reaction !== null &&
(current_reaction.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 &&
!current_untracking
) {
const unowned = (current_reaction.f & UNOWNED) !== 0;
const dependencies = current_reaction.deps;
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 (current_dependencies[current_dependencies.length - 1] !== signal) {
current_dependencies.push(signal);
}
}
if (
current_untracked_writes !== null &&
current_effect !== null &&
(current_effect.f & CLEAN) !== 0 &&
(current_effect.f & BRANCH_EFFECT) === 0 &&
current_untracked_writes.includes(signal)
) {
set_signal_status(current_effect, DIRTY);
schedule_effect(current_effect);
}
}
if (
(flags & DERIVED) !== 0 &&
check_dirtiness(/** @type {import('#client').Derived} */ (signal))
) {
if (DEV) {
// we want to avoid tracking indirect dependencies
const previous_inspect_fn = inspect_fn;
set_inspect_fn(null);
update_derived(/** @type {import('#client').Derived} **/ (signal), false);
set_inspect_fn(previous_inspect_fn);
} else {
update_derived(/** @type {import('#client').Derived} **/ (signal), false);
}
}
return signal.v;
}
/**
* 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) {
// Go one level up because derived signals created as part of props in legacy mode
if ((signal.f & LEGACY_DERIVED_PROP) !== 0) {
for (const dep of /** @type {import('#client').Derived} */ (signal).deps || []) {
if ((dep.f & DERIVED) === 0) {
mutate(dep, null /* doesnt matter */);
}
}
} else {
mutate(signal, null /* doesnt matter */);
}
}
}
/**
* @param {import('#client').Value} signal
* @param {number} to_status should be DIRTY or MAYBE_DIRTY
* @param {boolean} force_schedule
* @returns {void}
*/
export function mark_reactions(signal, to_status, force_schedule) {
var reactions = signal.reactions;
if (reactions === null) return;
var runes = is_runes();
var length = reactions.length;
for (var i = 0; i < length; i++) {
var reaction = reactions[i];
var flags = reaction.f;
// We skip any effects that are already dirty. Additionally, we also
// skip if the reaction is the same as the current effect (except if we're not in runes or we
// are in force schedule mode).
if ((flags & DIRTY) !== 0 || ((!force_schedule || !runes) && reaction === current_effect)) {
continue;
}
set_signal_status(reaction, to_status);
// If the signal is not clean, then skip over it with the exception of unowned signals that
// are already maybe dirty. Unowned signals might be dirty because they are not captured as part of an
// effect.
var maybe_dirty = (flags & MAYBE_DIRTY) !== 0;
var unowned = (flags & UNOWNED) !== 0;
if ((flags & CLEAN) !== 0 || (maybe_dirty && unowned)) {
if ((reaction.f & DERIVED) !== 0) {
mark_reactions(
/** @type {import('#client').Derived} */ (reaction),
MAYBE_DIRTY,
force_schedule
);
} else {
schedule_effect(/** @type {import('#client').Effect} */ (reaction));
}
}
}
}
/**
* 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;
}
}
const STATUS_MASK = ~(DIRTY | MAYBE_DIRTY | CLEAN);
/**
* @param {import('#client').Signal} 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('#client').Value<V>} val
* @returns {val is import('#client').Value<V>}
*/
export function is_signal(val) {
return (
typeof val === 'object' &&
val !== null &&
typeof (/** @type {import('#client').Value<V>} */ (val).f) === 'number'
);
}
/**
* Retrieves the context that belongs to the closest parent component with the specified `key`.
* Must be called during component initialisation.
*
* https://svelte.dev/docs/svelte#getcontext
* @template T
* @param {any} key
* @returns {T}
*/
export function getContext(key) {
const context_map = get_or_init_context_map('getContext');
const result = /** @type {T} */ (context_map.get(key));
if (DEV) {
const fn = /** @type {import('#client').ComponentContext} */ (current_component_context)
.function;
if (fn) {
add_owner(result, fn, true);
}
}
return result;
}
/**
* Associates an arbitrary `context` object with the current component and the specified `key`
* and returns that object. The context is then available to children of the component
* (including slotted content) with `getContext`.
*
* Like lifecycle functions, this must be called during component initialisation.
*
* https://svelte.dev/docs/svelte#setcontext
* @template T
* @param {any} key
* @param {T} context
* @returns {T}
*/
export function setContext(key, context) {
const context_map = get_or_init_context_map('setContext');
context_map.set(key, context);
return context;
}
/**
* Checks whether a given `key` has been set in the context of a parent component.
* Must be called during component initialisation.
*
* https://svelte.dev/docs/svelte#hascontext
* @param {any} key
* @returns {boolean}
*/
export function hasContext(key) {
const context_map = get_or_init_context_map('hasContext');
return context_map.has(key);
}
/**
* Retrieves the whole context map that belongs to the closest parent component.
* Must be called during component initialisation. Useful, for example, if you
* programmatically create a component and want to pass the existing context to it.
*
* https://svelte.dev/docs/svelte#getallcontexts
* @template {Map<any, any>} [T=Map<any, any>]
* @returns {T}
*/
export function getAllContexts() {
const context_map = get_or_init_context_map('getAllContexts');
if (DEV) {
const fn = current_component_context?.function;
if (fn) {
for (const value of context_map.values()) {
add_owner(value, fn, true);
}
}
}
return /** @type {T} */ (context_map);
}
/**
* @param {string} name
* @returns {Map<unknown, unknown>}
*/
function get_or_init_context_map(name) {
if (current_component_context === null) {
lifecycle_outside_component(name);
}
return (current_component_context.c ??= new Map(
get_parent_context(current_component_context) || undefined
));
}
/**
* @param {import('#client').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;
}
/**
* @param {import('#client').Value<number>} signal
* @param {1 | -1} [d]
* @returns {number}
*/
export function update(signal, d = 1) {
var value = +get(signal);
set(signal, value + d);
return value;
}
/**
* @param {import('#client').Value<number>} signal
* @param {1 | -1} [d]
* @returns {number}
*/
export function update_pre(signal, d = 1) {
return set(signal, +get(signal) + d);
}
/**
* @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 lazy because could contain side effects
* @returns {V}
*/
export function value_or_fallback(value, fallback) {
return value === undefined ? fallback() : value;
}
/**
* @template V
* @param {V} value
* @param {() => Promise<V>} fallback lazy because could contain side effects
* @returns {Promise<V>}
*/
export async function value_or_fallback_async(value, fallback) {
return value === undefined ? fallback() : value;
}
/**
* @param {Record<string, unknown>} props
* @param {any} runes
* @param {Function} [fn]
* @returns {void}
*/
export function push(props, runes = false, fn) {
current_component_context = {
p: current_component_context,
c: null,
e: null,
m: false,
s: props,
x: null,
l: null
};
if (!runes) {
current_component_context.l = {
s: null,
u: null,
r1: [],
r2: source(false)
};
}
if (DEV) {
// component function
current_component_context.function = fn;
dev_current_component_function = fn;
}
}
/**
* @template {Record<string, any>} T
* @param {T} [component]
* @returns {T}
*/
export function pop(component) {
const context_stack_item = current_component_context;
if (context_stack_item !== null) {
if (component !== undefined) {
context_stack_item.x = component;
}
const effects = context_stack_item.e;
if (effects !== null) {
context_stack_item.e = null;
for (var i = 0; i < effects.length; i++) {
effect(effects[i]);
}
}
current_component_context = context_stack_item.p;
if (DEV) {
dev_current_component_function = context_stack_item.p?.function ?? null;
}
context_stack_item.m = true;
}
// Micro-optimization: Don't set .a above to the empty object
// so it can be garbage-collected when the return here is unused
return component || /** @type {T} */ ({});
}
/**
* Possibly traverse an object and read all its properties so that they're all reactive in case this is `$state`.
* Does only check first level of an object for performance reasons (heuristic should be good for 99% of all cases).
* @param {any} value
* @returns {void}
*/
export function deep_read_state(value) {
if (typeof value !== 'object' || !value || value instanceof EventTarget) {
return;
}
if (STATE_SYMBOL in value) {
deep_read(value);
} else if (!Array.isArray(value)) {
for (let key in value) {
const prop = value[key];
if (typeof prop === 'object' && prop && STATE_SYMBOL in prop) {
deep_read(prop);
}
}
}
}
/**
* Deeply traverse an object and read all its properties
* so that they're all reactive in case this is `$state`
* @param {any} value
* @param {Set<any>} visited
* @returns {void}
*/
export function deep_read(value, visited = new Set()) {
if (
typeof value === 'object' &&
value !== null &&
// We don't want to traverse DOM elements
!(value instanceof EventTarget) &&
!visited.has(value)
) {
visited.add(value);
// When working with a possible ReactiveDate, this
// will ensure we capture changes to it.
if (value instanceof Date) {
value.getTime();
}
for (let key in value) {
try {
deep_read(value[key], visited);
} catch (e) {
// continue
}
}
const proto = get_prototype_of(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
}
}
}
}
}
}
/**
* @template V
* @param {V | import('#client').Value<V>} value
* @returns {V}
*/
export function unwrap(value) {
if (is_signal(value)) {
// @ts-ignore
return get(value);
}
// @ts-ignore
return value;
}
if (DEV) {
/**
* @param {string} rune
*/
function throw_rune_error(rune) {
if (!(rune in globalThis)) {
// TODO if people start adjusting the "this can contain runes" config through v-p-s more, adjust this message
/** @type {any} */
let value; // let's hope noone modifies this global, but belts and braces
Object.defineProperty(globalThis, rune, {
configurable: true,
// eslint-disable-next-line getter-return
get: () => {
if (value !== undefined) {
return value;
}
e.rune_outside_svelte(rune);
},
set: (v) => {
value = v;
}
});
}
}
throw_rune_error('$state');
throw_rune_error('$effect');
throw_rune_error('$derived');
throw_rune_error('$inspect');
throw_rune_error('$props');
throw_rune_error('$bindable');
}
/**
* Expects a value that was wrapped with `freeze` and makes it frozen in DEV.
* @template T
* @param {T} value
* @returns {Readonly<T>}
*/
export function freeze(value) {
if (
typeof value === 'object' &&
value != null &&
!is_frozen(value) &&
!(STATE_FROZEN_SYMBOL in value)
) {
// If the object is already proxified, then snapshot the value
if (STATE_SYMBOL in value) {
value = snapshot(value);
}
define_property(value, STATE_FROZEN_SYMBOL, {
value: true,
writable: true,
enumerable: false
});
// Freeze the object in DEV
if (DEV) {
object_freeze(value);
}
}
return value;
}