|
|
|
|
@ -1,22 +1,21 @@
|
|
|
|
|
/** @import { Fork } from 'svelte' */
|
|
|
|
|
/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */
|
|
|
|
|
/** @import { Derived, Effect, Reaction, Source, Value, ValueSnapshot } 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,
|
|
|
|
|
STATE_EAGER_EFFECT,
|
|
|
|
|
DESTROYING
|
|
|
|
|
} from '#client/constants';
|
|
|
|
|
import { async_mode_flag } from '../../flags/index.js';
|
|
|
|
|
@ -27,20 +26,26 @@ import {
|
|
|
|
|
get,
|
|
|
|
|
increment_write_version,
|
|
|
|
|
is_dirty,
|
|
|
|
|
update_effect
|
|
|
|
|
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 { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js';
|
|
|
|
|
import {
|
|
|
|
|
flush_eager_effects,
|
|
|
|
|
mark_reactions,
|
|
|
|
|
old_values,
|
|
|
|
|
set_eager_effects,
|
|
|
|
|
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 { legacy_is_updating_store } from './store.js';
|
|
|
|
|
import { invariant } from '../../shared/dev.js';
|
|
|
|
|
import { log_effect_tree } from '../dev/debug.js';
|
|
|
|
|
|
|
|
|
|
/** @type {Batch | null} */
|
|
|
|
|
let first_batch = null;
|
|
|
|
|
@ -52,18 +57,19 @@ let last_batch = null;
|
|
|
|
|
export let current_batch = null;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* This is needed to avoid overwriting inputs
|
|
|
|
|
* The batch that is currently applied. May not be the same as `current_batch`, since we
|
|
|
|
|
* null that out when flushing effects in case they set state, resulting in a new
|
|
|
|
|
* batch being created. Effects always run inside an active_batch.
|
|
|
|
|
* TODO most occurrences of `current_batch` should be this
|
|
|
|
|
* @type {Batch | null}
|
|
|
|
|
*/
|
|
|
|
|
export let previous_batch = null;
|
|
|
|
|
**/
|
|
|
|
|
export let active_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}
|
|
|
|
|
* This is needed to avoid overwriting inputs
|
|
|
|
|
* @type {Batch | null}
|
|
|
|
|
*/
|
|
|
|
|
export let batch_values = null;
|
|
|
|
|
export let previous_batch = null;
|
|
|
|
|
|
|
|
|
|
/** @type {Effect | null} */
|
|
|
|
|
let last_scheduled_effect = null;
|
|
|
|
|
@ -116,17 +122,26 @@ export class Batch {
|
|
|
|
|
* The current values of any signals that are updated in this batch.
|
|
|
|
|
* Tuple format: [value, is_derived] (note: 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, [any, boolean]>}
|
|
|
|
|
* @type {Map<Value, ValueSnapshot<unknown>>}
|
|
|
|
|
*/
|
|
|
|
|
current = new Map();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The values 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, any>}
|
|
|
|
|
* @type {Map<Value, ValueSnapshot<unknown>>}
|
|
|
|
|
*/
|
|
|
|
|
previous = new Map();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The combination of this batch's `current` and other batches' `previous` values,
|
|
|
|
|
* 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, ValueSnapshot<unknown>> | null}
|
|
|
|
|
*/
|
|
|
|
|
values = null;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
@ -171,16 +186,13 @@ export class Batch {
|
|
|
|
|
#new_effects = [];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Deferred effects (which run after async work has completed) that are DIRTY
|
|
|
|
|
* Deferred effects (which run after async work has completed) that are dirty
|
|
|
|
|
* @type {Set<Effect>}
|
|
|
|
|
*/
|
|
|
|
|
#dirty_effects = new Set();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Deferred effects that are MAYBE_DIRTY
|
|
|
|
|
* @type {Set<Effect>}
|
|
|
|
|
*/
|
|
|
|
|
#maybe_dirty_effects = new Set();
|
|
|
|
|
/** @type {Map<Reaction, number>} */
|
|
|
|
|
cvs = new Map();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A map of branches that still exist, but will be destroyed when this batch
|
|
|
|
|
@ -260,12 +272,10 @@ export class Batch {
|
|
|
|
|
this.#skipped_branches.delete(effect);
|
|
|
|
|
|
|
|
|
|
for (var e of tracked.d) {
|
|
|
|
|
set_signal_status(e, DIRTY);
|
|
|
|
|
callback(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (e of tracked.m) {
|
|
|
|
|
set_signal_status(e, MAYBE_DIRTY);
|
|
|
|
|
callback(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -273,6 +283,9 @@ export class Batch {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#process() {
|
|
|
|
|
if (this.#committed) return;
|
|
|
|
|
|
|
|
|
|
current_batch = this;
|
|
|
|
|
this.#started = true;
|
|
|
|
|
|
|
|
|
|
if (flush_count++ > 1000) {
|
|
|
|
|
@ -280,6 +293,11 @@ export class Batch {
|
|
|
|
|
infinite_loop_guard();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO we only need to do this for re-runs
|
|
|
|
|
for (const [source, snapshot] of this.current) {
|
|
|
|
|
mark_reactions(this, source, snapshot.wv, null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (DEV) {
|
|
|
|
|
// track all the values that were updated during this flush,
|
|
|
|
|
// so that they can be reset afterwards
|
|
|
|
|
@ -288,18 +306,7 @@ export class Batch {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 e of this.#dirty_effects) {
|
|
|
|
|
this.#maybe_dirty_effects.delete(e);
|
|
|
|
|
set_signal_status(e, DIRTY);
|
|
|
|
|
this.schedule(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const e of this.#maybe_dirty_effects) {
|
|
|
|
|
set_signal_status(e, MAYBE_DIRTY);
|
|
|
|
|
this.schedule(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -377,7 +384,6 @@ export class Batch {
|
|
|
|
|
|
|
|
|
|
// clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches.
|
|
|
|
|
this.#dirty_effects.clear();
|
|
|
|
|
this.#maybe_dirty_effects.clear();
|
|
|
|
|
|
|
|
|
|
// append/remove branches
|
|
|
|
|
for (const fn of this.#commit_callbacks) fn(this);
|
|
|
|
|
@ -418,6 +424,8 @@ export class Batch {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
active_batch = null;
|
|
|
|
|
|
|
|
|
|
if (next_batch !== null) {
|
|
|
|
|
next_batch.#process();
|
|
|
|
|
}
|
|
|
|
|
@ -450,7 +458,6 @@ export class Batch {
|
|
|
|
|
} else if (async_mode_flag && (flags & (RENDER_EFFECT | MANAGED_EFFECT)) !== 0) {
|
|
|
|
|
render_effects.push(effect);
|
|
|
|
|
} else if (is_dirty(effect)) {
|
|
|
|
|
if ((flags & BLOCK_EFFECT) !== 0) this.#maybe_dirty_effects.add(effect);
|
|
|
|
|
update_effect(effect);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -481,8 +488,8 @@ export class Batch {
|
|
|
|
|
while (batch !== null) {
|
|
|
|
|
if (!batch.is_fork) {
|
|
|
|
|
// if the batches are connected, break
|
|
|
|
|
for (const [value, [, is_derived]] of this.current) {
|
|
|
|
|
if (batch.current.has(value) && !is_derived) {
|
|
|
|
|
for (const value of this.current.keys()) {
|
|
|
|
|
if (batch.current.has(value)) {
|
|
|
|
|
return batch;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -498,21 +505,41 @@ export class Batch {
|
|
|
|
|
* @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, batch.previous.get(source));
|
|
|
|
|
for (const [source, snapshot] of batch.current) {
|
|
|
|
|
var previous = batch.previous.get(source);
|
|
|
|
|
|
|
|
|
|
if (previous && !this.previous.has(source)) {
|
|
|
|
|
this.previous.set(source, previous);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.current.set(source, value);
|
|
|
|
|
this.current.set(source, snapshot);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
var cv = batch.cvs.get(effect);
|
|
|
|
|
if (cv !== undefined) this.cvs.set(effect, cv);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mark is not guaranteed not touch these, so we transfer them
|
|
|
|
|
this.transfer_effects(batch.#dirty_effects, batch.#maybe_dirty_effects);
|
|
|
|
|
for (const fn of batch.#commit_callbacks) {
|
|
|
|
|
this.#commit_callbacks.add(() => fn(batch));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const fn of batch.#discard_callbacks) {
|
|
|
|
|
this.#discard_callbacks.add(() => fn(batch));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO this doesn't feel quite right, but it gets the tests to pass
|
|
|
|
|
this.oncommit(() => {
|
|
|
|
|
for (const fn of batch.#discard_callbacks) fn(batch);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.settled().then(() => batch.#deferred?.resolve());
|
|
|
|
|
|
|
|
|
|
// Mark is not guaranteed to not touch these, so we transfer them
|
|
|
|
|
this.transfer_effects(batch.#dirty_effects);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* mark all effects that depend on `batch.current`, except the
|
|
|
|
|
@ -535,8 +562,10 @@ export class Batch {
|
|
|
|
|
var effect = /** @type {Effect} */ (reaction);
|
|
|
|
|
|
|
|
|
|
if (flags & (ASYNC | BLOCK_EFFECT) && !this.async_deriveds.has(effect)) {
|
|
|
|
|
this.#maybe_dirty_effects.delete(effect);
|
|
|
|
|
set_signal_status(effect, DIRTY);
|
|
|
|
|
if ((effect.f & CLEAN) !== 0) {
|
|
|
|
|
effect.f ^= CLEAN;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.schedule(effect);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -547,7 +576,6 @@ export class Batch {
|
|
|
|
|
mark(source);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.oncommit(() => batch.discard());
|
|
|
|
|
batch.#unlink();
|
|
|
|
|
|
|
|
|
|
current_batch = this;
|
|
|
|
|
@ -559,30 +587,35 @@ export class Batch {
|
|
|
|
|
*/
|
|
|
|
|
#defer_effects(effects) {
|
|
|
|
|
for (var i = 0; i < effects.length; i += 1) {
|
|
|
|
|
defer_effect(effects[i], this.#dirty_effects, this.#maybe_dirty_effects);
|
|
|
|
|
defer_effect(effects[i], this.#dirty_effects);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 {Value} value
|
|
|
|
|
* @param {any} v
|
|
|
|
|
* @param {number} wv
|
|
|
|
|
*/
|
|
|
|
|
capture(source, value, is_derived = false) {
|
|
|
|
|
if (source.v !== UNINITIALIZED && !this.previous.has(source)) {
|
|
|
|
|
this.previous.set(source, source.v);
|
|
|
|
|
capture(value, v, wv) {
|
|
|
|
|
if (value.v !== UNINITIALIZED && !this.previous.has(value)) {
|
|
|
|
|
this.previous.set(value, { v: value.v, wv: value.wv });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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, [value, is_derived]);
|
|
|
|
|
batch_values?.set(source, value);
|
|
|
|
|
// Don't save errors or they won't be thrown in `runtime.js#get`
|
|
|
|
|
if ((value.f & ERROR_VALUE) === 0) {
|
|
|
|
|
var snapshot = { v, wv };
|
|
|
|
|
|
|
|
|
|
this.current.delete(value); // order must be preserved
|
|
|
|
|
|
|
|
|
|
this.current.set(value, snapshot);
|
|
|
|
|
active_batch?.values?.set(value, snapshot);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!this.is_fork) {
|
|
|
|
|
source.v = value;
|
|
|
|
|
value.v = v;
|
|
|
|
|
value.wv = wv;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -592,7 +625,7 @@ export class Batch {
|
|
|
|
|
|
|
|
|
|
deactivate() {
|
|
|
|
|
current_batch = null;
|
|
|
|
|
batch_values = null;
|
|
|
|
|
active_batch = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
flush() {
|
|
|
|
|
@ -602,8 +635,6 @@ export class Batch {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
is_processing = true;
|
|
|
|
|
current_batch = this;
|
|
|
|
|
|
|
|
|
|
this.#process();
|
|
|
|
|
} finally {
|
|
|
|
|
flush_count = 0;
|
|
|
|
|
@ -613,7 +644,7 @@ export class Batch {
|
|
|
|
|
is_processing = false;
|
|
|
|
|
|
|
|
|
|
current_batch = null;
|
|
|
|
|
batch_values = null;
|
|
|
|
|
active_batch = null;
|
|
|
|
|
|
|
|
|
|
old_values.clear();
|
|
|
|
|
|
|
|
|
|
@ -640,7 +671,13 @@ export class Batch {
|
|
|
|
|
this.#new_effects.push(effect);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#committed = false;
|
|
|
|
|
|
|
|
|
|
#commit() {
|
|
|
|
|
// TODO seems like a bug that we can end up here more than once
|
|
|
|
|
if (this.#committed) return;
|
|
|
|
|
this.#committed = true;
|
|
|
|
|
|
|
|
|
|
// If there are other pending batches, they now need to be 'rebased' —
|
|
|
|
|
// in other words, we re-run block/async effects with the newly
|
|
|
|
|
// committed state, unless the batch in question has a more
|
|
|
|
|
@ -651,13 +688,13 @@ export class Batch {
|
|
|
|
|
/** @type {Source[]} */
|
|
|
|
|
var sources = [];
|
|
|
|
|
|
|
|
|
|
for (const [source, [value, is_derived]] of this.current) {
|
|
|
|
|
if (batch.current.has(source)) {
|
|
|
|
|
var batch_value = /** @type {[any, boolean]} */ (batch.current.get(source))[0]; // faster than destructuring
|
|
|
|
|
for (const [source, snapshot] of this.current) {
|
|
|
|
|
var batch_snapshot = batch.current.get(source);
|
|
|
|
|
|
|
|
|
|
if (is_earlier && value !== batch_value) {
|
|
|
|
|
if (batch_snapshot) {
|
|
|
|
|
if (is_earlier && snapshot.v !== batch_snapshot.v) {
|
|
|
|
|
// bring the value up to date
|
|
|
|
|
batch.current.set(source, [value, is_derived]);
|
|
|
|
|
batch.current.set(source, snapshot);
|
|
|
|
|
} else {
|
|
|
|
|
// same value or later batch has more recent value,
|
|
|
|
|
// no need to re-run these effects
|
|
|
|
|
@ -680,9 +717,10 @@ export class Batch {
|
|
|
|
|
if (!batch.#started) continue;
|
|
|
|
|
|
|
|
|
|
// Re-run async/block effects that depend on distinct values changed in both batches (ignoring deriveds)
|
|
|
|
|
var others = [...batch.current.keys()].filter(
|
|
|
|
|
(s) => !(/** @type {[any, boolean]} */ (batch.current.get(s))[1]) && !this.current.has(s)
|
|
|
|
|
);
|
|
|
|
|
var others = Array.from(batch.current.keys()).filter((value) => {
|
|
|
|
|
if ((value.f & DERIVED) !== 0) return false;
|
|
|
|
|
return !this.current.has(value);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (others.length === 0) {
|
|
|
|
|
if (is_earlier) {
|
|
|
|
|
@ -718,32 +756,24 @@ export class Batch {
|
|
|
|
|
var checked = new Map();
|
|
|
|
|
|
|
|
|
|
for (var source of sources) {
|
|
|
|
|
mark_effects(source, others, marked, checked);
|
|
|
|
|
mark_effects(batch, source, others, marked, checked);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
checked = new Map();
|
|
|
|
|
var current_unequal = [...batch.current]
|
|
|
|
|
.filter(([c, v1]) => {
|
|
|
|
|
const v2 = this.current.get(c);
|
|
|
|
|
if (!v2) return true;
|
|
|
|
|
// Either their values are different or one is a derived but not the other
|
|
|
|
|
return v2[0] !== v1[0] || v2[1] !== v1[1];
|
|
|
|
|
return !v2 || v2.v !== v1.v;
|
|
|
|
|
})
|
|
|
|
|
.map(([c]) => c);
|
|
|
|
|
|
|
|
|
|
if (current_unequal.length > 0) {
|
|
|
|
|
for (const effect of this.#new_effects) {
|
|
|
|
|
if (
|
|
|
|
|
(effect.f & (DESTROYED | INERT | EAGER_EFFECT)) === 0 &&
|
|
|
|
|
depends_on(effect, current_unequal, checked)
|
|
|
|
|
) {
|
|
|
|
|
if ((effect.f & (ASYNC | BLOCK_EFFECT)) !== 0) {
|
|
|
|
|
set_signal_status(effect, DIRTY);
|
|
|
|
|
batch.schedule(effect);
|
|
|
|
|
} else {
|
|
|
|
|
batch.#dirty_effects.add(effect);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (const effect of this.#new_effects) {
|
|
|
|
|
if (
|
|
|
|
|
(effect.f & (DESTROYED | INERT | EAGER_EFFECT)) === 0 &&
|
|
|
|
|
depends_on(effect, current_unequal, checked)
|
|
|
|
|
) {
|
|
|
|
|
batch.schedule(effect);
|
|
|
|
|
batch.cvs.set(effect, -1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -808,19 +838,13 @@ export class Batch {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Set<Effect>} dirty_effects
|
|
|
|
|
* @param {Set<Effect>} maybe_dirty_effects
|
|
|
|
|
*/
|
|
|
|
|
transfer_effects(dirty_effects, maybe_dirty_effects) {
|
|
|
|
|
transfer_effects(dirty_effects) {
|
|
|
|
|
for (const e of dirty_effects) {
|
|
|
|
|
this.#dirty_effects.add(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const e of maybe_dirty_effects) {
|
|
|
|
|
this.#maybe_dirty_effects.add(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dirty_effects.clear();
|
|
|
|
|
maybe_dirty_effects.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param {(batch: Batch) => void} fn */
|
|
|
|
|
@ -854,17 +878,17 @@ export class Batch {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
apply() {
|
|
|
|
|
if (!async_mode_flag || (!this.is_fork && this.#prev === null && this.#next === null)) {
|
|
|
|
|
batch_values = null;
|
|
|
|
|
if (!async_mode_flag) {
|
|
|
|
|
// TODO previously we bailed here if there was only one (non-fork) batch... maybe we can reinstate that
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (active_batch === this) return;
|
|
|
|
|
active_batch = this;
|
|
|
|
|
|
|
|
|
|
// if there are multiple batches, we are 'time travelling' —
|
|
|
|
|
// we need to override values with the ones in this batch...
|
|
|
|
|
batch_values = new Map();
|
|
|
|
|
for (const [source, [value]] of this.current) {
|
|
|
|
|
batch_values.set(source, value);
|
|
|
|
|
}
|
|
|
|
|
this.values = new Map(this.current);
|
|
|
|
|
|
|
|
|
|
// ...and undo changes belonging to other batches unless they intersect
|
|
|
|
|
for (let batch = first_batch; batch !== null; batch = batch.#next) {
|
|
|
|
|
@ -875,11 +899,7 @@ export class Batch {
|
|
|
|
|
var intersects = false;
|
|
|
|
|
|
|
|
|
|
if (batch.id < this.id) {
|
|
|
|
|
for (const [source, [, is_derived]] of batch.current) {
|
|
|
|
|
// Derived values don't partake in the intersection mechanism, because a derived could
|
|
|
|
|
// be triggered in one batch already but not the other one yet, causing a false-positive
|
|
|
|
|
if (is_derived) continue;
|
|
|
|
|
|
|
|
|
|
for (const source of batch.current.keys()) {
|
|
|
|
|
if (this.current.has(source)) {
|
|
|
|
|
intersects = true;
|
|
|
|
|
break;
|
|
|
|
|
@ -887,15 +907,10 @@ export class Batch {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Since the latter batch merges into the earlier (if it resolves before the earlier one),
|
|
|
|
|
// we treat the earlier values as "already applied". This way we don't need to rerun async
|
|
|
|
|
// effects of the earlier batch in case they are merged.
|
|
|
|
|
// As a result you can think of batch_values as having the latest values of all intersecting
|
|
|
|
|
// batches up until this batch.
|
|
|
|
|
if (!intersects) {
|
|
|
|
|
for (const [source, previous] of batch.previous) {
|
|
|
|
|
if (!batch_values.has(source)) {
|
|
|
|
|
batch_values.set(source, previous);
|
|
|
|
|
for (const [value, snapshot] of batch.previous) {
|
|
|
|
|
if (!this.values.has(value)) {
|
|
|
|
|
this.values.set(value, snapshot);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -909,6 +924,10 @@ export class Batch {
|
|
|
|
|
schedule(effect) {
|
|
|
|
|
last_scheduled_effect = effect;
|
|
|
|
|
|
|
|
|
|
if (!this.cvs.has(effect)) {
|
|
|
|
|
this.cvs.set(effect, effect.cv);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 (
|
|
|
|
|
@ -1137,12 +1156,13 @@ function flush_queued_effects(effects) {
|
|
|
|
|
* This is similar to `mark_reactions`, but it only marks async/block effects
|
|
|
|
|
* depending on `value` and at least one of the other `sources`, so that
|
|
|
|
|
* these effects can re-run after another batch has been committed
|
|
|
|
|
* @param {Batch} batch
|
|
|
|
|
* @param {Value} value
|
|
|
|
|
* @param {Source[]} sources
|
|
|
|
|
* @param {Set<Value>} marked
|
|
|
|
|
* @param {Map<Reaction, boolean>} checked
|
|
|
|
|
*/
|
|
|
|
|
function mark_effects(value, sources, marked, checked) {
|
|
|
|
|
function mark_effects(batch, value, sources, marked, checked) {
|
|
|
|
|
if (marked.has(value)) return;
|
|
|
|
|
marked.add(value);
|
|
|
|
|
|
|
|
|
|
@ -1151,14 +1171,13 @@ function mark_effects(value, sources, marked, checked) {
|
|
|
|
|
const flags = reaction.f;
|
|
|
|
|
|
|
|
|
|
if ((flags & DERIVED) !== 0) {
|
|
|
|
|
mark_effects(/** @type {Derived} */ (reaction), sources, marked, checked);
|
|
|
|
|
} else if (
|
|
|
|
|
(flags & (ASYNC | BLOCK_EFFECT)) !== 0 &&
|
|
|
|
|
(flags & DIRTY) === 0 &&
|
|
|
|
|
depends_on(reaction, sources, checked)
|
|
|
|
|
) {
|
|
|
|
|
set_signal_status(reaction, DIRTY);
|
|
|
|
|
schedule_effect(/** @type {Effect} */ (reaction));
|
|
|
|
|
batch.current.delete(/** @type {Derived} */ (reaction));
|
|
|
|
|
batch.cvs.set(/** @type {Derived} */ (reaction), -1);
|
|
|
|
|
|
|
|
|
|
mark_effects(batch, /** @type {Derived} */ (reaction), sources, marked, checked);
|
|
|
|
|
} else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0 && depends_on(reaction, sources, checked)) {
|
|
|
|
|
batch.schedule(/** @type {Effect} */ (reaction));
|
|
|
|
|
batch.cvs.set(reaction, -1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -1180,7 +1199,6 @@ function mark_eager_effects(value, effects) {
|
|
|
|
|
if ((flags & DERIVED) !== 0) {
|
|
|
|
|
mark_eager_effects(/** @type {Derived} */ (reaction), effects);
|
|
|
|
|
} else if ((flags & EAGER_EFFECT) !== 0) {
|
|
|
|
|
set_signal_status(reaction, DIRTY);
|
|
|
|
|
effects.add(/** @type {Effect} */ (reaction));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -1213,17 +1231,11 @@ function depends_on(reaction, sources, checked) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Effect} effect
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
export function schedule_effect(effect) {
|
|
|
|
|
/** @type {Batch} */ (current_batch).schedule(effect);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @type {Source<number>[]} */
|
|
|
|
|
let eager_versions = [];
|
|
|
|
|
|
|
|
|
|
let running_eager_effect = false;
|
|
|
|
|
|
|
|
|
|
function eager_flush() {
|
|
|
|
|
flushSync(() => {
|
|
|
|
|
const eager = eager_versions;
|
|
|
|
|
@ -1262,32 +1274,47 @@ export function eager(fn) {
|
|
|
|
|
|
|
|
|
|
get(version);
|
|
|
|
|
|
|
|
|
|
eager_effect(() => {
|
|
|
|
|
if (DEV) {
|
|
|
|
|
version.label = '<eager>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var effect = eager_effect(() => {
|
|
|
|
|
if (initial) {
|
|
|
|
|
// the first time this runs, we create an eager effect
|
|
|
|
|
// that will run eagerly whenever the expression changes
|
|
|
|
|
var previous_batch_values = batch_values;
|
|
|
|
|
var previous_batch = active_batch;
|
|
|
|
|
var previous_running_eager_effect = running_eager_effect;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
batch_values = null;
|
|
|
|
|
running_eager_effect = true;
|
|
|
|
|
active_batch = null;
|
|
|
|
|
value = fn();
|
|
|
|
|
} finally {
|
|
|
|
|
batch_values = previous_batch_values;
|
|
|
|
|
active_batch = previous_batch;
|
|
|
|
|
running_eager_effect = previous_running_eager_effect;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
if (!current_batch?.is_fork) {
|
|
|
|
|
// 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);
|
|
|
|
|
eager_versions.push(version);
|
|
|
|
|
} else {
|
|
|
|
|
fn();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// TODO ideally this wouldn't be necessary. I haven't figured out a way for these
|
|
|
|
|
// effects to correctly be marked dirty when `$state.eager(...)` arguments change
|
|
|
|
|
effect.f |= STATE_EAGER_EFFECT;
|
|
|
|
|
|
|
|
|
|
initial = false;
|
|
|
|
|
|
|
|
|
|
return value;
|
|
|
|
|
@ -1302,18 +1329,14 @@ export function eager(fn) {
|
|
|
|
|
*/
|
|
|
|
|
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);
|
|
|
|
|
if ((effect.f & BRANCH_EFFECT) !== 0) {
|
|
|
|
|
if ((effect.f & CLEAN) === 0) {
|
|
|
|
|
effect.f ^= CLEAN;
|
|
|
|
|
} else {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
set_signal_status(effect, CLEAN);
|
|
|
|
|
|
|
|
|
|
var e = effect.first;
|
|
|
|
|
while (e !== null) {
|
|
|
|
|
reset_branch(e, tracked);
|
|
|
|
|
@ -1326,8 +1349,6 @@ function reset_branch(effect, tracked) {
|
|
|
|
|
* @param {Effect} effect
|
|
|
|
|
*/
|
|
|
|
|
function reset_all(effect) {
|
|
|
|
|
set_signal_status(effect, CLEAN);
|
|
|
|
|
|
|
|
|
|
var e = effect.first;
|
|
|
|
|
while (e !== null) {
|
|
|
|
|
reset_all(e);
|
|
|
|
|
@ -1365,11 +1386,11 @@ export function fork(fn) {
|
|
|
|
|
|
|
|
|
|
var batch = Batch.ensure();
|
|
|
|
|
batch.is_fork = true;
|
|
|
|
|
batch_values = new Map();
|
|
|
|
|
|
|
|
|
|
var committed = false;
|
|
|
|
|
var settled = batch.settled();
|
|
|
|
|
|
|
|
|
|
batch.apply();
|
|
|
|
|
flushSync(fn);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
@ -1387,10 +1408,15 @@ export function fork(fn) {
|
|
|
|
|
|
|
|
|
|
batch.is_fork = false;
|
|
|
|
|
|
|
|
|
|
// apply changes and update write versions so deriveds see the change
|
|
|
|
|
for (var [source, [value]] of batch.current) {
|
|
|
|
|
source.v = value;
|
|
|
|
|
source.wv = increment_write_version();
|
|
|
|
|
for (var [reaction, cv] of batch.cvs) {
|
|
|
|
|
if (cv > reaction.cv) {
|
|
|
|
|
reaction.cv = cv;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (var [value, snapshot] of batch.current) {
|
|
|
|
|
value.v = snapshot.v;
|
|
|
|
|
value.wv = snapshot.wv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// trigger any `$state.eager(...)` expressions with the new state.
|
|
|
|
|
@ -1398,6 +1424,8 @@ export function fork(fn) {
|
|
|
|
|
// can't just encounter them during traversal, we need to
|
|
|
|
|
// proactively flush them
|
|
|
|
|
// TODO maybe there's a better implementation?
|
|
|
|
|
// e.g. maybe we can just schedule them so that they run
|
|
|
|
|
// with everything else during batch.flush?
|
|
|
|
|
flushSync(() => {
|
|
|
|
|
/** @type {Set<Effect>} */
|
|
|
|
|
var eager_effects = new Set();
|
|
|
|
|
@ -1414,9 +1442,8 @@ export function fork(fn) {
|
|
|
|
|
await settled;
|
|
|
|
|
},
|
|
|
|
|
discard: () => {
|
|
|
|
|
// cause any MAYBE_DIRTY deriveds to update
|
|
|
|
|
// if they depend on things thath changed
|
|
|
|
|
// inside the discarded fork
|
|
|
|
|
// cause any deriveds to update if they depend on
|
|
|
|
|
// things that changed inside the discarded fork
|
|
|
|
|
for (var source of batch.current.keys()) {
|
|
|
|
|
source.wv = increment_write_version();
|
|
|
|
|
}
|
|
|
|
|
@ -1428,6 +1455,34 @@ export function fork(fn) {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Value} value
|
|
|
|
|
*/
|
|
|
|
|
export function get_wv(value) {
|
|
|
|
|
var snapshot = active_batch?.values?.get(value);
|
|
|
|
|
return snapshot ? snapshot.wv : value.wv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Reaction} reaction
|
|
|
|
|
*/
|
|
|
|
|
export function get_cv(reaction) {
|
|
|
|
|
return active_batch?.cvs.get(reaction) ?? reaction.cv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Reaction} reaction
|
|
|
|
|
*/
|
|
|
|
|
export function set_cv(reaction, cv = write_version) {
|
|
|
|
|
// TODO seems weird to have both of these
|
|
|
|
|
current_batch?.cvs.set(reaction, cv);
|
|
|
|
|
active_batch?.cvs.set(reaction, cv);
|
|
|
|
|
|
|
|
|
|
if (!current_batch?.is_fork && !running_eager_effect) {
|
|
|
|
|
reaction.cv = cv;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Forcibly remove all current batches, to prevent cross-talk between tests
|
|
|
|
|
*/
|
|
|
|
|
|