fix: merge async batches on new dependencies

pull/18674/head
Kamil Jakubus 3 weeks ago
parent 10ceb91adc
commit 597609c53c

@ -171,6 +171,14 @@ export class Batch {
*/ */
#new_effects = []; #new_effects = [];
/**
* Values that were first read by a reaction while this batch was time travelling
* over an earlier batch that changed them. These reads connect the batches even
* though this batch did not write to the values itself.
* @type {Set<Value>}
*/
#new_dependencies = new Set();
/** /**
* 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>} * @type {Set<Effect>}
@ -482,6 +490,12 @@ export class Batch {
while (batch !== null) { while (batch !== null) {
if (!batch.is_fork) { if (!batch.is_fork) {
for (const value of this.#new_dependencies) {
if (batch.current.has(value)) {
return batch;
}
}
// if the batches are connected, break // if the batches are connected, break
for (const [value, [, is_derived]] of this.current) { for (const [value, [, is_derived]] of this.current) {
if (batch.current.has(value) && !is_derived) { if (batch.current.has(value) && !is_derived) {
@ -500,6 +514,10 @@ export class Batch {
* @param {Batch} batch * @param {Batch} batch
*/ */
#merge(batch) { #merge(batch) {
for (const value of batch.#new_dependencies) {
this.#new_dependencies.add(value);
}
for (const [source, value] of batch.current) { for (const [source, value] of batch.current) {
if (!this.previous.has(source) && batch.previous.has(source)) { if (!this.previous.has(source) && batch.previous.has(source)) {
this.previous.set(source, batch.previous.get(source)); this.previous.set(source, batch.previous.get(source));
@ -598,6 +616,28 @@ export class Batch {
} }
} }
/**
* If a reaction discovers a dependency that was changed by an earlier pending
* batch, connect the two batches and expose the current value while traversing.
* The current batch will be merged into the earlier one before it can commit.
* @param {Value} value
*/
capture_dependency(value) {
if (this.is_fork || this.current.has(value)) return;
var batch = this.#prev;
while (batch !== null) {
if (!batch.is_fork && batch.current.has(value)) {
this.#new_dependencies.add(value);
batch_values?.set(value, /** @type {[any, boolean]} */ (batch.current.get(value))[0]);
return;
}
batch = batch.#prev;
}
}
activate() { activate() {
current_batch = this; current_batch = this;
} }

@ -540,6 +540,14 @@ export function settled() {
export function get(signal) { export function get(signal) {
var flags = signal.f; var flags = signal.f;
var is_derived = (flags & DERIVED) !== 0; var is_derived = (flags & DERIVED) !== 0;
var is_new_dependency =
batch_values !== null &&
!untracking &&
((active_reaction !== null &&
(active_reaction.deps === null || !includes.call(active_reaction.deps, signal))) ||
(active_reaction === null &&
active_effect !== null &&
(active_effect.f & REACTION_RAN) === 0));
captured_signals?.add(signal); captured_signals?.add(signal);
@ -699,6 +707,10 @@ export function get(signal) {
} }
if (batch_values?.has(signal)) { if (batch_values?.has(signal)) {
if (is_new_dependency) {
(current_batch ?? previous_batch)?.capture_dependency(signal);
}
return batch_values.get(signal); return batch_values.get(signal);
} }

@ -16,20 +16,12 @@ export default test({
<button>x</button> <button>x</button>
<button>y++</button> <button>y++</button>
<button>resolve</button> <button>resolve</button>
world `
` // if this does not show world - that would also be ok
); );
resolve.click(); resolve.click();
await tick(); await tick();
assert.deepEqual(logs, [ assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']);
'universe',
'world',
'$effect: world',
'$effect: universe',
'$effect: universe'
]);
// assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']); // this would also be ok
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
` `

@ -17,13 +17,7 @@ export default test({
<button>y++</button> <button>y++</button>
<button>resolve</button> <button>resolve</button>
<hr> <hr>
world `
"world"
world
world
world
"world"
` // if this does not show world "world" world world world "world" - then this would also be ok
); );
resolve.click(); resolve.click();

@ -29,13 +29,7 @@ export default test({
<button>y++</button> <button>y++</button>
<button>resolve</button> <button>resolve</button>
<hr> <hr>
world `
"world"
world
world
world
"world"
` // if this does not show world "world" world world world "world" - then this would also be ok
); );
resolve.click(); resolve.click();

@ -11,26 +11,19 @@ export default test({
y.click(); y.click();
await tick(); await tick();
assert.deepEqual(logs, ['universe', 'world', '$effect: world']); assert.deepEqual(logs, ['universe', 'universe']);
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
` `
<button>x</button> <button>x</button>
<button>y++</button> <button>y++</button>
<button>resolve</button> <button>resolve</button>
world
` `
); );
resolve.click(); resolve.click();
await tick(); await tick();
assert.deepEqual(logs, [ assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']);
'universe',
'world',
'$effect: world',
'$effect: universe',
'$effect: universe'
]);
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
` `

Loading…
Cancel
Save