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/batch.js

1722 lines
48 KiB

/** @import { Fork } from 'svelte' */
/** @import { Derived, Effect, Reaction, Signal, Source, Value } from '#client' */
import {
BLOCK_EFFECT,
BRANCH_EFFECT,
CLEAN,
DESTROYED,
DIRTY,
EFFECT,
ASYNC,
INERT,
RENDER_EFFECT,
ROOT_EFFECT,
MAYBE_DIRTY,
DERIVED,
EAGER_EFFECT,
ERROR_VALUE,
MANAGED_EFFECT,
REACTION_RAN,
DESTROYING,
FORK_ONLY_BRANCH
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js';
import {
active_reaction,
get,
increment_write_version,
is_dirty,
update_effect,
write_version
} from '../runtime.js';
import * as e from '../errors.js';
import { flush_tasks, queue_micro_task } from '../dom/task.js';
import { DEV } from 'esm-env';
import { invoke_error_boundary } from '../error-handling.js';
import {
eager_effects,
flush_eager_effects,
invalidate,
old_values,
source,
update
} from './sources.js';
import { eager_effect, teardown, unlink_effect } from './effects.js';
import { defer_effect } from './utils.js';
import { UNINITIALIZED } from '../../../constants.js';
import { set_signal_status } from './status.js';
import { log_effect_tree } from '../dev/debug.js';
import { OBSOLETE } from './deriveds.js';
/** @type {Batch | null} */
export let first_batch = null;
/** @type {Batch | null} */
let last_batch = null;
/** @type {Batch | null} */
export let current_batch = null;
/**
* This is needed to avoid overwriting inputs
* @type {Batch | null}
*/
export let previous_batch = null;
/**
* When time travelling (i.e. working in one batch, while other batches
* still have ongoing work), we ignore the real values of affected
* signals in favour of their values within the batch
* @type {Map<Value, any> | null}
*/
export let batch_values = null;
/**
* When time travelling (i.e. working in one batch, while other batches
* still have ongoing work), we ignore the real wv of affected
* signals in favour of their wv within the batch
* @type {Map<Signal, number> | null}
*/
export let wv_values = null;
/**
* Sources which were written to before the current batch. Used to discover dependencies between batches.
* @type {Map<Value, Batch> | null}
*/
export let held_sources = null;
/**
* Sources where the current batch reads an outdated value of a later batch (or if this is an eager batch, any other batch) not in its own `current` map.
* Used to discover dependencies between batches.
* @type {Map<Value, Batch> | null}
*/
export let stale_sources = null;
/**
* Whether `batch_values` handed `reaction` a value of one of its dependencies that differs
* from the real one, i.e. whether it (partly) ran with its batch's view rather than the latest values.
* Looking one level deep is enough: a derived that was itself computed from such a value
* was not written to the real world either, so it differs as well.
* @param {Reaction} reaction
*/
export function read_batch_local_value(reaction) {
var deps = reaction.deps;
if (batch_values === null || deps === null) return false;
for (var i = 0; i < deps.length; i++) {
if (batch_values.has(deps[i]) && batch_values.get(deps[i]) !== deps[i].v) return true;
}
return false;
}
/**
* What a new value of `signal` is compared against (as the `this` of its `equals`). While several batches
* exist (which is when a batch view is set up), that is what the current batch holds, rather than the
* signal itself: the batch's own (e.g. outdated async) value can differ from the real one
* @param {Value} signal
* @returns {{ v: any }}
*/
export function own_value(signal) {
if (batch_values === null) return signal;
var own = current_batch?.current.get(signal);
if (own !== undefined) return own;
// Outside of effect flushes, the view is the one of the current batch: it may show another value, e.g. a later
// batch's previous one. While effects are flushed, the view is the one of the flushed batch, but writes belong
// to a new batch (and deriveds are captured into both)
return previous_batch === null && batch_values.has(signal)
? { v: batch_values.get(signal) }
: signal;
}
/** @type {Effect | null} */
let last_scheduled_effect = null;
export let is_flushing_sync = false;
let is_processing = false;
/**
* During traversal, this is an array. Newly created effects are (if not immediately
* executed) pushed to this array, rather than going through the scheduling
* rigamarole that would cause another turn of the flush loop.
* @type {Effect[] | null}
*/
export let collected_effects = null;
/**
* An array of effects that are marked during traversal as a result of a `set`
* (not `internal_set`) call. These will be added to the next batch and
* trigger another `batch.process()`
* @type {Effect[] | null}
* @deprecated when we get rid of legacy mode and stores, we can get rid of this
*/
export let legacy_updates = null;
var flush_count = 0;
/** @type {Set<Value>} */
var source_stacks = new Set();
let uid = 1;
export class Batch {
/** Reflects the position in the queue, i.e. a batch with a lower id is an earlier batch */
id = 0;
/** True as soon as `#process` was called */
started = false;
linked = false;
/**
* The batch into which this batch has been merged (because it finished and
* depended on a prior batch that needs to resolve first), if any.
* @type {Batch | null}
*/
merged_into = null;
/**
* @type {Map<Effect, number> | null}
*/
#stale_effects = null;
/**
* Effects that were executed with stale values. Its wv is written into the map.
* Lazily initialized for performance reasons.
* @type {Map<Effect, number>}
*/
get stale_effects() {
return (this.#stale_effects ??= new Map());
}
/**
* @type {Set<Reaction> | null}
*/
#stale_readers = null;
/**
* Reactions that ran with values that differ from this batch's view (e.g. while running in an
* earlier batch, they read a value that this batch holds a newer version of), and that are
* therefore scheduled to re-run in this batch. Used to avoid scheduling the same reaction
* multiple times. Lazily initialized for performance reasons.
*/
get stale_readers() {
return (this.#stale_readers ??= new Set());
}
/**
* Block/async effects that were created or traversed in this batch. Only tracked
* for forks (`null` otherwise), because only they need it (see `mark`).
* @type {Set<Effect> | null}
*/
seen_effects = null;
/** @type {Batch | null} */
prev = null;
/** @type {Batch | null} */
next = null;
/**
* @type {Set<Batch> | null}
*/
#dependent = null;
/**
* Earlier batches that this batch depends on, i.e. that it will be merged into if it resolves first.
* Lazily initialized for performance reasons.
*/
get dependent() {
return (this.#dependent ??= new Set());
}
/**
* @type {Map<Effect, ReturnType<typeof deferred<any>>> | null}
*/
#async_deriveds = null;
/**
* All started async work in this batch.
* Lazily initialized for performance reasons.
* @type {Map<Effect, ReturnType<typeof deferred<any>>>}
*/
get async_deriveds() {
return (this.#async_deriveds ??= new Map());
}
/**
* The current values of any signals that are updated in this batch.
* `is_derived` is false for deriveds, too, if they were overridden via assignment.
* They keys of this map are identical to `this.previous`
* @type {Map<Value, { v: any, wv: number, is_derived: boolean }>}
*/
current = new Map();
/**
* The values and write versions of any signals (sources and deriveds) that are updated in this batch _before_ those updates took place.
* They keys of this map are identical to `this.current`
* @type {Map<Value, { v: any, wv: number }>}
*/
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<(batch: Batch) => void>}
*/
#commit_callbacks = new Set();
/**
* If a fork is discarded, we need to destroy any effects that are no longer needed
* @type {Set<(batch: Batch) => void>}
*/
#discard_callbacks = new Set();
/**
* The number of async effects that are currently in flight
*/
#pending = 0;
/**
* Async effects that are currently in flight, _not_ inside a pending boundary
* @type {Map<Effect, number> | null}
*/
#blocking_pending = null;
/**
* 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;
/**
* Effects that were scheduled in this batch but not yet 'resolved' into the
* root effects that need to be flushed. Resolving — the upwards traversal that
* marks the path to each effect on the shared effect tree (see #resolve) — is
* deferred until the batch is processed, so that the markers are created and
* consumed within a single traversal. Scheduling into other batches (which can
* happen concurrently, e.g. while a batch is committed) can therefore never
* observe (and be confused by) this batch's markers.
* May contain duplicates — deduplication happens during resolving
* @type {Effect[]}
*/
#scheduled = [];
/**
* Effects scheduled outside of a flush, and the status they are to have. Their status is only
* applied when this batch is processed, so that other batches don't run them before that
* @type {Map<Effect, number>}
*/
#unprocessed = new Map();
/**
* Deferred reactions and their status.
*
* Leaf (i.e. not block/async) effects (dirty and maybe_dirty) are stored because we need
* to reset their status when a batch becomes pending, to not pollute other batches.
*
* Dirty and maybe_dirty deriveds are stored because they might be evaluated and
* marked globally clean by another batch before this batch resumes. Restoring
* only DIRTY deriveds is insufficient: a clean parent would skip their checks.
* Relying on wv_values alone is also insufficient, because a later evaluation
* with newer inputs could incorrectly prevent reevaluation with this batch's inputs.
* @type {Map<Reaction, number>}
*/
#dirty_reactions = new Map();
/**
* A map of branches that still exist, but will be destroyed when this batch
* is committed — we skip over these during `process`.
* The value contains child effects that were dirty/maybe_dirty before being reset,
* so they can be rescheduled if the branch survives.
* @type {Map<Effect, { d: Effect[], m: Effect[] }>}
*/
#skipped_branches = new Map();
/**
* @type {Set<Effect> | null}
*/
#unskipped_branches = null;
/**
* Inverse of #skipped_branches which we need to tell prior batches to unskip them when committing.
* Lazily initialized for performance reasons.
* @type {Set<Effect>}
*/
get unskipped_branches() {
return (this.#unskipped_branches ??= new Set());
}
is_fork = false;
is_eager = false;
#decrement_queued = false;
constructor() {
this.enqueue();
}
/**
* Move this batch behind all other non-fork batches, but before all forks
* (which are always the latest batches), keeping the ids in queue order
*/
enqueue() {
var next = first_batch;
while (next !== null && !next.is_fork) next = next.next;
this.#unlink();
this.prev = next === null ? last_batch : next.prev;
this.next = next;
this.linked = true;
if (this.prev === null) first_batch = this;
else this.prev.next = this;
if (next === null) last_batch = this;
else next.prev = this;
for (this.id = uid++; next !== null; next = next.next) next.id = uid++;
}
#is_deferred() {
if (this.is_fork) return true;
if (this.#blocking_pending === null) return false;
for (let e of this.#blocking_pending.keys()) {
// pending work inside a skipped branch doesn't block, the branch is going to be destroyed
while (e.parent !== null && !this.#skipped_branches.has(e)) {
e = e.parent;
}
if (e.parent === null) return true;
}
return false;
}
/**
* Add an effect to the #skipped_branches map and reset its children
* @param {Effect} effect
*/
skip_effect(effect) {
if (!this.#skipped_branches.has(effect)) {
this.#skipped_branches.set(effect, { d: [], m: [] });
}
this.#unskipped_branches?.delete(effect);
}
/**
* Remove an effect from the #skipped_branches map and reschedule
* any tracked dirty/maybe_dirty child effects
* @param {Effect} effect
*/
unskip_effect(effect) {
for (var batch = /** @type {Batch | null} */ (this); batch !== null; batch = batch.prev) {
// Earlier (non-fork) batches that skip this branch have reset the effects inside it that are
// dirty because of their changes. This batch sees those changes as well, so it runs them, too
var tracked =
batch === this || !batch.is_fork ? batch.#skipped_branches.get(effect) : undefined;
if (tracked) {
for (var e of tracked.d) {
this.schedule(e, DIRTY);
}
for (e of tracked.m) {
this.schedule(e, MAYBE_DIRTY);
}
}
}
this.#skipped_branches.delete(effect);
this.unskipped_branches.add(effect);
}
/**
* Called when this batch selects a branch that so far only exists for forks. Its effects ran
* with the values of the fork that created it, so they need to re-run with the values of this
* batch. If this is a real batch, the branch also becomes part of the real world.
* @param {Effect} branch
*/
reveal(branch) {
if (this.#unskipped_branches?.has(branch)) return; // already revealed by this batch
if (!this.is_fork) branch.f ^= FORK_ONLY_BRANCH;
this.#schedule_branch(branch);
}
/**
* Schedule all effects inside a revealed branch. Effects that ran without dependencies are the
* same in every batch — except async effects, whose results only exist in the batch that ran
* them. Branches inside it that only exist for (other) forks are left alone, the traversal skips
* them anyway.
* TODO this can overfire, maybe there's a way to detect which effects saw values that differ.
* @param {Effect} effect
*/
#schedule_branch(effect) {
if (
(effect.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 &&
effect.fn !== null &&
// TODO maybe we find a way to instead find the (pending) async effect and set the resulting value on this batch
(effect.deps !== null || (effect.f & (REACTION_RAN | ASYNC)) !== REACTION_RAN)
) {
this.schedule(effect, DIRTY);
// The effect's state will reflect this batch, so other forks that ran it need to re-run it.
// Not so for async effects, as their result is stored in the fork and remains valid.
if ((effect.f & ASYNC) === 0) {
for (var batch = first_batch; batch !== null; batch = batch.next) {
if (batch !== this && batch.#stale_effects?.has(effect)) batch.rerun(effect);
}
}
}
for (var e = effect.first; e !== null; e = e.next) {
if ((e.f & FORK_ONLY_BRANCH) === 0) this.#schedule_branch(e);
}
}
/**
* Convert the effects that were scheduled in this batch into the root effects
* that need to be traversed, marking the path to each effect (by clearing the
* `CLEAN` flag on ancestor branches) so that the traversal can find them.
* This happens right before traversal rather than at scheduling time, so that
* the markers left on the (shared) effect tree are created and consumed within
* a single traversal — scheduling into other batches can never observe them
* @returns {Effect[]}
*/
#resolve() {
/** @type {Effect[]} */
var roots = [];
for (const effect of this.#scheduled) {
// skip effects that are destroyed, or that already ran (e.g. because
// they were reached by the traversal that preceded a drain iteration,
// or because they were scheduled twice)
if ((effect.f & DESTROYED) !== 0 || (effect.f & (DIRTY | MAYBE_DIRTY)) === 0) continue;
var e = effect;
var covered = false;
while (e.parent !== null) {
e = e.parent;
var flags = e.f;
if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) {
if ((flags & CLEAN) === 0) {
// the path to the root was already marked, meaning the
// root was already collected — nothing left to do
covered = true;
break;
}
e.f ^= CLEAN;
}
}
if (!covered) {
roots.push(e);
}
}
this.#scheduled = [];
return roots;
}
#process() {
this.started = true;
if (DEV) {
// track all the values that were updated during this flush,
// so that they can be reset afterwards
for (const value of this.current.keys()) {
source_stacks.add(value);
}
}
// We always reschedule previously-deferred effects, not just when
// #is_deferred() is true, because traversing the tree could make
// an if block that contains the last blocking pending effect falsy,
// causing the block to no longer be deferred.
for (const [reaction, status] of this.#dirty_reactions) {
if ((reaction.f & DERIVED) !== 0) {
set_signal_status(reaction, status);
} else {
this.schedule(/** @type {Effect} */ (reaction), status);
}
}
for (const [effect, status] of this.#unprocessed) {
this.#add(effect, status);
}
this.#unprocessed.clear();
this.apply();
/** @type {Effect[]} */
var effects = (collected_effects = []);
/** @type {Effect[]} */
var render_effects = [];
/**
* @type {Effect[]}
* @deprecated when we get rid of legacy mode and stores, we can get rid of this
*/
var updates = (legacy_updates = []);
// Effects can be scheduled during traversal (e.g. because a parent each/await/etc
// block updated an internal source, or because an effect invalidated itself)
// hence we loop until there are no more scheduled effects.
while (this.#scheduled.length > 0) {
if (flush_count++ > 1000) {
this.#unlink();
infinite_loop_guard(); // TODO try to reset_all() here?
}
for (const root of this.#resolve()) {
try {
this.#traverse(root, effects, render_effects);
} catch (e) {
reset_all(root);
// If there's no async work left, this branch is now dead and needs
// to be discarded to not become a zombie that is never cleaned up.
// See https://github.com/sveltejs/svelte/issues/18221#issuecomment-4497918414
// for a (non-minimal) reproduction that demonstrates a case where this is necessary
// to not get follow-up false-positives via "batch has scheduled roots" invariant errors.
if (!this.#is_deferred()) this.discard();
throw e;
}
}
}
// any writes should take effect in a subsequent batch
current_batch = null;
if (updates.length > 0) {
var batch = Batch.ensure();
for (const e of updates) {
// these were marked during traversal already (see `mark_reactions`), unless they ran since
if ((e.f & CLEAN) === 0) batch.schedule(e, e.f & (DIRTY | MAYBE_DIRTY));
}
}
collected_effects = null;
legacy_updates = null;
// if the batch has outstanding pending work, stash effects and bail
if (this.#is_deferred()) {
this.#defer_effects(render_effects);
this.#defer_effects(effects);
for (const [e, t] of this.#skipped_branches) {
reset_branch(e, t);
}
if (updates.length > 0) {
/** @type {Batch} */ (/** @type {unknown} */ (current_batch)).#process();
}
return;
}
const earlier_batch = this.#find_earlier_batch();
if (earlier_batch) {
// If this batch collected deferred effects during traversal, they still need
// to run after being merged into the earlier batch.
this.#defer_effects(render_effects);
this.#defer_effects(effects);
earlier_batch.#merge(this);
return;
}
// clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches.
this.#dirty_reactions.clear();
this.apply(true);
// append/remove branches
for (const fn of this.#commit_callbacks) fn(this);
this.#commit_callbacks.clear();
previous_batch = this;
flush_queued_effects(render_effects);
flush_queued_effects(effects);
previous_batch = null;
this.#deferred?.resolve();
var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch));
if (this.#pending === 0) {
this.#unlink();
}
if (next_batch !== null) {
old_values.clear();
next_batch.#process();
}
}
/**
* Traverse the effect tree, executing effects or stashing
* them for later execution as appropriate
* @param {Effect} root
* @param {Effect[]} effects
* @param {Effect[]} render_effects
*/
#traverse(root, effects, render_effects) {
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_branches.has(effect);
// Branches that only exist for forks are skipped unless this batch revealed them
if ((flags & FORK_ONLY_BRANCH) !== 0 && !this.#unskipped_branches?.has(effect)) {
skip = true;
this.skip_effect(effect);
reset_branch(
effect,
/** @type {{d: Effect[], m: Effect[]}} */ (this.#skipped_branches.get(effect))
);
}
if (!skip && effect.fn !== null) {
if (is_branch) {
effect.f ^= CLEAN;
} else if ((flags & EFFECT) !== 0) {
effects.push(effect);
} else if (async_mode_flag && (flags & (RENDER_EFFECT | MANAGED_EFFECT)) !== 0) {
render_effects.push(effect);
} else {
this.seen_effects?.add(effect);
if (is_dirty(effect)) {
update_effect(effect);
}
}
var child = effect.first;
if (child !== null) {
effect = child;
continue;
}
}
while (effect !== null) {
var next = effect.next;
if (next !== null) {
effect = next;
break;
}
effect = effect.parent;
}
}
}
#find_earlier_batch() {
if (this.is_eager) return null;
for (var batch = this.prev; batch !== null; batch = batch.prev) {
// no need to check for forks, they are always the latest batches
if (this.dependent.has(batch)) {
return batch;
}
}
return null;
}
/**
* Mark all reactive trees leading to block/async effects that (indirectly) depend on `value`
* @param {Value} value
* @param {number} status
* @param {boolean} not_yet - whether to mark effects that have not yet run, as opposed to those that have already run
*/
mark(value, status, not_yet = false) {
var reactions = value.reactions;
if (reactions === null) return false;
let marked = false;
for (const reaction of reactions) {
var flags = reaction.f;
if ((flags & DERIVED) !== 0) {
var derived = /** @type {Derived} */ (reaction);
// deriveds are traversed regardless of their status and only marked
// if something downstream was marked, so that we don't dirty deriveds needlessly
if (this.mark(derived, MAYBE_DIRTY, not_yet)) {
set_signal_status(derived, status);
marked = true;
}
} else {
var effect = /** @type {Effect} */ (reaction);
if ((flags & EAGER_EFFECT) !== 0) {
// Like in `mark_reactions`, these aren't scheduled but run right away (`flush_eager_effects`).
// For revalidations, they already did as part of the real world's write that caused it.
if (not_yet) eager_effects.add(effect);
} else if (
not_yet
? !this.seen_effects?.has(effect) && !this.#dirty_reactions.has(effect)
: (flags & (ASYNC | BLOCK_EFFECT)) === 0 || this.seen_effects?.has(effect)
) {
this.schedule(effect, status);
marked = true;
}
}
}
return marked;
}
/**
* @param {Batch} batch
*/
#merge(batch) {
for (const [source, value] of batch.current) {
if (!this.previous.has(source) && batch.previous.has(source)) {
this.previous.set(
source,
/** @type {{ v: any, wv: number }} */ (batch.previous.get(source))
);
}
this.current.set(source, value);
}
for (const [effect, deferred] of batch.async_deriveds) {
const d = this.async_deriveds.get(effect);
if (d) deferred.promise.then(d.resolve).catch(d.reject);
}
for (const b of batch.dependent) {
if (b !== this) this.dependent.add(b);
}
for (let b = first_batch; b !== null; b = b.next) {
if (b.dependent.has(batch)) {
b.dependent.add(this);
}
}
this.#pending += batch.#pending;
if (batch.#blocking_pending !== null) {
const blocking_pending = (this.#blocking_pending ??= new Map());
for (const [effect, count] of batch.#blocking_pending) {
blocking_pending.set(effect, (blocking_pending.get(effect) ?? 0) + count);
}
}
for (const c of batch.#commit_callbacks) {
this.oncommit(() => c(batch));
}
for (const c of batch.#discard_callbacks) {
this.ondiscard(() => c(batch));
}
for (const [s, v] of batch.#skipped_branches) {
this.#skipped_branches.set(s, v);
this.#unskipped_branches?.delete(s);
}
for (const s of batch.unskipped_branches) {
const v = this.#skipped_branches.get(s);
if (v) {
v.d = v.d.filter((e) => !batch.async_deriveds.has(e));
v.m = v.m.filter((e) => !batch.async_deriveds.has(e));
}
this.unskip_effect(s);
}
// Clear them or else those that are still pending might get rejected on discard (after merged-into batch is done).
// This can happen when batch Y merged into X and Y has a pending boundary and therefore still-pending async deriveds inside.
batch.async_deriveds.clear();
this.transfer_reactions(batch.#dirty_reactions);
this.oncommit(() => batch.discard());
batch.#unlink();
batch.merged_into = this;
current_batch = this;
this.#process();
}
/**
* @param {Effect[]} effects
*/
#defer_effects(effects) {
for (var i = 0; i < effects.length; i += 1) {
defer_effect(effects[i], this.#dirty_reactions);
}
}
/**
* Associate a change to a given source with the current
* batch, noting its previous and current values
* @param {Value} source
* @param {any} value
* @param {boolean} [is_derived]
* @param {boolean} [no_bump] True if the value is seen as equal, but this batch doesn't contain the value yet
*/
capture(source, value, is_derived = false, no_bump = false) {
// Fast path for performance: When render/pre/user effects are flushed and this is the sole batch,
// we don't need to capture the value
if (
is_derived &&
current_batch === null &&
previous_batch === this &&
this.linked &&
!this.is_fork &&
this.prev === null &&
this.next === null &&
this.#pending === 0
) {
source.v = value;
source.wv = increment_write_version();
return;
}
// Separate method for further optimization; e.g. v8 does only need to invoke
// CreateFunctionContext in this internal method due to capturing `this` in a closure.
this.#capture(source, value, is_derived, no_bump);
}
/**
* Keep callbacks that capture `this` out of the fast path above, so that taking it
* doesn't require allocating a function context.
* @param {Value} source
* @param {any} value
* @param {boolean} is_derived
* @param {boolean} no_bump
*/
#capture(source, value, is_derived, no_bump) {
if (source.v !== UNINITIALIZED && !this.previous.has(source)) {
this.previous.set(source, { v: source.v, wv: source.wv });
}
// no_bump true means we see the same value as the real world but we still want to
// record it in our batch in case it becomes important later for batch_values etc
// calculation - we don't want to actually bump the wv etc.
const wv = no_bump ? source.wv : increment_write_version();
// Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get`
if ((source.f & ERROR_VALUE) === 0) {
this.current.set(source, { v: value, wv, is_derived });
batch_values?.set(source, value);
wv_values?.set(source, wv);
}
// A derived computed from inputs that differ from the real values must stay batch-local.
// This also happens when committing a later batch hides an earlier batch's pending writes.
let is_latest_value =
!this.is_fork &&
!no_bump &&
!(is_derived && read_batch_local_value(/** @type {Derived} */ (source)));
// A later batch may also own a newer value of the source or one of a derived's dependencies.
// The check above isn't sufficient here: a later batch's write is visible through `batch_values`,
// so comparing what was read against the real value could not attribute the value to the right
// batch, see `async-dont-rebase-new-batch-4`. We only need to look one level deep: `is_dirty`
// evaluates the top-most deriveds first, so a dependency derived that was itself not the latest
// value was not written to the real world, which the check above already catches.
for (let batch = this.next; batch !== null && is_latest_value; batch = batch.next) {
if (batch.is_fork) continue;
if (
batch.current.has(source) ||
((source.f & DERIVED) !== 0 &&
/** @type {Derived} */ (source).deps?.some((d) =>
/** @type {Batch} */ (batch).current.has(d)
))
) {
is_latest_value = false;
}
}
if (is_latest_value) {
source.v = value;
source.wv = wv;
}
for (let batch = first_batch; batch !== null && !this.is_eager; batch = batch.next) {
if (batch.id < this.id && batch.current.has(source)) {
this.dependent.add(batch);
}
if (batch.is_fork && is_latest_value) {
batch.overtake(source, is_derived, value);
}
}
}
/**
* Tell this fork that the real world has written `value` to `source`. This overtakes the
* fork's own write to it, unless that is a value the fork computed itself (a derived or
* the result of an async effect the fork ran). The fork is revalidated with the new value,
* or discarded if it has nothing left to commit (only deriveds, and values that equal the
* real ones, e.g. ones it merely holds, see `internal_set`).
* @param {Value} source
* @param {boolean} is_derived
* @param {any} value
*/
overtake(source, is_derived, value) {
const current = this.current.get(source);
this.current.delete(source);
if (
[...this.current].every(
([written, content]) => content.is_derived || written.equals(content.v)
)
) {
// The real world has overtaken every write of this fork that would change anything, so it
// is obsolete. Discard it right away (its speculative branches must not be adopted by anyone),
// and empty `current` so that `commit()` can tell this apart from a user-initiated discard
this.current.clear();
this.discard();
} else if (current === undefined || current.v !== value) {
if (
is_derived ||
// If the fork ran an async effect, its pending/resolved result belongs to the
// fork. Revalidate it when its inputs change, not when another batch resolves
// the same expression with a different view of those inputs.
((source.f & ASYNC) !== 0 &&
this.#stale_effects?.has(/** @type {Effect} */ (/** @type {Source} */ (source).e)))
) {
if (current !== undefined) this.current.set(source, current);
} else {
queue_micro_task(() => {
if (this.linked && this.mark(source, DIRTY)) this.flush();
});
}
}
}
/**
* Re-run `reaction` in this batch, because it ran with values that differ from this batch's view.
* If this batch is gone by then, re-run it in the real world instead.
* @param {Reaction} reaction
*/
rerun(reaction) {
// a reaction can be stale in several ways at once — only schedule the re-run once
if (this.stale_readers.has(reaction)) return;
this.stale_readers.add(reaction);
queue_micro_task(() => {
this.stale_readers.delete(reaction);
if (!this.activate().linked) this.deactivate();
var batch = Batch.ensure();
invalidate(reaction);
batch.flush();
});
}
/**
* Activate batch - could be merged into another batch in the meantime,
* in which case that other batch becomes the active batch.
* @returns {Batch}
*/
activate() {
return (current_batch = this.merged_into?.activate() ?? this);
}
deactivate() {
current_batch = null;
batch_values = wv_values = held_sources = stale_sources = null;
}
flush() {
try {
if (DEV) {
source_stacks.clear();
}
is_processing = true;
current_batch = this;
this.#process();
} finally {
flush_count = 0;
last_scheduled_effect = null;
collected_effects = null;
legacy_updates = null;
is_processing = false;
this.deactivate();
old_values.clear();
if (DEV) {
for (const source of source_stacks) {
source.updated = null;
}
}
}
}
discard() {
for (const fn of this.#discard_callbacks) fn(this);
this.#discard_callbacks.clear();
for (const deferred of this.async_deriveds.values()) {
deferred.reject(OBSOLETE);
}
this.#unlink();
this.#deferred?.resolve();
}
/**
* @param {boolean} blocking
* @param {Effect} effect
*/
increment(blocking, effect) {
this.#pending += 1;
if (blocking) {
const blocking_pending = (this.#blocking_pending ??= new Map());
let blocking_pending_count = blocking_pending.get(effect) ?? 0;
blocking_pending.set(effect, blocking_pending_count + 1);
}
}
/**
* @param {boolean} blocking
* @param {Effect} effect
* @returns {void}
*/
decrement(blocking, effect) {
if (this.merged_into) return this.merged_into.decrement(blocking, effect);
this.#pending -= 1;
if (blocking) {
const blocking_pending = (this.#blocking_pending ??= new Map());
let blocking_pending_count = blocking_pending.get(effect) ?? 0;
if (blocking_pending_count === 1) {
blocking_pending.delete(effect);
} else {
blocking_pending.set(effect, blocking_pending_count - 1);
}
}
if (this.#decrement_queued) return;
this.#decrement_queued = true;
queue_micro_task(() => {
this.#decrement_queued = false;
if (this.linked) {
this.flush();
}
});
}
/**
* @param {Map<Reaction, number>} dirty_reactions
* @returns {void}
*/
transfer_reactions(dirty_reactions) {
if (this.merged_into) {
return this.merged_into.transfer_reactions(dirty_reactions);
}
for (const [reaction, status] of dirty_reactions) {
this.add_dirty_reaction(reaction, status);
}
dirty_reactions.clear();
}
/**
* @param {Reaction} reaction
* @param {number} status
*/
add_dirty_reaction(reaction, status) {
if (status === DIRTY || this.#dirty_reactions.get(reaction) !== DIRTY) {
this.#dirty_reactions.set(reaction, status);
}
}
/** @param {Reaction} reaction */
remove_dirty_reaction(reaction) {
// Check is done for perf reasons
if (this.#dirty_reactions.size !== 0) this.#dirty_reactions.delete(reaction);
}
/** @param {(batch: Batch) => void} fn */
oncommit(fn) {
this.#commit_callbacks.add(fn);
}
/** @param {(batch: Batch) => void} fn */
ondiscard(fn) {
this.#discard_callbacks.add(fn);
}
settled() {
return (this.#deferred ??= deferred()).promise;
}
static ensure() {
if (current_batch === null) {
const batch = (current_batch = new Batch());
if (!is_processing) {
queue_micro_task(() => {
if (!batch.started) {
batch.flush();
}
});
}
}
return current_batch;
}
/**
* - `include_earlier` false: batch_values etc should see the latest values up until itself,
* i.e. including latest value of all earlier batches; previous values only of later batches.
* - `include_earlier` true: the "world view" of batch_values is current values are only of this batch,
* previous values of all other batches.
* @param {boolean} include_earlier
*/
apply(include_earlier = false) {
if (!async_mode_flag || (!this.is_fork && this.prev === null && this.next === null)) {
batch_values = wv_values = held_sources = stale_sources = null;
return;
}
// if there are multiple batches, we are 'time travelling' —
// we need to override values with the ones in this batch...
batch_values = new Map();
wv_values = new Map();
held_sources = new Map();
stale_sources = new Map();
for (const [source, current] of this.current) {
batch_values.set(source, current.v);
wv_values.set(source, current.wv);
}
for (const [effect, wv] of this.#stale_effects ?? []) {
wv_values.set(effect, wv);
}
for (let batch = first_batch; batch !== null; batch = batch.next) {
if (batch === this) continue;
if (batch.id < this.id) {
for (const source of batch.current.keys()) {
held_sources.set(source, batch);
}
}
if (batch.is_fork) continue;
if (batch.id > this.id || this.is_eager) {
for (const source of batch.current.keys()) {
if (!this.current.has(source)) stale_sources.set(source, batch);
}
}
if (batch.id > this.id || include_earlier || this.is_eager) {
for (const [source, previous] of batch.previous) {
if (!batch_values.has(source)) {
batch_values.set(source, previous.v);
wv_values.set(source, previous.wv);
}
}
}
}
}
/**
* Schedule `effect` to run in this batch. Outside of a flush (and of effects running
* synchronously, e.g. during mount), the status is kept to this batch until it is processed, so that other
* batches don't run the effect (and mark it clean) before that, and so that other batches scheduling it don't
* skip it as already dirty. During a flush it is applied right away, so that the ongoing flush can run the effect
* @param {Effect} effect
* @param {number} status `DIRTY` or `MAYBE_DIRTY`
*/
schedule(effect, status) {
last_scheduled_effect = effect;
if (
is_processing ||
active_reaction !== null ||
// fast path to avoid map lookups when there's only one batch (no danger of other batches stealing this batch's effects)
(!this.is_fork && !(/** @type {Batch} */ (first_batch).next))
) {
this.#add(effect, status);
} else if (status === DIRTY || this.#unprocessed.get(effect) !== DIRTY) {
this.#unprocessed.set(effect, status);
}
}
/**
* Apply the status to `effect` and add it to the effects that this batch is going to process
* @param {Effect} effect
* @param {number} status
*/
#add(effect, status) {
// don't set a DIRTY effect to MAYBE_DIRTY
if ((effect.f & DIRTY) === 0) {
set_signal_status(effect, status);
}
// defer render effects inside a pending boundary
// TODO the `REACTION_RAN` check is only necessary because of legacy `$:` effects AFAICT — we can remove later
if (
effect.b?.is_pending &&
(effect.f & (EFFECT | RENDER_EFFECT | MANAGED_EFFECT)) !== 0 &&
(effect.f & REACTION_RAN) === 0
) {
effect.b.defer_effect(effect);
} else {
this.#scheduled.push(effect);
}
}
#unlink() {
// #merge calls #unlink, discard later on does it again - prevent
// running it multiple times to not corrupt the linked list
if (!this.linked) return;
var prev = this.prev;
var next = this.next;
if (prev === null) {
first_batch = next;
} else {
prev.next = next;
}
if (next === null) {
last_batch = prev;
} else {
next.prev = prev;
}
this.linked = false;
}
}
// TODO Svelte@6 think about removing the callback argument.
/**
* 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) {
var was_flushing_sync = is_flushing_sync;
var prev_previous_batch = previous_batch;
previous_batch = null;
is_flushing_sync = true;
try {
var result;
if (fn) {
flushSync(); // flush anything pending through the while loop below
result = fn();
}
while (true) {
flush_tasks();
if (current_batch === null) {
return /** @type {T} */ (result);
}
current_batch.flush();
}
} finally {
is_flushing_sync = was_flushing_sync;
previous_batch = prev_previous_batch;
}
}
function infinite_loop_guard() {
if (DEV) {
var updates = new Map();
for (const source of /** @type {Batch} */ (current_batch).current.keys()) {
for (const [stack, update] of source.updated ?? []) {
var entry = updates.get(stack);
if (!entry) {
entry = { error: update.error, count: 0 };
updates.set(stack, entry);
}
entry.count += update.count;
}
}
for (const update of updates.values()) {
if (update.error) {
// eslint-disable-next-line no-console
console.error(update.error);
}
}
}
try {
e.effect_update_depth_exceeded();
} catch (error) {
if (DEV) {
// stack contains no useful information, replace it
define_property(error, 'stack', { value: '' });
}
// Best effort: invoke the boundary nearest the most recent
// effect and hope that it's relevant to the infinite loop
invoke_error_boundary(error, last_scheduled_effect);
}
}
/** @type {Set<Effect> | null} */
export let eager_block_effects = null;
/**
* @param {Array<Effect>} effects
* @returns {void}
*/
function flush_queued_effects(effects) {
var length = effects.length;
if (length === 0) return;
var i = 0;
while (i < length) {
var effect = effects[i++];
if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) {
eager_block_effects = new Set();
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 === null &&
effect.teardown === null &&
effect.ac === null
) {
// remove this effect from the graph
unlink_effect(effect);
}
// If update_effect() has a flushSync() in it, we may have flushed another flush_queued_effects(),
// which already handled this logic and did set eager_block_effects to null.
if (eager_block_effects?.size > 0) {
old_values.clear();
for (const e of eager_block_effects) {
// Skip eager effects that have already been unmounted
if ((e.f & (DESTROYED | INERT)) !== 0) continue;
// Run effects in order from ancestor to descendant, else we could run into nullpointers
/** @type {Effect[]} */
const ordered_effects = [e];
let ancestor = e.parent;
while (ancestor !== null) {
if (eager_block_effects.has(ancestor)) {
eager_block_effects.delete(ancestor);
ordered_effects.push(ancestor);
}
ancestor = ancestor.parent;
}
for (let j = ordered_effects.length - 1; j >= 0; j--) {
const e = ordered_effects[j];
// Skip eager effects that have already been unmounted
if ((e.f & (DESTROYED | INERT)) !== 0) continue;
update_effect(e);
}
}
eager_block_effects.clear();
}
}
}
eager_block_effects = null;
}
/**
* @param {Effect} effect
* @param {number} status `DIRTY` or `MAYBE_DIRTY`
* @returns {void}
*/
export function schedule_effect(effect, status) {
/** @type {Batch} */ (current_batch).schedule(effect, status);
}
/** @type {Source<number>[]} */
let eager_versions = [];
function eager_flush() {
flushSync(() => {
var batch = Batch.ensure();
batch.is_eager = true;
const eager = eager_versions;
eager_versions = [];
for (const version of eager) {
update(version);
}
});
}
/** @type {Map<Reaction, Source<number>>} */
var version_map = new Map();
/**
* Implementation of `$state.eager(fn())`
* @template T
* @param {() => T} fn
* @returns {T}
*/
export function eager(fn) {
var initial = true;
var value = /** @type {T} */ (undefined);
if (active_reaction === null) {
return fn();
}
let parent = active_reaction;
let version = version_map.get(parent) ?? source(0);
version_map.set(parent, version);
if (DEV) {
version.label ??= '$state.eager version';
}
teardown(() => {
if (parent.f & DESTROYING) version_map.delete(parent);
});
get(version);
eager_effect(() => {
if (initial && !current_batch?.is_fork) {
// the first time this runs, we create an eager effect
// that will run eagerly whenever the expression changes
var previous_batch_values = batch_values;
try {
batch_values = null;
value = fn();
} finally {
batch_values = previous_batch_values;
}
return;
}
// the second time this effect runs, it's to schedule a
// `version` update. since this will recreate the effect,
// we don't need to evaluate the expression here
if (eager_versions.length === 0) {
queue_micro_task(eager_flush);
}
eager_versions.push(version);
});
initial = false;
return value;
}
/**
* Mark all the effects inside a skipped branch CLEAN, so that
* they can be correctly rescheduled later. Tracks dirty and maybe_dirty
* effects so they can be rescheduled if the branch survives.
* @param {Effect} effect
* @param {{ d: Effect[], m: Effect[] }} tracked
*/
function reset_branch(effect, tracked) {
// clean branch = nothing dirty inside, no need to traverse further
if ((effect.f & BRANCH_EFFECT) !== 0 && (effect.f & CLEAN) !== 0) {
return;
}
if ((effect.f & DIRTY) !== 0) {
tracked.d.push(effect);
} else if ((effect.f & MAYBE_DIRTY) !== 0) {
tracked.m.push(effect);
}
set_signal_status(effect, CLEAN);
var e = effect.first;
while (e !== null) {
reset_branch(e, tracked);
e = e.next;
}
}
/**
* Mark an entire effect tree clean following an error
* @param {Effect} effect
*/
function reset_all(effect) {
set_signal_status(effect, CLEAN);
var e = effect.first;
while (e !== null) {
reset_all(e);
e = e.next;
}
}
/**
* Creates a 'fork', in which state changes are evaluated but not applied to the DOM.
* This is useful for speculatively loading data (for example) when you suspect that
* the user is about to take some action.
*
* Frameworks like SvelteKit can use this to preload data when the user touches or
* hovers over a link, making any subsequent navigation feel instantaneous.
*
* The `fn` parameter is a synchronous function that modifies some state. The
* state changes will be reverted after the fork is initialised, then reapplied
* if and when the fork is eventually committed.
*
* When it becomes clear that a fork will _not_ be committed (e.g. because the
* user navigated elsewhere), it must be discarded to avoid leaking memory.
*
* @param {() => void} fn
* @returns {Fork}
* @since 5.42
*/
export function fork(fn) {
if (!async_mode_flag) {
e.experimental_async_required('fork');
}
if (current_batch !== null) {
e.fork_timing();
}
flushSync();
var committed = false;
var batch = Batch.ensure();
batch.is_fork = true;
batch.seen_effects = new Set();
batch_values = new Map();
wv_values = new Map();
var settled = batch.settled();
fn();
flushSync();
return {
commit: async () => {
if (committed) {
await settled;
return;
}
if (batch.current.size === 0) {
// Nothing to commit: either the fork never wrote anything (e.g. it assigned a value
// that was already current), or the real world has since written to every source
// it did write to and the fork was discarded as obsolete (see `overtake`)
committed = true;
batch.discard();
return;
}
if (!batch.linked) {
e.fork_discarded();
}
committed = true;
batch.is_fork = false;
batch.enqueue();
current_batch?.deactivate(); // write as the real world, not within some other batch's view
// Committing means that the batch writes its values to the real world, just like a real
// batch would (which also tells the other forks about it). Everything still in `current` at
// this point is the latest value: sources that the real world has written to in the meantime
// were removed from the fork via `overtake`, while async results are kept up to date by
// revalidating their producers when inputs change. Capturing them anew gives them fresh write
// versions, because the real world may have run reactions since that would otherwise outrank them.
for (var [source, content] of batch.current) {
// Values that equal the real ones (e.g. ones the fork only holds, see `internal_set`) don't change
// the real world. Capturing them would give them a new write version, which would make every
// reaction that the fork hasn't seen yet look dirty
if (source.equals(content.v)) continue;
batch.capture(source, content.v, content.is_derived);
// dirty those effects the fork did not see yet, e.g. because a later batch created new branches
if (!content.is_derived) batch.mark(source, MAYBE_DIRTY, true);
}
// trigger any `$state.eager(...)` expressions with the new state
flush_eager_effects();
// All the block/async effects the fork executed are now guaranteed to be up to date
for (const effect of batch.stale_effects.keys()) {
effect.wv = write_version;
}
batch.stale_effects.clear();
// Promote fork-only branches to the real world
for (const e of batch.unskipped_branches) {
if (e.f & FORK_ONLY_BRANCH) {
e.f ^= FORK_ONLY_BRANCH;
}
}
batch.flush();
await settled;
},
discard: () => {
if (!committed && batch.linked) {
batch.discard();
}
}
};
}
/**
* Forcibly remove all current batches, to prevent cross-talk between tests
*/
export function clear() {
first_batch = last_batch = null;
}