mirror of https://github.com/sveltejs/svelte
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.
340 lines
7.2 KiB
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);
|
|
}
|