svelte
Version:
Cybernetically enhanced web apps
619 lines (536 loc) • 14.4 kB
JavaScript
/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager } from '#client' */
import {
check_dirtiness,
active_effect,
active_reaction,
update_effect,
get,
is_destroying_effect,
remove_reactions,
schedule_effect,
set_active_reaction,
set_is_destroying_effect,
set_signal_status,
untrack,
untracking
} from '../runtime.js';
import {
DIRTY,
BRANCH_EFFECT,
RENDER_EFFECT,
EFFECT,
DESTROYED,
INERT,
EFFECT_RAN,
BLOCK_EFFECT,
ROOT_EFFECT,
EFFECT_TRANSPARENT,
DERIVED,
UNOWNED,
CLEAN,
INSPECT_EFFECT,
HEAD_EFFECT,
MAYBE_DIRTY,
EFFECT_HAS_DERIVED,
BOUNDARY_EFFECT
} from '../constants.js';
import { set } from './sources.js';
import * as e from '../errors.js';
import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
import { derived } from './deriveds.js';
import { component_context, dev_current_component_function } from '../context.js';
/**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
*/
export function validate_effect(rune) {
if (active_effect === null && active_reaction === null) {
e.effect_orphan(rune);
}
if (active_reaction !== null && (active_reaction.f & UNOWNED) !== 0 && active_effect === null) {
e.effect_in_unowned_derived();
}
if (is_destroying_effect) {
e.effect_in_teardown(rune);
}
}
/**
* @param {Effect} effect
* @param {Effect} parent_effect
*/
function push_effect(effect, parent_effect) {
var parent_last = parent_effect.last;
if (parent_last === null) {
parent_effect.last = parent_effect.first = effect;
} else {
parent_last.next = effect;
effect.prev = parent_last;
parent_effect.last = effect;
}
}
/**
* @param {number} type
* @param {null | (() => void | (() => void))} fn
* @param {boolean} sync
* @param {boolean} push
* @returns {Effect}
*/
function create_effect(type, fn, sync, push = true) {
var parent = active_effect;
if (DEV) {
// Ensure the parent is never an inspect effect
while (parent !== null && (parent.f & INSPECT_EFFECT) !== 0) {
parent = parent.parent;
}
}
/** @type {Effect} */
var effect = {
ctx: component_context,
deps: null,
nodes_start: null,
nodes_end: null,
f: type | DIRTY,
first: null,
fn,
last: null,
next: null,
parent,
prev: null,
teardown: null,
transitions: null,
wv: 0
};
if (DEV) {
effect.component_function = dev_current_component_function;
}
if (sync) {
try {
update_effect(effect);
effect.f |= EFFECT_RAN;
} catch (e) {
destroy_effect(effect);
throw e;
}
} else if (fn !== null) {
schedule_effect(effect);
}
// if an effect has no dependencies, no DOM and no teardown function,
// don't bother adding it to the effect tree
var inert =
sync &&
effect.deps === null &&
effect.first === null &&
effect.nodes_start === null &&
effect.teardown === null &&
(effect.f & (EFFECT_HAS_DERIVED | BOUNDARY_EFFECT)) === 0;
if (!inert && push) {
if (parent !== null) {
push_effect(effect, parent);
}
// if we're in a derived, add the effect there too
if (active_reaction !== null && (active_reaction.f & DERIVED) !== 0) {
var derived = /** @type {Derived} */ (active_reaction);
(derived.effects ??= []).push(effect);
}
}
return effect;
}
/**
* Internal representation of `$effect.tracking()`
* @returns {boolean}
*/
export function effect_tracking() {
return active_reaction !== null && !untracking;
}
/**
* @param {() => void} fn
*/
export function teardown(fn) {
const effect = create_effect(RENDER_EFFECT, null, false);
set_signal_status(effect, CLEAN);
effect.teardown = fn;
return effect;
}
/**
* Internal representation of `$effect(...)`
* @param {() => void | (() => void)} fn
*/
export function user_effect(fn) {
validate_effect('$effect');
// Non-nested `$effect(...)` in a component should be deferred
// until the component is mounted
var defer =
active_effect !== null &&
(active_effect.f & BRANCH_EFFECT) !== 0 &&
component_context !== null &&
!component_context.m;
if (DEV) {
define_property(fn, 'name', {
value: '$effect'
});
}
if (defer) {
var context = /** @type {ComponentContext} */ (component_context);
(context.e ??= []).push({
fn,
effect: active_effect,
reaction: active_reaction
});
} else {
var signal = effect(fn);
return signal;
}
}
/**
* Internal representation of `$effect.pre(...)`
* @param {() => void | (() => void)} fn
* @returns {Effect}
*/
export function user_pre_effect(fn) {
validate_effect('$effect.pre');
if (DEV) {
define_property(fn, 'name', {
value: '$effect.pre'
});
}
return render_effect(fn);
}
/** @param {() => void | (() => void)} fn */
export function inspect_effect(fn) {
return create_effect(INSPECT_EFFECT, fn, true);
}
/**
* Internal representation of `$effect.root(...)`
* @param {() => void | (() => void)} fn
* @returns {() => void}
*/
export function effect_root(fn) {
const effect = create_effect(ROOT_EFFECT, fn, true);
return () => {
destroy_effect(effect);
};
}
/**
* An effect root whose children can transition out
* @param {() => void} fn
* @returns {(options?: { outro?: boolean }) => Promise<void>}
*/
export function component_root(fn) {
const effect = create_effect(ROOT_EFFECT, fn, true);
return (options = {}) => {
return new Promise((fulfil) => {
if (options.outro) {
pause_effect(effect, () => {
destroy_effect(effect);
fulfil(undefined);
});
} else {
destroy_effect(effect);
fulfil(undefined);
}
});
};
}
/**
* @param {() => void | (() => void)} fn
* @returns {Effect}
*/
export function effect(fn) {
return create_effect(EFFECT, fn, false);
}
/**
* Internal representation of `$: ..`
* @param {() => any} deps
* @param {() => void | (() => void)} fn
*/
export function legacy_pre_effect(deps, fn) {
var context = /** @type {ComponentContextLegacy} */ (component_context);
/** @type {{ effect: null | Effect, ran: boolean }} */
var token = { effect: null, ran: false };
context.l.r1.push(token);
token.effect = render_effect(() => {
deps();
// If this legacy pre effect has already run before the end of the reset, then
// bail out to emulate the same behavior.
if (token.ran) return;
token.ran = true;
set(context.l.r2, true);
untrack(fn);
});
}
export function legacy_pre_effect_reset() {
var context = /** @type {ComponentContextLegacy} */ (component_context);
render_effect(() => {
if (!get(context.l.r2)) return;
// Run dirty `$:` statements
for (var token of context.l.r1) {
var effect = token.effect;
// If the effect is CLEAN, then make it MAYBE_DIRTY. This ensures we traverse through
// the effects dependencies and correctly ensure each dependency is up-to-date.
if ((effect.f & CLEAN) !== 0) {
set_signal_status(effect, MAYBE_DIRTY);
}
if (check_dirtiness(effect)) {
update_effect(effect);
}
token.ran = false;
}
context.l.r2.v = false; // set directly to avoid rerunning this effect
});
}
/**
* @param {() => void | (() => void)} fn
* @returns {Effect}
*/
export function render_effect(fn) {
return create_effect(RENDER_EFFECT, fn, true);
}
/**
* @param {(...expressions: any) => void | (() => void)} fn
* @param {Array<() => any>} thunks
* @returns {Effect}
*/
export function template_effect(fn, thunks = [], d = derived) {
const deriveds = thunks.map(d);
const effect = () => fn(...deriveds.map(get));
if (DEV) {
define_property(effect, 'name', {
value: '{expression}'
});
}
return block(effect);
}
/**
* @param {(() => void)} fn
* @param {number} flags
*/
export function block(fn, flags = 0) {
return create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
}
/**
* @param {(() => void)} fn
* @param {boolean} [push]
*/
export function branch(fn, push = true) {
return create_effect(RENDER_EFFECT | BRANCH_EFFECT, fn, true, push);
}
/**
* @param {Effect} effect
*/
export function execute_effect_teardown(effect) {
var teardown = effect.teardown;
if (teardown !== null) {
const previously_destroying_effect = is_destroying_effect;
const previous_reaction = active_reaction;
set_is_destroying_effect(true);
set_active_reaction(null);
try {
teardown.call(null);
} finally {
set_is_destroying_effect(previously_destroying_effect);
set_active_reaction(previous_reaction);
}
}
}
/**
* @param {Effect} signal
* @param {boolean} remove_dom
* @returns {void}
*/
export function destroy_effect_children(signal, remove_dom = false) {
var effect = signal.first;
signal.first = signal.last = null;
while (effect !== null) {
var next = effect.next;
if ((effect.f & ROOT_EFFECT) !== 0) {
// this is now an independent root
effect.parent = null;
} else {
destroy_effect(effect, remove_dom);
}
effect = next;
}
}
/**
* @param {Effect} signal
* @returns {void}
*/
export function destroy_block_effect_children(signal) {
var effect = signal.first;
while (effect !== null) {
var next = effect.next;
if ((effect.f & BRANCH_EFFECT) === 0) {
destroy_effect(effect);
}
effect = next;
}
}
/**
* @param {Effect} effect
* @param {boolean} [remove_dom]
* @returns {void}
*/
export function destroy_effect(effect, remove_dom = true) {
var removed = false;
if ((remove_dom || (effect.f & HEAD_EFFECT) !== 0) && effect.nodes_start !== null) {
/** @type {TemplateNode | null} */
var node = effect.nodes_start;
var end = effect.nodes_end;
while (node !== null) {
/** @type {TemplateNode | null} */
var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node));
node.remove();
node = next;
}
removed = true;
}
destroy_effect_children(effect, remove_dom && !removed);
remove_reactions(effect, 0);
set_signal_status(effect, DESTROYED);
var transitions = effect.transitions;
if (transitions !== null) {
for (const transition of transitions) {
transition.stop();
}
}
execute_effect_teardown(effect);
var parent = effect.parent;
// If the parent doesn't have any children, then skip this work altogether
if (parent !== null && parent.first !== null) {
unlink_effect(effect);
}
if (DEV) {
effect.component_function = null;
}
// `first` and `child` are nulled out in destroy_effect_children
// we don't null out `parent` so that error propagation can work correctly
effect.next =
effect.prev =
effect.teardown =
effect.ctx =
effect.deps =
effect.fn =
effect.nodes_start =
effect.nodes_end =
null;
}
/**
* Detach an effect from the effect tree, freeing up memory and
* reducing the amount of work that happens on subsequent traversals
* @param {Effect} effect
*/
export function unlink_effect(effect) {
var parent = effect.parent;
var prev = effect.prev;
var next = effect.next;
if (prev !== null) prev.next = next;
if (next !== null) next.prev = prev;
if (parent !== null) {
if (parent.first === effect) parent.first = next;
if (parent.last === effect) parent.last = prev;
}
}
/**
* When a block effect is removed, we don't immediately destroy it or yank it
* out of the DOM, because it might have transitions. Instead, we 'pause' it.
* It stays around (in memory, and in the DOM) until outro transitions have
* completed, and if the state change is reversed then we _resume_ it.
* A paused effect does not update, and the DOM subtree becomes inert.
* @param {Effect} effect
* @param {() => void} [callback]
*/
export function pause_effect(effect, callback) {
/** @type {TransitionManager[]} */
var transitions = [];
pause_children(effect, transitions, true);
run_out_transitions(transitions, () => {
destroy_effect(effect);
if (callback) callback();
});
}
/**
* @param {TransitionManager[]} transitions
* @param {() => void} fn
*/
export function run_out_transitions(transitions, fn) {
var remaining = transitions.length;
if (remaining > 0) {
var check = () => --remaining || fn();
for (var transition of transitions) {
transition.out(check);
}
} else {
fn();
}
}
/**
* @param {Effect} effect
* @param {TransitionManager[]} transitions
* @param {boolean} local
*/
export function pause_children(effect, transitions, local) {
if ((effect.f & INERT) !== 0) return;
effect.f ^= INERT;
if (effect.transitions !== null) {
for (const transition of effect.transitions) {
if (transition.is_global || local) {
transitions.push(transition);
}
}
}
var child = effect.first;
while (child !== null) {
var sibling = child.next;
var transparent = (child.f & EFFECT_TRANSPARENT) !== 0 || (child.f & BRANCH_EFFECT) !== 0;
// TODO we don't need to call pause_children recursively with a linked list in place
// it's slightly more involved though as we have to account for `transparent` changing
// through the tree.
pause_children(child, transitions, transparent ? local : false);
child = sibling;
}
}
/**
* The opposite of `pause_effect`. We call this if (for example)
* `x` becomes falsy then truthy: `{#if x}...{/if}`
* @param {Effect} effect
*/
export function resume_effect(effect) {
resume_children(effect, true);
}
/**
* @param {Effect} effect
* @param {boolean} local
*/
function resume_children(effect, local) {
if ((effect.f & INERT) === 0) return;
effect.f ^= INERT;
// Ensure the effect is marked as clean again so that any dirty child
// effects can schedule themselves for execution
if ((effect.f & CLEAN) === 0) {
effect.f ^= CLEAN;
}
// If a dependency of this effect changed while it was paused,
// schedule the effect to update
if (check_dirtiness(effect)) {
set_signal_status(effect, DIRTY);
schedule_effect(effect);
}
var child = effect.first;
while (child !== null) {
var sibling = child.next;
var transparent = (child.f & EFFECT_TRANSPARENT) !== 0 || (child.f & BRANCH_EFFECT) !== 0;
// TODO we don't need to call resume_children recursively with a linked list in place
// it's slightly more involved though as we have to account for `transparent` changing
// through the tree.
resume_children(child, transparent ? local : false);
child = sibling;
}
if (effect.transitions !== null) {
for (const transition of effect.transitions) {
if (transition.is_global || local) {
transition.in();
}
}
}
}