incremental-batches
Rich Harris 3 weeks ago
parent de94223003
commit 12235f0300

@ -26,13 +26,21 @@ 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, unlink_effect } from './effects.js';
import { defer_effect } from './utils.js';
import { UNINITIALIZED } from '../../../constants.js';
@ -60,6 +68,16 @@ export let previous_batch = null;
*/
export let batch_values = null;
/**
* @type {Map<Reaction, number> | null}
*/
export let batch_cvs = null;
/**
* @type {Map<Value, number> | null}
*/
export let batch_wvs = null;
/** @type {Effect | null} */
let last_scheduled_effect = null;
@ -161,6 +179,12 @@ export class Batch {
*/
#maybe_dirty_effects = new Set();
/** @type {Map<Value, number>} */
wvs = new Map();
/** @type {Map<Reaction, number>} */
cvs = new Map();
/**
* A map of branches that still exist, but will be destroyed when this batch
* is committed we skip over these during `process`.
@ -241,6 +265,10 @@ export class Batch {
infinite_loop_guard();
}
for (const source of this.current.keys()) {
mark_reactions(source, null);
}
// we only reschedule previously-deferred effects if we expect
// to be able to run them after processing the batch
if (!this.#is_deferred()) {
@ -414,9 +442,8 @@ export class Batch {
* batch, noting its previous and current values
* @param {Value} source
* @param {any} old_value
* @param {boolean} [is_derived]
*/
capture(source, old_value, is_derived = false) {
capture(source, old_value) {
if (old_value !== UNINITIALIZED && !this.previous.has(source)) {
this.previous.set(source, old_value);
}
@ -426,6 +453,11 @@ export class Batch {
this.current.set(source, source.v);
batch_values?.set(source, source.v);
}
var version = increment_write_version();
source.wv = version;
this.wvs.set(source, version);
}
/**
@ -450,7 +482,7 @@ export class Batch {
deactivate() {
current_batch = null;
batch_values = null;
batch_values = batch_cvs = batch_wvs = null;
}
flush() {
@ -469,7 +501,7 @@ export class Batch {
is_processing = false;
current_batch = null;
batch_values = null;
batch_values = batch_cvs = batch_wvs = null;
old_values.clear();
@ -672,7 +704,7 @@ export class Batch {
apply() {
if (!async_mode_flag) {
// TODO previously we bailed here if there was only one (non-fork) batch... maybe we can reinstate that
batch_values = null;
batch_values = batch_cvs = batch_wvs = null;
return;
}
@ -683,6 +715,9 @@ export class Batch {
batch_values.set(source, value);
}
batch_cvs = this.cvs;
batch_wvs = this.wvs;
// ...and undo changes belonging to other batches unless they block this one
for (const batch of batches) {
if (batch === this || batch.is_fork) continue;
@ -717,6 +752,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 (
@ -1036,12 +1075,16 @@ export function eager(fn) {
// 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_cvs = batch_cvs;
var previous_batch_wvs = batch_wvs;
try {
batch_values = null;
batch_values = batch_cvs = batch_wvs = null;
value = fn();
} finally {
batch_values = previous_batch_values;
batch_cvs = previous_batch_cvs;
batch_wvs = previous_batch_wvs;
}
return;
@ -1198,6 +1241,14 @@ export function fork(fn) {
};
}
/**
* @param {Reaction} reaction
*/
export function set_cv(reaction) {
batch_cvs?.set(reaction, write_version);
reaction.cv = write_version;
}
/**
* Forcibly remove all current batches, to prevent cross-talk between tests
*/

@ -41,7 +41,7 @@ import { get_error } from '../../shared/dev.js';
import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { component_context } from '../context.js';
import { UNINITIALIZED } from '../../../constants.js';
import { batch_values, current_batch } from './batch.js';
import { batch_values, batch_wvs, current_batch, set_cv } from './batch.js';
import { increment_pending, unset_context } from './async.js';
import { deferred, includes, noop } from '../../shared/utils.js';
@ -120,7 +120,7 @@ export function async_derived(fn, label, location) {
var promise = /** @type {Promise<V>} */ (/** @type {unknown} */ (undefined));
var signal = source(/** @type {V} */ (UNINITIALIZED));
if (DEV) signal.label = label;
if (DEV) signal.label = label ?? '{await ...}';
// only suspend in async deriveds created on initialisation
var should_suspend = !active_reaction;
@ -385,12 +385,12 @@ export function execute_derived(derived) {
* @returns {void}
*/
export function update_derived(derived) {
var old_value = derived.v;
var value = execute_derived(derived);
derived.cv = write_version;
set_cv(derived);
if (!derived.equals(value)) {
batch_wvs?.set(derived, write_version);
derived.wv = write_version;
// in a fork, we don't update the underlying value, just `batch_values`.

@ -111,7 +111,6 @@ function create_effect(type, fn) {
b: parent && parent.b,
prev: null,
teardown: null,
wv: 0,
cv: -1,
ac: null
};

@ -231,11 +231,9 @@ export function internal_set(source, value, updated_during_traversal = null) {
}
}
source.wv = increment_write_version();
// For debugging, in case you want to know which reactions are being scheduled:
// log_reactions(source);
mark_reactions(source, DIRTY, updated_during_traversal);
mark_reactions(source, updated_during_traversal);
// It's possible that the current reaction might not have up-to-date dependencies
// whilst it's actively running. So in the case of ensuring it registers the reaction
@ -314,11 +312,10 @@ export function increment(source) {
/**
* @param {Value} signal
* @param {number} status should be DIRTY or MAYBE_DIRTY
* @param {Effect[] | null} updated_during_traversal
* @returns {void}
*/
function mark_reactions(signal, status, updated_during_traversal) {
export function mark_reactions(signal, updated_during_traversal) {
var reactions = signal.reactions;
if (reactions === null) return;
@ -349,7 +346,7 @@ function mark_reactions(signal, status, updated_during_traversal) {
reaction.f |= WAS_MARKED;
}
mark_reactions(derived, MAYBE_DIRTY, updated_during_traversal);
mark_reactions(derived, updated_during_traversal);
}
} else {
var effect = /** @type {Effect} */ (reaction);

@ -11,8 +11,6 @@ import type { Boundary } from '../dom/blocks/boundary';
export interface Signal {
/** Flags bitmask */
f: number;
/** Write version */
wv: number;
}
export interface Value<V = unknown> extends Signal {
@ -36,6 +34,8 @@ export interface Value<V = unknown> extends Signal {
set_during_effect?: boolean;
/** A function that retrieves the underlying source, used for each block item signals */
trace?: null | (() => void);
/** Write version */
wv: number;
/** Read version */
rv: number;
}

@ -48,10 +48,13 @@ import {
} from './context.js';
import {
Batch,
batch_cvs,
batch_values,
batch_wvs,
current_batch,
flushSync,
schedule_effect
schedule_effect,
set_cv
} from './reactivity/batch.js';
import { handle_error } from './error-handling.js';
import { UNINITIALIZED } from '../../constants.js';
@ -165,26 +168,31 @@ export function is_dirty(reaction) {
var dependencies = /** @type {Value[]} */ (reaction.deps);
if (dependencies !== null) {
var length = dependencies.length;
if (dependencies === null) {
return false;
}
for (var i = 0; i < length; i++) {
var dependency = dependencies[i];
var cv = batch_cvs?.get(reaction) ?? reaction.cv;
if ((dependency.f & DERIVED) !== 0) {
if (is_dirty(/** @type {Derived} */ (dependency))) {
update_derived(/** @type {Derived} */ (dependency));
}
}
var length = dependencies.length;
if (dependency.wv > reaction.cv) {
return true;
for (var i = 0; i < length; i++) {
var dependency = dependencies[i];
if ((dependency.f & DERIVED) !== 0) {
if (is_dirty(/** @type {Derived} */ (dependency))) {
update_derived(/** @type {Derived} */ (dependency));
}
}
}
reaction.cv = write_version;
var wv = batch_wvs?.get(dependency) ?? dependency.wv;
if (wv > cv) {
return true;
}
}
set_cv(reaction);
return false;
}
@ -443,18 +451,16 @@ export function update_effect(effect) {
destroy_effect_children(effect);
}
effect.cv = write_version;
set_cv(effect);
execute_effect_teardown(effect);
var teardown = update_reaction(effect);
effect.teardown = typeof teardown === 'function' ? teardown : null;
if (!is_runes()) {
effect.cv = write_version;
set_cv(effect);
}
effect.wv = write_version; // TODO should this be assigned before the update?
// In DEV, increment versions of any sources that were written to during the effect,
// so that they are correctly marked as dirty when the effect re-runs
if (DEV && tracing_mode_flag && (effect.f & DIRTY) !== 0 && effect.deps !== null) {

Loading…
Cancel
Save