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/reactivity/deriveds.js

340 lines
7.2 KiB

/** @import { Derived, Effect, Source } from '#client' */
/** @import { Batch } from './batch.js'; */
import { DEV } from 'esm-env';
import {
CLEAN,
DERIVED,
DESTROYED,
DIRTY,
EFFECT_ASYNC,
EFFECT_PRESERVED,
MAYBE_DIRTY,
STALE_REACTION,
UNOWNED
} from '#client/constants';
import {
active_reaction,
active_effect,
set_signal_status,
skip_reaction,
update_reaction,
increment_write_version,
set_active_effect,
handle_error,
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 { destroy_effect, render_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 { capture, get_pending_boundary } from '../dom/blocks/boundary.js';
import { component_context } from '../context.js';
import { UNINITIALIZED } from '../../../constants.js';
import { current_batch } from './batch.js';
/** @type {Effect | null} */
export let from_async_derived = null;
/** @param {Effect | null} v */
export function set_from_async_derived(v) {
from_async_derived = 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} */ (null),
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) {
throw new Error('TODO cannot create unowned async derived');
}
let boundary = get_pending_boundary(parent);
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;
render_effect(() => {
if (DEV) from_async_derived = active_effect;
var p = fn();
if (DEV) from_async_derived = null;
promise =
prev === null
? Promise.resolve(p)
: prev.then(
() => p,
() => p
);
prev = promise;
var restore = capture();
var batch = /** @type {Batch} */ (current_batch);
var ran = boundary.ran;
if (should_suspend) {
if (!ran) {
boundary.increment();
} else {
batch.increment();
}
}
/**
* @param {any} value
* @param {unknown} error
*/
const handler = (value, error = undefined) => {
prev = null;
if ((parent.f & DESTROYED) !== 0) {
return;
}
restore();
from_async_derived = null;
if (should_suspend) {
if (!ran) {
boundary.decrement();
} else {
batch.decrement();
}
}
if (ran) batch.restore();
if (error) {
if (error !== STALE_REACTION) {
handle_error(error, parent, null, parent.ctx);
}
} else {
internal_set(signal, value);
if (DEV && location !== undefined) {
recent_async_deriveds.add(signal);
setTimeout(() => {
if (recent_async_deriveds.has(signal)) {
w.await_waterfall(location);
recent_async_deriveds.delete(signal);
}
});
}
}
if (ran) batch.flush();
};
promise.then(handler, (e) => handler(null, e || 'unknown'));
}, EFFECT_ASYNC | EFFECT_PRESERVED);
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;
var status =
(skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN;
set_signal_status(derived, status);
}