UNPKG

svelte

Version:

Cybernetically enhanced web apps

341 lines (287 loc) • 7.85 kB
/** @import { Derived, Effect, Source, Value } from '#client' */ import { DEV } from 'esm-env'; import { active_reaction, active_effect, untracked_writes, get, set_untracked_writes, set_signal_status, untrack, increment_write_version, update_effect, current_sources, is_dirty, untracking, is_destroying_effect, push_reaction_value } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import { CLEAN, DERIVED, DIRTY, BRANCH_EFFECT, INSPECT_EFFECT, UNOWNED, MAYBE_DIRTY, BLOCK_EFFECT, ROOT_EFFECT, ASYNC } from '#client/constants'; import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { get_stack, tag_proxy } from '../dev/tracing.js'; import { component_context, is_runes } from '../context.js'; import { Batch, schedule_effect } from './batch.js'; import { proxy } from '../proxy.js'; import { execute_derived } from './deriveds.js'; export let inspect_effects = new Set(); /** @type {Map<Source, any>} */ export const old_values = new Map(); /** * @param {Set<any>} v */ export function set_inspect_effects(v) { inspect_effects = v; } let inspect_effects_deferred = false; export function set_inspect_effects_deferred() { inspect_effects_deferred = true; } /** * @template V * @param {V} v * @param {Error | null} [stack] * @returns {Source<V>} */ // TODO rename this to `state` throughout the codebase export function source(v, stack) { /** @type {Value} */ var signal = { f: 0, // TODO ideally we could skip this altogether, but it causes type errors v, reactions: null, equals, rv: 0, wv: 0 }; if (DEV && tracing_mode_flag) { signal.created = stack ?? get_stack('CreatedAt'); signal.updated = null; signal.set_during_effect = false; signal.trace = null; } return signal; } /** * @template V * @param {V} v * @param {Error | null} [stack] */ /*#__NO_SIDE_EFFECTS__*/ export function state(v, stack) { const s = source(v, stack); push_reaction_value(s); return s; } /** * @template V * @param {V} initial_value * @param {boolean} [immutable] * @returns {Source<V>} */ /*#__NO_SIDE_EFFECTS__*/ export function mutable_source(initial_value, immutable = false, trackable = true) { const s = source(initial_value); if (!immutable) { s.equals = safe_equals; } // bind the signal to the component context, in case we need to // track updates to trigger beforeUpdate/afterUpdate callbacks if (legacy_mode_flag && trackable && component_context !== null && component_context.l !== null) { (component_context.l.s ??= []).push(s); } return s; } /** * @template V * @param {Value<V>} source * @param {V} value */ export function mutate(source, value) { set( source, untrack(() => get(source)) ); return value; } /** * @template V * @param {Source<V>} source * @param {V} value * @param {boolean} [should_proxy] * @returns {V} */ export function set(source, value, should_proxy = false) { if ( active_reaction !== null && // since we are untracking the function inside `$inspect.with` we need to add this check // to ensure we error if state is set inside an inspect effect (!untracking || (active_reaction.f & INSPECT_EFFECT) !== 0) && is_runes() && (active_reaction.f & (DERIVED | BLOCK_EFFECT | ASYNC | INSPECT_EFFECT)) !== 0 && !current_sources?.includes(source) ) { e.state_unsafe_mutation(); } let new_value = should_proxy ? proxy(value) : value; if (DEV) { tag_proxy(new_value, /** @type {string} */ (source.label)); } return internal_set(source, new_value); } /** * @template V * @param {Source<V>} source * @param {V} value * @returns {V} */ export function internal_set(source, value) { if (!source.equals(value)) { var old_value = source.v; if (is_destroying_effect) { old_values.set(source, value); } else { old_values.set(source, old_value); } source.v = value; const batch = Batch.ensure(); batch.capture(source, old_value); if (DEV) { if (tracing_mode_flag || active_effect !== null) { const error = get_stack('UpdatedAt'); if (error !== null) { source.updated ??= new Map(); let entry = source.updated.get(error.stack); if (!entry) { entry = { error, count: 0 }; source.updated.set(error.stack, entry); } entry.count++; } } if (active_effect !== null) { source.set_during_effect = true; } } if ((source.f & DERIVED) !== 0) { // if we are assigning to a dirty derived we set it to clean/maybe dirty but we also eagerly execute it to track the dependencies if ((source.f & DIRTY) !== 0) { execute_derived(/** @type {Derived} */ (source)); } set_signal_status(source, (source.f & UNOWNED) === 0 ? CLEAN : MAYBE_DIRTY); } source.wv = increment_write_version(); mark_reactions(source, DIRTY); // It's possible that the current reaction might not have up-to-date dependencies // whilst it's actively running. So in the case of ensuring it registers the reaction // properly for itself, we need to ensure the current effect actually gets // scheduled. i.e: `$effect(() => x++)` if ( is_runes() && active_effect !== null && (active_effect.f & CLEAN) !== 0 && (active_effect.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ) { if (untracked_writes === null) { set_untracked_writes([source]); } else { untracked_writes.push(source); } } if (DEV && inspect_effects.size > 0 && !inspect_effects_deferred) { flush_inspect_effects(); } } return value; } export function flush_inspect_effects() { inspect_effects_deferred = false; const inspects = Array.from(inspect_effects); for (const effect of inspects) { // Mark clean inspect-effects as maybe dirty and then check their dirtiness // instead of just updating the effects - this way we avoid overfiring. if ((effect.f & CLEAN) !== 0) { set_signal_status(effect, MAYBE_DIRTY); } if (is_dirty(effect)) { update_effect(effect); } } inspect_effects.clear(); } /** * @template {number | bigint} T * @param {Source<T>} source * @param {1 | -1} [d] * @returns {T} */ export function update(source, d = 1) { var value = get(source); var result = d === 1 ? value++ : value--; set(source, value); // @ts-expect-error return result; } /** * @template {number | bigint} T * @param {Source<T>} source * @param {1 | -1} [d] * @returns {T} */ export function update_pre(source, d = 1) { var value = get(source); // @ts-expect-error return set(source, d === 1 ? ++value : --value); } /** * Silently (without using `get`) increment a source * @param {Source<number>} source */ export function increment(source) { set(source, source.v + 1); } /** * @param {Value} signal * @param {number} status should be DIRTY or MAYBE_DIRTY * @returns {void} */ function mark_reactions(signal, status) { 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; // Skip any effects that are already dirty if ((flags & DIRTY) !== 0) continue; // In legacy mode, skip the current effect to prevent infinite loops if (!runes && reaction === active_effect) continue; // Inspect effects need to run immediately, so that the stack trace makes sense if (DEV && (flags & INSPECT_EFFECT) !== 0) { inspect_effects.add(reaction); continue; } set_signal_status(reaction, status); // If the signal a) was previously clean or b) is an unowned derived, then mark it if ((flags & (CLEAN | UNOWNED)) !== 0) { if ((flags & DERIVED) !== 0) { mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); } else { schedule_effect(/** @type {Effect} */ (reaction)); } } } }