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.
604 lines
14 KiB
604 lines
14 KiB
/** @import { Derived, Effect, Source } from '#client' */
|
|
import {
|
|
BLOCK_EFFECT,
|
|
BRANCH_EFFECT,
|
|
CLEAN,
|
|
DESTROYED,
|
|
DIRTY,
|
|
EFFECT,
|
|
ASYNC,
|
|
INERT,
|
|
RENDER_EFFECT,
|
|
ROOT_EFFECT,
|
|
USER_EFFECT
|
|
} from '#client/constants';
|
|
import { async_mode_flag } from '../../flags/index.js';
|
|
import { deferred, define_property } from '../../shared/utils.js';
|
|
import { get_pending_boundary } from '../dom/blocks/boundary.js';
|
|
import {
|
|
active_effect,
|
|
is_dirty,
|
|
is_updating_effect,
|
|
set_is_updating_effect,
|
|
set_signal_status,
|
|
update_effect,
|
|
write_version
|
|
} from '../runtime.js';
|
|
import * as e from '../errors.js';
|
|
import { flush_tasks } from '../dom/task.js';
|
|
import { DEV } from 'esm-env';
|
|
import { invoke_error_boundary } from '../error-handling.js';
|
|
import { old_values } from './sources.js';
|
|
import { unlink_effect } from './effects.js';
|
|
import { unset_context } from './async.js';
|
|
|
|
/** @type {Set<Batch>} */
|
|
const batches = new Set();
|
|
|
|
/** @type {Batch | null} */
|
|
export let current_batch = null;
|
|
|
|
/**
|
|
* When time travelling, we re-evaluate deriveds based on the temporary
|
|
* values of their dependencies rather than their actual values, and cache
|
|
* the results in this map rather than on the deriveds themselves
|
|
* @type {Map<Derived, any> | null}
|
|
*/
|
|
export let batch_deriveds = null;
|
|
|
|
/** @type {Effect[]} Stack of effects, dev only */
|
|
export let dev_effect_stack = [];
|
|
|
|
/** @type {Effect[]} */
|
|
let queued_root_effects = [];
|
|
|
|
/** @type {Effect | null} */
|
|
let last_scheduled_effect = null;
|
|
|
|
let is_flushing = false;
|
|
|
|
export class Batch {
|
|
/**
|
|
* The current values of any sources that are updated in this batch
|
|
* They keys of this map are identical to `this.#previous`
|
|
* @type {Map<Source, any>}
|
|
*/
|
|
#current = new Map();
|
|
|
|
/**
|
|
* The values of any sources that are updated in this batch _before_ those updates took place.
|
|
* They keys of this map are identical to `this.#current`
|
|
* @type {Map<Source, any>}
|
|
*/
|
|
#previous = new Map();
|
|
|
|
/**
|
|
* When the batch is committed (and the DOM is updated), we need to remove old branches
|
|
* and append new ones by calling the functions added inside (if/each/key/etc) blocks
|
|
* @type {Set<() => void>}
|
|
*/
|
|
#callbacks = new Set();
|
|
|
|
/**
|
|
* The number of async effects that are currently in flight
|
|
*/
|
|
#pending = 0;
|
|
|
|
/**
|
|
* A deferred that resolves when the batch is committed, used with `settled()`
|
|
* TODO replace with Promise.withResolvers once supported widely enough
|
|
* @type {{ promise: Promise<void>, resolve: (value?: any) => void, reject: (reason: unknown) => void } | null}
|
|
*/
|
|
#deferred = null;
|
|
|
|
/**
|
|
* True if an async effect inside this batch resolved and
|
|
* its parent branch was already deleted
|
|
*/
|
|
#neutered = false;
|
|
|
|
/**
|
|
* Async effects (created inside `async_derived`) encountered during processing.
|
|
* These run after the rest of the batch has updated, since they should
|
|
* always have the latest values
|
|
* @type {Effect[]}
|
|
*/
|
|
#async_effects = [];
|
|
|
|
/**
|
|
* The same as `#async_effects`, but for effects inside a newly-created
|
|
* `<svelte:boundary>` — these do not prevent the batch from committing
|
|
* @type {Effect[]}
|
|
*/
|
|
#boundary_async_effects = [];
|
|
|
|
/**
|
|
* Template effects and `$effect.pre` effects, which run when
|
|
* a batch is committed
|
|
* @type {Effect[]}
|
|
*/
|
|
#render_effects = [];
|
|
|
|
/**
|
|
* The same as `#render_effects`, but for `$effect` (which runs after)
|
|
* @type {Effect[]}
|
|
*/
|
|
#effects = [];
|
|
|
|
/**
|
|
* Block effects, which may need to re-run on subsequent flushes
|
|
* in order to update internal sources (e.g. each block items)
|
|
* @type {Effect[]}
|
|
*/
|
|
#block_effects = [];
|
|
|
|
/**
|
|
* A set of branches that still exist, but will be destroyed when this batch
|
|
* is committed — we skip over these during `process`
|
|
* @type {Set<Effect>}
|
|
*/
|
|
skipped_effects = new Set();
|
|
|
|
/**
|
|
*
|
|
* @param {Effect[]} root_effects
|
|
*/
|
|
#process(root_effects) {
|
|
queued_root_effects = [];
|
|
|
|
/** @type {Map<Source, { v: unknown, wv: number }> | null} */
|
|
var current_values = null;
|
|
|
|
// if there are multiple batches, we are 'time travelling' —
|
|
// we need to undo the changes belonging to any batch
|
|
// other than the current one
|
|
if (batches.size > 1) {
|
|
current_values = new Map();
|
|
batch_deriveds = new Map();
|
|
|
|
for (const [source, current] of this.#current) {
|
|
current_values.set(source, { v: source.v, wv: source.wv });
|
|
source.v = current;
|
|
}
|
|
|
|
for (const batch of batches) {
|
|
if (batch === this) continue;
|
|
|
|
for (const [source, previous] of batch.#previous) {
|
|
if (!current_values.has(source)) {
|
|
current_values.set(source, { v: source.v, wv: source.wv });
|
|
source.v = previous;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const root of root_effects) {
|
|
this.#traverse_effect_tree(root);
|
|
}
|
|
|
|
// if we didn't start any new async work, and no async work
|
|
// is outstanding from a previous flush, commit
|
|
if (this.#async_effects.length === 0 && this.#pending === 0) {
|
|
var render_effects = this.#render_effects;
|
|
var effects = this.#effects;
|
|
|
|
this.#render_effects = [];
|
|
this.#effects = [];
|
|
this.#block_effects = [];
|
|
|
|
this.#commit();
|
|
|
|
flush_queued_effects(render_effects);
|
|
flush_queued_effects(effects);
|
|
|
|
this.#deferred?.resolve();
|
|
} else {
|
|
// otherwise mark effects clean so they get scheduled on the next run
|
|
for (const e of this.#render_effects) set_signal_status(e, CLEAN);
|
|
for (const e of this.#effects) set_signal_status(e, CLEAN);
|
|
for (const e of this.#block_effects) set_signal_status(e, CLEAN);
|
|
}
|
|
|
|
if (current_values) {
|
|
for (const [source, { v, wv }] of current_values) {
|
|
// reset the source to the current value (unless
|
|
// it got a newer value as a result of effects running)
|
|
if (source.wv <= wv) {
|
|
source.v = v;
|
|
}
|
|
}
|
|
|
|
batch_deriveds = null;
|
|
}
|
|
|
|
for (const effect of this.#async_effects) {
|
|
update_effect(effect);
|
|
}
|
|
|
|
for (const effect of this.#boundary_async_effects) {
|
|
update_effect(effect);
|
|
}
|
|
|
|
this.#async_effects = [];
|
|
this.#boundary_async_effects = [];
|
|
}
|
|
|
|
/**
|
|
* Traverse the effect tree, executing effects or stashing
|
|
* them for later execution as appropriate
|
|
* @param {Effect} root
|
|
*/
|
|
#traverse_effect_tree(root) {
|
|
root.f ^= CLEAN;
|
|
|
|
var effect = root.first;
|
|
|
|
while (effect !== null) {
|
|
var flags = effect.f;
|
|
var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0;
|
|
var is_skippable_branch = is_branch && (flags & CLEAN) !== 0;
|
|
|
|
var skip = is_skippable_branch || (flags & INERT) !== 0 || this.skipped_effects.has(effect);
|
|
|
|
if (!skip && effect.fn !== null) {
|
|
if (is_branch) {
|
|
effect.f ^= CLEAN;
|
|
} else if ((flags & EFFECT) !== 0) {
|
|
this.#effects.push(effect);
|
|
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
|
|
this.#render_effects.push(effect);
|
|
} else if (is_dirty(effect)) {
|
|
if ((flags & ASYNC) !== 0) {
|
|
var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects;
|
|
effects.push(effect);
|
|
} else {
|
|
if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect);
|
|
update_effect(effect);
|
|
}
|
|
}
|
|
|
|
var child = effect.first;
|
|
|
|
if (child !== null) {
|
|
effect = child;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var parent = effect.parent;
|
|
effect = effect.next;
|
|
|
|
while (effect === null && parent !== null) {
|
|
effect = parent.next;
|
|
parent = parent.parent;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Associate a change to a given source with the current
|
|
* batch, noting its previous and current values
|
|
* @param {Source} source
|
|
* @param {any} value
|
|
*/
|
|
capture(source, value) {
|
|
if (!this.#previous.has(source)) {
|
|
this.#previous.set(source, value);
|
|
}
|
|
|
|
this.#current.set(source, source.v);
|
|
}
|
|
|
|
activate() {
|
|
current_batch = this;
|
|
}
|
|
|
|
deactivate() {
|
|
current_batch = null;
|
|
}
|
|
|
|
neuter() {
|
|
this.#neutered = true;
|
|
}
|
|
|
|
flush() {
|
|
if (queued_root_effects.length > 0) {
|
|
this.flush_effects();
|
|
} else {
|
|
this.#commit();
|
|
}
|
|
|
|
if (current_batch !== this) {
|
|
// this can happen if a `flushSync` occurred during `this.flush_effects()`,
|
|
// which is permitted in legacy mode despite being a terrible idea
|
|
return;
|
|
}
|
|
|
|
if (this.#pending === 0) {
|
|
batches.delete(this);
|
|
}
|
|
|
|
current_batch = null;
|
|
}
|
|
|
|
flush_effects() {
|
|
var was_updating_effect = is_updating_effect;
|
|
is_flushing = true;
|
|
|
|
try {
|
|
var flush_count = 0;
|
|
set_is_updating_effect(true);
|
|
|
|
while (queued_root_effects.length > 0) {
|
|
if (flush_count++ > 1000) {
|
|
infinite_loop_guard();
|
|
}
|
|
|
|
this.#process(queued_root_effects);
|
|
old_values.clear();
|
|
}
|
|
} finally {
|
|
is_flushing = false;
|
|
set_is_updating_effect(was_updating_effect);
|
|
|
|
last_scheduled_effect = null;
|
|
if (DEV) {
|
|
dev_effect_stack = [];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Append and remove branches to/from the DOM
|
|
*/
|
|
#commit() {
|
|
if (!this.#neutered) {
|
|
for (const fn of this.#callbacks) {
|
|
fn();
|
|
}
|
|
}
|
|
|
|
this.#callbacks.clear();
|
|
}
|
|
|
|
increment() {
|
|
this.#pending += 1;
|
|
}
|
|
|
|
decrement() {
|
|
this.#pending -= 1;
|
|
|
|
if (this.#pending === 0) {
|
|
for (const e of this.#render_effects) {
|
|
set_signal_status(e, DIRTY);
|
|
schedule_effect(e);
|
|
}
|
|
|
|
for (const e of this.#effects) {
|
|
set_signal_status(e, DIRTY);
|
|
schedule_effect(e);
|
|
}
|
|
|
|
for (const e of this.#block_effects) {
|
|
set_signal_status(e, DIRTY);
|
|
schedule_effect(e);
|
|
}
|
|
|
|
this.#render_effects = [];
|
|
this.#effects = [];
|
|
|
|
this.flush();
|
|
}
|
|
}
|
|
|
|
/** @param {() => void} fn */
|
|
add_callback(fn) {
|
|
this.#callbacks.add(fn);
|
|
}
|
|
|
|
settled() {
|
|
return (this.#deferred ??= deferred()).promise;
|
|
}
|
|
|
|
static ensure() {
|
|
if (current_batch === null) {
|
|
const batch = (current_batch = new Batch());
|
|
batches.add(current_batch);
|
|
|
|
queueMicrotask(() => {
|
|
if (current_batch !== batch) {
|
|
// a flushSync happened in the meantime
|
|
return;
|
|
}
|
|
|
|
batch.flush();
|
|
});
|
|
}
|
|
|
|
return current_batch;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Synchronously flush any pending updates.
|
|
* Returns void if no callback is provided, otherwise returns the result of calling the callback.
|
|
* @template [T=void]
|
|
* @param {(() => T) | undefined} [fn]
|
|
* @returns {T}
|
|
*/
|
|
export function flushSync(fn) {
|
|
if (async_mode_flag && active_effect !== null) {
|
|
e.flush_sync_in_effect();
|
|
}
|
|
|
|
var result;
|
|
|
|
const batch = Batch.ensure();
|
|
|
|
if (fn) {
|
|
batch.flush_effects();
|
|
|
|
result = fn();
|
|
}
|
|
|
|
while (true) {
|
|
flush_tasks();
|
|
|
|
if (queued_root_effects.length === 0) {
|
|
if (batch === current_batch) {
|
|
batch.flush();
|
|
}
|
|
|
|
// this would be reset in `batch.flush_effects()` but since we are early returning here,
|
|
// we need to reset it here as well in case the first time there's 0 queued root effects
|
|
last_scheduled_effect = null;
|
|
|
|
if (DEV) {
|
|
dev_effect_stack = [];
|
|
}
|
|
|
|
return /** @type {T} */ (result);
|
|
}
|
|
|
|
batch.flush_effects();
|
|
}
|
|
}
|
|
|
|
function log_effect_stack() {
|
|
// eslint-disable-next-line no-console
|
|
console.error(
|
|
'Last ten effects were: ',
|
|
dev_effect_stack.slice(-10).map((d) => d.fn)
|
|
);
|
|
dev_effect_stack = [];
|
|
}
|
|
|
|
function infinite_loop_guard() {
|
|
try {
|
|
e.effect_update_depth_exceeded();
|
|
} catch (error) {
|
|
if (DEV) {
|
|
// stack is garbage, ignore. Instead add a console.error message.
|
|
define_property(error, 'stack', {
|
|
value: ''
|
|
});
|
|
}
|
|
// Try and handle the error so it can be caught at a boundary, that's
|
|
// if there's an effect available from when it was last scheduled
|
|
if (last_scheduled_effect !== null) {
|
|
if (DEV) {
|
|
try {
|
|
invoke_error_boundary(error, last_scheduled_effect);
|
|
} catch (e) {
|
|
// Only log the effect stack if the error is re-thrown
|
|
log_effect_stack();
|
|
throw e;
|
|
}
|
|
} else {
|
|
invoke_error_boundary(error, last_scheduled_effect);
|
|
}
|
|
} else {
|
|
if (DEV) {
|
|
log_effect_stack();
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Array<Effect>} effects
|
|
* @returns {void}
|
|
*/
|
|
function flush_queued_effects(effects) {
|
|
var length = effects.length;
|
|
if (length === 0) return;
|
|
|
|
for (var i = 0; i < length; i++) {
|
|
var effect = effects[i];
|
|
|
|
if ((effect.f & (DESTROYED | INERT)) === 0) {
|
|
if (is_dirty(effect)) {
|
|
var wv = write_version;
|
|
|
|
update_effect(effect);
|
|
|
|
// Effects with no dependencies or teardown do not get added to the effect tree.
|
|
// Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we
|
|
// don't know if we need to keep them until they are executed. Doing the check
|
|
// here (rather than in `update_effect`) allows us to skip the work for
|
|
// immediate effects.
|
|
if (effect.deps === null && effect.first === null && effect.nodes_start === null) {
|
|
if (effect.teardown === null) {
|
|
// remove this effect from the graph
|
|
unlink_effect(effect);
|
|
} else {
|
|
// keep the effect in the graph, but free up some memory
|
|
effect.fn = null;
|
|
}
|
|
}
|
|
|
|
// if state is written in a user effect, abort and re-schedule, lest we run
|
|
// effects that should be removed as a result of the state change
|
|
if (write_version > wv && (effect.f & USER_EFFECT) !== 0) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (; i < length; i += 1) {
|
|
schedule_effect(effects[i]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Effect} signal
|
|
* @returns {void}
|
|
*/
|
|
export function schedule_effect(signal) {
|
|
var effect = (last_scheduled_effect = signal);
|
|
|
|
while (effect.parent !== null) {
|
|
effect = effect.parent;
|
|
var flags = effect.f;
|
|
|
|
// if the effect is being scheduled because a parent (each/await/etc) block
|
|
// updated an internal source, bail out or we'll cause a second flush
|
|
if (is_flushing && effect === active_effect && (flags & BLOCK_EFFECT) !== 0) {
|
|
return;
|
|
}
|
|
|
|
if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) {
|
|
if ((flags & CLEAN) === 0) return;
|
|
effect.f ^= CLEAN;
|
|
}
|
|
}
|
|
|
|
queued_root_effects.push(effect);
|
|
}
|
|
|
|
export function suspend() {
|
|
var boundary = get_pending_boundary();
|
|
var batch = /** @type {Batch} */ (current_batch);
|
|
var pending = boundary.pending;
|
|
|
|
boundary.update_pending_count(1);
|
|
if (!pending) batch.increment();
|
|
|
|
return function unsuspend() {
|
|
boundary.update_pending_count(-1);
|
|
if (!pending) batch.decrement();
|
|
|
|
unset_context();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Forcibly remove all current batches, to prevent cross-talk between tests
|
|
*/
|
|
export function clear() {
|
|
batches.clear();
|
|
}
|