entangle at the source-level, too

entangle-batches-2
Simon Holthausen 2 months ago
parent 546abea513
commit 2481374e95
No known key found for this signature in database

@ -400,9 +400,9 @@ export class Batch {
* Keep this batch separate from a sealed predecessor, coalescing it with any * Keep this batch separate from a sealed predecessor, coalescing it with any
* other work that is already waiting for that predecessor. * other work that is already waiting for that predecessor.
* @param {Batch} owner * @param {Batch} owner
* @param {Reaction} reaction * @param {Reaction | Value} signal
*/ */
#wait(owner, reaction) { #wait(owner, signal) {
var waiter = owner.waiter; var waiter = owner.waiter;
if (waiter !== null) { if (waiter !== null) {
@ -418,7 +418,7 @@ export class Batch {
owner.waiter = this; owner.waiter = this;
var waiting = (this.waiting ??= { batches: new Set(), reactions: new Map() }); var waiting = (this.waiting ??= { batches: new Set(), reactions: new Map() });
waiting.batches.add(owner); waiting.batches.add(owner);
waiting.reactions.set(reaction, owner); if (Object.hasOwn(signal, 'deps')) waiting.reactions.set(signal, owner);
} }
/** /**
@ -431,9 +431,7 @@ export class Batch {
if (waiter === null || !(waiter = waiter.resolved()).linked) return; if (waiter === null || !(waiter = waiter.resolved()).linked) return;
var waiting = /** @type {{ batches: Set<Batch>, reactions: Map<Reaction, Batch> }} */ ( var waiting = /** @type {NonNullable<typeof waiter.waiting>} */ (waiter.waiting);
waiter.waiting
);
waiting.batches.delete(this); waiting.batches.delete(this);
for (const [reaction, owner] of waiting.reactions) { for (const [reaction, owner] of waiting.reactions) {
@ -474,42 +472,48 @@ export class Batch {
} }
/** /**
* Take ownership of a reaction. Deriveds and user/block/async effects can * Take ownership of a signal. Sources, deriveds and user/block/async effects can
* only ever belong to one live batch if the reaction is already claimed * only ever belong to one live batch. If the signal is already claimed
* by another live batch, the two reactivity graphs overlap, which means the * by another live batch, the two reactivity graphs overlap, which means the
* batches describe the same 'world', and they are merged into one. After too * batches describe the same 'world', and they are merged into one. After too
* many restarts, new work waits behind the current batch instead. * many restarts, new work waits behind the current batch instead.
* Template-level (render/managed) effects are exempt: they are leaves, so * Template-level (render/managed) effects are exempt: they are leaves, so
* independent batches can share them without their graphs being entangled. * independent batches can share them without their graphs being entangled.
* Forks are also exempt they are speculative and live in their own world * Forks are also exempt - they are speculative and live in their own world.
* @param {Reaction} reaction * @param {Reaction | Value} signal
* @returns {boolean} Whether the reaction must wait for a sealed batch * @returns {boolean} Whether the signal must wait for a sealed batch
*/ */
claim(reaction) { claim(signal) {
// already claimed by this batch — nothing below could change anything // already claimed by this batch — nothing below could change anything
if (reaction.batch === this) return false; if (signal.batch === this) return false;
if (!async_mode_flag || this.is_fork) return false; if (!async_mode_flag || this.is_fork) return false;
if ((reaction.f & (DERIVED | ASYNC | BLOCK_EFFECT | USER_EFFECT)) === 0) return false; var is_source = !Object.hasOwn(signal, 'deps');
if (!is_source && (signal.f & (DERIVED | ASYNC | BLOCK_EFFECT | USER_EFFECT)) === 0) {
return false;
}
// template expression deriveds are leaves — they don't entangle // template expression deriveds are leaves — they don't entangle
if ((reaction.f & TEMPLATE_EXPRESSION) !== 0) return false; if ((signal.f & TEMPLATE_EXPRESSION) !== 0) return false;
var owner = reaction.batch && reaction.batch.resolved(); var owner = signal.batch && signal.batch.resolved();
if (this.is_eager) { if (this.is_eager) {
// eager version bumps don't entangle — but an effect that belongs to // eager version bumps don't entangle — but an effect that belongs to
// another live batch's world, and which we are about to run in ours, // another live batch's world, and which we are about to run in ours,
// must afterwards be re-established in the owner's world // must afterwards be re-established in the owner's world
if ( if (
(reaction.f & DERIVED) === 0 && !is_source &&
(signal.f & DERIVED) === 0 &&
owner !== null && owner !== null &&
owner !== this && owner !== this &&
owner.linked && owner.linked &&
!owner.is_fork !owner.is_fork
) { ) {
(owner.#dirty_effects ??= new Set()).add(/** @type {Effect} */ (reaction)); // TODO
(owner.#dirty_effects ??= new Set()).add(/** @type {Effect} */ (signal));
} }
return false; return false;
@ -518,12 +522,12 @@ export class Batch {
if (owner !== null && owner !== this && owner.linked && !owner.is_fork) { if (owner !== null && owner !== this && owner.linked && !owner.is_fork) {
if (owner.waiting !== null) { if (owner.waiting !== null) {
this.#merge(owner); this.#merge(owner);
reaction.batch = this; signal.batch = this;
return false; return false;
} }
if (owner.restarts >= MAX_ENTANGLED_RESTARTS) { if (owner.restarts >= MAX_ENTANGLED_RESTARTS) {
this.#wait(owner, reaction); this.#wait(owner, signal);
return true; return true;
} }
@ -532,7 +536,7 @@ export class Batch {
this.restarts = Math.max(this.restarts, restarts); this.restarts = Math.max(this.restarts, restarts);
} }
reaction.batch = this; signal.batch = this;
return false; return false;
} }

@ -79,6 +79,7 @@ export function source(v, stack) {
f: 0, // TODO ideally we could skip this altogether, but it causes type errors f: 0, // TODO ideally we could skip this altogether, but it causes type errors
v, v,
reactions: null, reactions: null,
batch: null,
equals, equals,
rv: 0, rv: 0,
wv: 0 wv: 0
@ -233,6 +234,7 @@ export function internal_set(source, value, updated_during_traversal = null) {
source.wv = increment_write_version(); source.wv = increment_write_version();
batch.claim(source);
// For debugging, in case you want to know which reactions are being scheduled: // For debugging, in case you want to know which reactions are being scheduled:
// log_reactions(source); // log_reactions(source);
mark_reactions(source, DIRTY, updated_during_traversal); mark_reactions(source, DIRTY, updated_during_traversal);

@ -14,6 +14,14 @@ export interface Signal {
f: number; f: number;
/** Write version */ /** Write version */
wv: number; wv: number;
/**
* The batch that most recently claimed this signal.
* Only set for sources, non-render-only deriveds and user/block/async effects.
* If two batches claim the same reaction, their reactivity graphs overlap and are merged,
* unless the newer batch is waiting behind a sealed predecessor.
* A claim expires when its batch is committed or discarded (`!batch.linked`)
*/
batch: null | Batch;
} }
export interface Value<V = unknown> extends Signal { export interface Value<V = unknown> extends Signal {
@ -51,14 +59,6 @@ export interface Reaction extends Signal {
deps: null | Value[]; deps: null | Value[];
/** An AbortController that aborts when the signal is destroyed */ /** An AbortController that aborts when the signal is destroyed */
ac: null | AbortController; ac: null | AbortController;
/**
* The batch that most recently claimed this reaction by marking it dirty.
* Only set for deriveds and user/block/async effects if two batches claim
* the same reaction, their reactivity graphs overlap and they are merged,
* unless the newer batch is waiting behind a sealed predecessor.
* A claim expires when its batch is committed or discarded (`!batch.linked`)
*/
batch: null | Batch;
} }
export interface Derived<V = unknown> extends Value<V>, Reaction { export interface Derived<V = unknown> extends Value<V>, Reaction {

@ -13,14 +13,11 @@ export default test({
'<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0' '<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0'
); );
// `b` is only read by template leaves, so the second update doesn't
// entangle with the pending batch — it commits immediately, while the
// pending batch's `a` is still held back
b.click(); b.click();
await tick(); await tick();
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
'<button>a_b 0_2</button> <button>b 2</button> <button>resolve</button> 0' '<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0'
); );
resolve.click(); resolve.click();

@ -0,0 +1,30 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [a_b, b, resolve] = target.querySelectorAll('button');
a_b.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0'
);
b.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0'
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>a_b 1_2</button> <button>b 2</button> <button>resolve</button> 1'
);
}
});

@ -0,0 +1,18 @@
<script>
let a = $state(0);
let b = $state(0);
let deferreds = [];
const value = () => (`${a}_${b}`);
function push(value) {
if (!value) return value;
return new Promise((resolve) => {
deferreds.push(() => resolve(value));
});
}
</script>
<button onclick={() => {a++;b++}}>a_b {value()}</button>
<button onclick={() => (b++)}>b {b}</button>
<button onclick={() => (deferreds.shift()?.())}>resolve</button>
{await push(a)}
Loading…
Cancel
Save