entangle-batches-2
Simon Holthausen 2 months ago
parent 014be9cccd
commit 60f8c84a6d
No known key found for this signature in database

@ -122,6 +122,53 @@ function transfer_set(target, source) {
return target; return target;
} }
/**
* Transfer entries from `source` into `target`, creating `target` if necessary.
* If a key exists in both, `combine` determines the resulting value
* (defaulting to the incoming value)
* @template K, V
* @param {Map<K, V> | null} target
* @param {Map<K, V> | null} source
* @param {(existing: V, incoming: V) => V} [combine]
* @returns {Map<K, V> | null}
*/
function transfer_map(target, source, combine) {
if (source === null || source.size === 0) return target;
target ??= new Map();
for (const [key, value] of source) {
target.set(
key,
combine !== undefined && target.has(key)
? combine(/** @type {V} */ (target.get(key)), value)
: value
);
}
return target;
}
/**
* Transfer commit/discard callbacks from a merged batch, keeping them
* bound to the batch they were registered under
* @param {Set<(batch: Batch) => void> | null} target
* @param {Set<(batch: Batch) => void> | null} source
* @param {Batch} batch
* @returns {Set<(batch: Batch) => void> | null}
*/
function transfer_callbacks(target, source, batch) {
if (source === null) return target;
target ??= new Set();
for (const fn of source) {
target.add(() => fn(batch));
}
return target;
}
export class Batch { export class Batch {
id = uid++; id = uid++;
@ -288,7 +335,8 @@ export class Batch {
* `true` while this batch is flushing its effects and is provably terminal * `true` while this batch is flushing its effects and is provably terminal
* solitary, with no pending async work and nothing scheduled. Such a batch * solitary, with no pending async work and nothing scheduled. Such a batch
* unlinks before any other batch can observe its `previous` values, so * unlinks before any other batch can observe its `previous` values, so
* recording them (one `Map` op per derived update) would be dead weight * recording them (one `Map` op per derived update) would be dead weight.
* This exists for performance reasons only.
*/ */
skip_previous = false; skip_previous = false;
@ -586,50 +634,31 @@ export class Batch {
// for signals touched by both batches, keep the oldest `previous` // for signals touched by both batches, keep the oldest `previous`
// (the value from before either batch touched it)... // (the value from before either batch touched it)...
for (const [source, previous] of other.previous) { transfer_map(this.previous, other.previous, (existing, incoming) =>
if (other_is_older || !this.previous.has(source)) { other_is_older ? incoming : existing
this.previous.set(source, previous); );
}
}
// ...and the newest current value // ...and the newest current value
for (const [source, value] of other.current) { transfer_map(this.current, other.current, (existing, incoming) =>
if (!other_is_older || !this.current.has(source)) { other_is_older ? existing : incoming
this.current.set(source, value); );
}
} this.async_deriveds = transfer_map(this.async_deriveds, other.async_deriveds, (existing, d) => {
var newer = d.id > existing.id;
if (other.async_deriveds !== null) { (newer ? existing : d).reject(OBSOLETE);
this.async_deriveds ??= new Map(); return newer ? d : existing;
});
for (const [effect, d] of other.async_deriveds) { other.async_deriveds = null;
var existing = this.async_deriveds.get(effect);
if (existing !== undefined) {
if (d.id > existing.id) {
existing.reject(OBSOLETE);
this.async_deriveds.set(effect, d);
} else {
d.reject(OBSOLETE);
}
} else {
this.async_deriveds.set(effect, d);
}
}
other.async_deriveds = null;
}
this.#pending += other.#pending; this.#pending += other.#pending;
other.#pending = 0; other.#pending = 0;
if (other.#blocking_pending !== null) { this.#blocking_pending = transfer_map(
this.#blocking_pending ??= new Map(); this.#blocking_pending,
other.#blocking_pending,
for (const [effect, count] of other.#blocking_pending) { (existing, count) => existing + count
this.#blocking_pending.set(effect, (this.#blocking_pending.get(effect) ?? 0) + count); );
} other.#blocking_pending = null;
other.#blocking_pending = null;
}
this.transfer_effects(other.#dirty_effects, other.#maybe_dirty_effects); this.transfer_effects(other.#dirty_effects, other.#maybe_dirty_effects);
other.#dirty_effects = null; other.#dirty_effects = null;
@ -638,75 +667,47 @@ export class Batch {
this.#reestablish_effects = transfer_set(this.#reestablish_effects, other.#reestablish_effects); this.#reestablish_effects = transfer_set(this.#reestablish_effects, other.#reestablish_effects);
other.#reestablish_effects = null; other.#reestablish_effects = null;
if (other.#skipped_branches !== null) { this.#skipped_branches = transfer_map(
this.#skipped_branches ??= new Map(); this.#skipped_branches,
other.#skipped_branches,
for (const [effect, tracked] of other.#skipped_branches) { (skipped, tracked) => (skipped.d.push(...tracked.d), skipped.m.push(...tracked.m), skipped)
var skipped = this.#skipped_branches.get(effect); );
if (skipped) { other.#skipped_branches = null;
skipped.d.push(...tracked.d);
skipped.m.push(...tracked.m);
} else {
this.#skipped_branches.set(effect, tracked);
}
}
other.#skipped_branches = null;
}
// commit/discard callbacks stay bound to the batch they were registered // commit/discard callbacks stay bound to the batch they were registered
// under — consumers (branch managers etc) key their state by batch // under — consumers (branch managers etc) key their state by batch
if (other.#commit_callbacks !== null) { this.#commit_callbacks = transfer_callbacks(
this.#commit_callbacks ??= new Set(); this.#commit_callbacks,
other.#commit_callbacks,
for (const fn of other.#commit_callbacks) { other
this.#commit_callbacks.add(() => fn(other)); );
} other.#commit_callbacks = null;
other.#commit_callbacks = null;
} this.#discard_callbacks = transfer_callbacks(
this.#discard_callbacks,
if (other.#discard_callbacks !== null) { other.#discard_callbacks,
this.#discard_callbacks ??= new Set(); other
);
for (const fn of other.#discard_callbacks) { other.#discard_callbacks = null;
this.#discard_callbacks.add(() => fn(other));
} this.#scheduled.push(...other.#scheduled);
other.#discard_callbacks = null;
}
for (const effect of other.#scheduled) {
this.#scheduled.push(effect);
}
other.#scheduled = []; other.#scheduled = [];
if (other.stale_readers !== null) { // TODO could a newer value have been observed by this and other is older?
this.stale_readers ??= new Map(); this.stale_readers = transfer_map(this.stale_readers, other.stale_readers, (observed, seen) =>
/** @type {Map<Value, any>} */ (transfer_map(observed, seen))
for (const [reader, seen] of other.stale_readers) { );
var observed = this.stale_readers.get(reader); other.stale_readers = null;
if (observed === undefined) {
this.stale_readers.set(reader, seen);
} else {
for (const [signal, value] of seen) {
// TODO could a newer value have been observed by this and other is older?
observed.set(signal, value);
}
}
}
other.stale_readers = null;
}
if (other.waiting !== null) { if (other.waiting !== null) {
this.waiting ??= { batches: new Set(), reactions: new Map() }; var waiting = (this.waiting ??= { batches: new Set(), reactions: new Map() });
for (const owner of other.waiting.batches) { for (const owner of other.waiting.batches) {
this.waiting.batches.add(owner); waiting.batches.add(owner);
owner.waiter = this; owner.waiter = this;
} }
for (const [reaction, owner] of other.waiting.reactions) { transfer_map(waiting.reactions, other.waiting.reactions);
this.waiting.reactions.set(reaction, owner);
}
other.waiting = null; other.waiting = null;
} }

Loading…
Cancel
Save