UNPKG

svelte

Version:

Cybernetically enhanced web apps

351 lines (293 loc) • 7.76 kB
/** @import { Derived, Effect, Source } from '#client' */ /** @import { Batch } from './batch.js'; */ import { DEV } from 'esm-env'; import { ERROR_VALUE, CLEAN, DERIVED, DIRTY, EFFECT_PRESERVED, MAYBE_DIRTY, STALE_REACTION, UNOWNED, ASYNC } from '#client/constants'; import { active_reaction, active_effect, set_signal_status, skip_reaction, update_reaction, increment_write_version, set_active_effect, push_reaction_value, is_destroying_effect } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; import * as w from '../warnings.js'; import { async_effect, destroy_effect } from './effects.js'; import { inspect_effects, internal_set, set_inspect_effects, source } from './sources.js'; import { get_stack } from '../dev/tracing.js'; import { tracing_mode_flag } from '../../flags/index.js'; import { Boundary } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; import { batch_deriveds, current_batch } from './batch.js'; import { unset_context } from './async.js'; /** @type {Effect | null} */ export let current_async_effect = null; /** @param {Effect | null} v */ export function set_from_async_derived(v) { current_async_effect = v; } export const recent_async_deriveds = new Set(); /** * @template V * @param {() => V} fn * @returns {Derived<V>} */ /*#__NO_SIDE_EFFECTS__*/ export function derived(fn) { var flags = DERIVED | DIRTY; var parent_derived = active_reaction !== null && (active_reaction.f & DERIVED) !== 0 ? /** @type {Derived} */ (active_reaction) : null; if (active_effect === null || (parent_derived !== null && (parent_derived.f & UNOWNED) !== 0)) { flags |= UNOWNED; } else { // Since deriveds are evaluated lazily, any effects created inside them are // created too late to ensure that the parent effect is added to the tree active_effect.f |= EFFECT_PRESERVED; } /** @type {Derived<V>} */ const signal = { ctx: component_context, deps: null, effects: null, equals, f: flags, fn, reactions: null, rv: 0, v: /** @type {V} */ (UNINITIALIZED), wv: 0, parent: parent_derived ?? active_effect, ac: null }; if (DEV && tracing_mode_flag) { signal.created = get_stack('CreatedAt'); } return signal; } /** * @template V * @param {() => V | Promise<V>} fn * @param {string} [location] If provided, print a warning if the value is not read immediately after update * @returns {Promise<Source<V>>} */ /*#__NO_SIDE_EFFECTS__*/ export function async_derived(fn, location) { let parent = /** @type {Effect | null} */ (active_effect); if (parent === null) { e.async_derived_orphan(); } var boundary = /** @type {Boundary} */ (parent.b); var promise = /** @type {Promise<V>} */ (/** @type {unknown} */ (undefined)); var signal = source(/** @type {V} */ (UNINITIALIZED)); /** @type {Promise<V> | null} */ var prev = null; // only suspend in async deriveds created on initialisation var should_suspend = !active_reaction; async_effect(() => { if (DEV) current_async_effect = active_effect; try { var p = fn(); } catch (error) { p = Promise.reject(error); } if (DEV) current_async_effect = null; var r = () => p; promise = prev?.then(r, r) ?? Promise.resolve(p); prev = promise; var batch = /** @type {Batch} */ (current_batch); var pending = boundary.pending; if (should_suspend) { boundary.update_pending_count(1); if (!pending) batch.increment(); } /** * @param {any} value * @param {unknown} error */ const handler = (value, error = undefined) => { prev = null; current_async_effect = null; if (!pending) batch.activate(); if (error) { if (error !== STALE_REACTION) { signal.f |= ERROR_VALUE; // @ts-expect-error the error is the wrong type, but we don't care internal_set(signal, error); } } else { if ((signal.f & ERROR_VALUE) !== 0) { signal.f ^= ERROR_VALUE; } internal_set(signal, value); if (DEV && location !== undefined) { recent_async_deriveds.add(signal); setTimeout(() => { if (recent_async_deriveds.has(signal)) { w.await_waterfall(/** @type {string} */ (signal.label), location); recent_async_deriveds.delete(signal); } }); } } if (should_suspend) { boundary.update_pending_count(-1); if (!pending) batch.decrement(); } unset_context(); }; promise.then(handler, (e) => handler(null, e || 'unknown')); if (batch) { return () => { queueMicrotask(() => batch.neuter()); }; } }); if (DEV) { // add a flag that lets this be printed as a derived // when using `$inspect.trace()` signal.f |= ASYNC; } return new Promise((fulfil) => { /** @param {Promise<V>} p */ function next(p) { function go() { if (p === promise) { fulfil(signal); } else { // if the effect re-runs before the initial promise // resolves, delay resolution until we have a value next(promise); } } p.then(go, go); } next(promise); }); } /** * @template V * @param {() => V} fn * @returns {Derived<V>} */ /*#__NO_SIDE_EFFECTS__*/ export function user_derived(fn) { const d = derived(fn); push_reaction_value(d); return d; } /** * @template V * @param {() => V} fn * @returns {Derived<V>} */ /*#__NO_SIDE_EFFECTS__*/ export function derived_safe_equal(fn) { const signal = derived(fn); signal.equals = safe_equals; return signal; } /** * @param {Derived} derived * @returns {void} */ export function destroy_derived_effects(derived) { var effects = derived.effects; if (effects !== null) { derived.effects = null; for (var i = 0; i < effects.length; i += 1) { destroy_effect(/** @type {Effect} */ (effects[i])); } } } /** * The currently updating deriveds, used to detect infinite recursion * in dev mode and provide a nicer error than 'too much recursion' * @type {Derived[]} */ let stack = []; /** * @param {Derived} derived * @returns {Effect | null} */ function get_derived_parent_effect(derived) { var parent = derived.parent; while (parent !== null) { if ((parent.f & DERIVED) === 0) { return /** @type {Effect} */ (parent); } parent = parent.parent; } return null; } /** * @template T * @param {Derived} derived * @returns {T} */ export function execute_derived(derived) { var value; var prev_active_effect = active_effect; set_active_effect(get_derived_parent_effect(derived)); if (DEV) { let prev_inspect_effects = inspect_effects; set_inspect_effects(new Set()); try { if (stack.includes(derived)) { e.derived_references_self(); } stack.push(derived); destroy_derived_effects(derived); value = update_reaction(derived); } finally { set_active_effect(prev_active_effect); set_inspect_effects(prev_inspect_effects); stack.pop(); } } else { try { destroy_derived_effects(derived); value = update_reaction(derived); } finally { set_active_effect(prev_active_effect); } } return value; } /** * @param {Derived} derived * @returns {void} */ export function update_derived(derived) { var value = execute_derived(derived); if (!derived.equals(value)) { derived.v = value; derived.wv = increment_write_version(); } // don't mark derived clean if we're reading it inside a // cleanup function, or it will cache a stale value if (is_destroying_effect) return; if (batch_deriveds !== null) { batch_deriveds.set(derived, derived.v); } else { var status = (skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN; set_signal_status(derived, status); } }