svelte
Version:
Cybernetically enhanced web apps
389 lines (330 loc) • 9.53 kB
JavaScript
/** @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,
ASYNC,
WAS_MARKED,
CONNECTED,
DESTROYED
} from '#client/constants';
import {
active_reaction,
active_effect,
set_signal_status,
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, effect_tracking, teardown } from './effects.js';
import { eager_effects, internal_set, set_eager_effects, source } from './sources.js';
import { get_error } from '../../shared/dev.js';
import { async_mode_flag, 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_values, current_batch } from './batch.js';
import { unset_context } from './async.js';
import { deferred } from '../../shared/utils.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) {
// 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_error('created at');
}
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));
// only suspend in async deriveds created on initialisation
var should_suspend = !active_reaction;
/** @type {Map<Batch, ReturnType<typeof deferred<V>>>} */
var deferreds = new Map();
async_effect(() => {
if (DEV) current_async_effect = active_effect;
/** @type {ReturnType<typeof deferred<V>>} */
var d = deferred();
promise = d.promise;
try {
// If this code is changed at some point, make sure to still access the then property
// of fn() to read any signals it might access, so that we track them as dependencies.
// We call `unset_context` to undo any `save` calls that happen inside `fn()`
Promise.resolve(fn())
.then(d.resolve, d.reject)
.then(() => {
if (batch === current_batch && batch.committed) {
// if the batch was rejected as stale, we need to cleanup
// after any `$.save(...)` calls inside `fn()`
batch.deactivate();
}
unset_context();
});
} catch (error) {
d.reject(error);
unset_context();
}
if (DEV) current_async_effect = null;
var batch = /** @type {Batch} */ (current_batch);
if (should_suspend) {
var blocking = !boundary.is_pending();
boundary.update_pending_count(1);
batch.increment(blocking);
deferreds.get(batch)?.reject(STALE_REACTION);
deferreds.delete(batch); // delete to ensure correct order in Map iteration below
deferreds.set(batch, d);
}
/**
* @param {any} value
* @param {unknown} error
*/
const handler = (value, error = undefined) => {
current_async_effect = null;
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);
// All prior async derived runs are now stale
for (const [b, d] of deferreds) {
deferreds.delete(b);
if (b === batch) break;
d.reject(STALE_REACTION);
}
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);
batch.decrement(blocking);
}
};
d.promise.then(handler, (e) => handler(null, e || 'unknown'));
});
teardown(() => {
for (const d of deferreds.values()) {
d.reject(STALE_REACTION);
}
});
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);
if (!async_mode_flag) 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) {
// The original parent effect might've been destroyed but the derived
// is used elsewhere now - do not return the destroyed effect in that case
return (parent.f & DESTROYED) === 0 ? /** @type {Effect} */ (parent) : null;
}
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_eager_effects = eager_effects;
set_eager_effects(new Set());
try {
if (stack.includes(derived)) {
e.derived_references_self();
}
stack.push(derived);
derived.f &= ~WAS_MARKED;
destroy_derived_effects(derived);
value = update_reaction(derived);
} finally {
set_active_effect(prev_active_effect);
set_eager_effects(prev_eager_effects);
stack.pop();
}
} else {
try {
derived.f &= ~WAS_MARKED;
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)) {
// in a fork, we don't update the underlying value, just `batch_values`.
// the underlying value will be updated when the fork is committed.
// otherwise, the next time we get here after a 'real world' state
// change, `derived.equals` may incorrectly return `true`
if (!current_batch?.is_fork) {
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;
}
// During time traveling we don't want to reset the status so that
// traversal of the graph in the other batches still happens
if (batch_values !== null) {
// only cache the value if we're in a tracking context, otherwise we won't
// clear the cache in `mark_reactions` when dependencies are updated
if (effect_tracking() || current_batch?.is_fork) {
batch_values.set(derived, value);
}
} else {
var status = (derived.f & CONNECTED) === 0 ? MAYBE_DIRTY : CLEAN;
set_signal_status(derived, status);
}
}