Skip to content

Commit 25cbdc8

Browse files
authored
fix: merge batches (#16866)
whenever a batch is committed, we essentially 'rebase' other pending batches on top of the newly applied state
1 parent 87f7e97 commit 25cbdc8

File tree

8 files changed

+208
-144
lines changed

8 files changed

+208
-144
lines changed

.changeset/lemon-cars-count.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: rebase pending batches when other batches are committed

packages/svelte/src/internal/client/reactivity/batch.js

Lines changed: 126 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { Derived, Effect, Source } from '#client' */
1+
/** @import { Derived, Effect, Source, Value } from '#client' */
22
import {
33
BLOCK_EFFECT,
44
BRANCH_EFFECT,
@@ -10,10 +10,11 @@ import {
1010
INERT,
1111
RENDER_EFFECT,
1212
ROOT_EFFECT,
13-
MAYBE_DIRTY
13+
MAYBE_DIRTY,
14+
DERIVED
1415
} from '#client/constants';
1516
import { async_mode_flag } from '../../flags/index.js';
16-
import { deferred, define_property } from '../../shared/utils.js';
17+
import { deferred, define_property, noop } from '../../shared/utils.js';
1718
import {
1819
active_effect,
1920
is_dirty,
@@ -97,22 +98,8 @@ export class Batch {
9798
#deferred = null;
9899

99100
/**
100-
* True if an async effect inside this batch resolved and
101-
* its parent branch was already deleted
102-
*/
103-
#neutered = false;
104-
105-
/**
106-
* Async effects (created inside `async_derived`) encountered during processing.
107-
* These run after the rest of the batch has updated, since they should
108-
* always have the latest values
109-
* @type {Effect[]}
110-
*/
111-
#async_effects = [];
112-
113-
/**
114-
* The same as `#async_effects`, but for effects inside a newly-created
115-
* `<svelte:boundary>` — these do not prevent the batch from committing
101+
* Async effects inside a newly-created `<svelte:boundary>`
102+
* — these do not prevent the batch from committing
116103
* @type {Effect[]}
117104
*/
118105
#boundary_async_effects = [];
@@ -165,40 +152,15 @@ export class Batch {
165152

166153
previous_batch = null;
167154

168-
/** @type {Map<Source, { v: unknown, wv: number }> | null} */
169-
var current_values = null;
170-
171-
// if there are multiple batches, we are 'time travelling' —
172-
// we need to undo the changes belonging to any batch
173-
// other than the current one
174-
if (async_mode_flag && batches.size > 1) {
175-
current_values = new Map();
176-
batch_deriveds = new Map();
177-
178-
for (const [source, current] of this.current) {
179-
current_values.set(source, { v: source.v, wv: source.wv });
180-
source.v = current;
181-
}
182-
183-
for (const batch of batches) {
184-
if (batch === this) continue;
185-
186-
for (const [source, previous] of batch.#previous) {
187-
if (!current_values.has(source)) {
188-
current_values.set(source, { v: source.v, wv: source.wv });
189-
source.v = previous;
190-
}
191-
}
192-
}
193-
}
155+
var revert = Batch.apply(this);
194156

195157
for (const root of root_effects) {
196158
this.#traverse_effect_tree(root);
197159
}
198160

199161
// if we didn't start any new async work, and no async work
200162
// is outstanding from a previous flush, commit
201-
if (this.#async_effects.length === 0 && this.#pending === 0) {
163+
if (this.#pending === 0) {
202164
this.#commit();
203165

204166
var render_effects = this.#render_effects;
@@ -210,7 +172,7 @@ export class Batch {
210172

211173
// If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with
212174
// newly updated sources, which could lead to infinite loops when effects run over and over again.
213-
previous_batch = current_batch;
175+
previous_batch = this;
214176
current_batch = null;
215177

216178
flush_queued_effects(render_effects);
@@ -223,27 +185,12 @@ export class Batch {
223185
this.#defer_effects(this.#block_effects);
224186
}
225187

226-
if (current_values) {
227-
for (const [source, { v, wv }] of current_values) {
228-
// reset the source to the current value (unless
229-
// it got a newer value as a result of effects running)
230-
if (source.wv <= wv) {
231-
source.v = v;
232-
}
233-
}
234-
235-
batch_deriveds = null;
236-
}
237-
238-
for (const effect of this.#async_effects) {
239-
update_effect(effect);
240-
}
188+
revert();
241189

242190
for (const effect of this.#boundary_async_effects) {
243191
update_effect(effect);
244192
}
245193

246-
this.#async_effects = [];
247194
this.#boundary_async_effects = [];
248195
}
249196

@@ -272,12 +219,8 @@ export class Batch {
272219
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
273220
this.#render_effects.push(effect);
274221
} else if ((flags & CLEAN) === 0) {
275-
if ((flags & ASYNC) !== 0) {
276-
var effects = effect.b?.is_pending()
277-
? this.#boundary_async_effects
278-
: this.#async_effects;
279-
280-
effects.push(effect);
222+
if ((flags & ASYNC) !== 0 && effect.b?.is_pending()) {
223+
this.#boundary_async_effects.push(effect);
281224
} else if (is_dirty(effect)) {
282225
if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect);
283226
update_effect(effect);
@@ -350,10 +293,6 @@ export class Batch {
350293
}
351294
}
352295

353-
neuter() {
354-
this.#neutered = true;
355-
}
356-
357296
flush() {
358297
if (queued_root_effects.length > 0) {
359298
this.activate();
@@ -374,13 +313,58 @@ export class Batch {
374313
* Append and remove branches to/from the DOM
375314
*/
376315
#commit() {
377-
if (!this.#neutered) {
378-
for (const fn of this.#callbacks) {
379-
fn();
380-
}
316+
for (const fn of this.#callbacks) {
317+
fn();
381318
}
382319

383320
this.#callbacks.clear();
321+
322+
// If there are other pending batches, they now need to be 'rebased' —
323+
// in other words, we re-run block/async effects with the newly
324+
// committed state, unless the batch in question has a more
325+
// recent value for a given source
326+
if (batches.size > 1) {
327+
this.#previous.clear();
328+
329+
let is_earlier = true;
330+
331+
for (const batch of batches) {
332+
if (batch === this) {
333+
is_earlier = false;
334+
continue;
335+
}
336+
337+
for (const [source, value] of this.current) {
338+
if (batch.current.has(source)) {
339+
if (is_earlier) {
340+
// bring the value up to date
341+
batch.current.set(source, value);
342+
} else {
343+
// later batch has more recent value,
344+
// no need to re-run these effects
345+
continue;
346+
}
347+
}
348+
349+
mark_effects(source);
350+
}
351+
352+
if (queued_root_effects.length > 0) {
353+
current_batch = batch;
354+
const revert = Batch.apply(batch);
355+
356+
for (const root of queued_root_effects) {
357+
batch.#traverse_effect_tree(root);
358+
}
359+
360+
queued_root_effects = [];
361+
revert();
362+
}
363+
}
364+
365+
current_batch = null;
366+
}
367+
384368
batches.delete(this);
385369
}
386370

@@ -402,9 +386,6 @@ export class Batch {
402386
schedule_effect(e);
403387
}
404388

405-
this.#render_effects = [];
406-
this.#effects = [];
407-
408389
this.flush();
409390
} else {
410391
this.deactivate();
@@ -444,6 +425,51 @@ export class Batch {
444425
static enqueue(task) {
445426
queue_micro_task(task);
446427
}
428+
429+
/**
430+
* @param {Batch} current_batch
431+
*/
432+
static apply(current_batch) {
433+
if (!async_mode_flag || batches.size === 1) {
434+
return noop;
435+
}
436+
437+
// if there are multiple batches, we are 'time travelling' —
438+
// we need to undo the changes belonging to any batch
439+
// other than the current one
440+
441+
/** @type {Map<Source, { v: unknown, wv: number }>} */
442+
var current_values = new Map();
443+
batch_deriveds = new Map();
444+
445+
for (const [source, current] of current_batch.current) {
446+
current_values.set(source, { v: source.v, wv: source.wv });
447+
source.v = current;
448+
}
449+
450+
for (const batch of batches) {
451+
if (batch === current_batch) continue;
452+
453+
for (const [source, previous] of batch.#previous) {
454+
if (!current_values.has(source)) {
455+
current_values.set(source, { v: source.v, wv: source.wv });
456+
source.v = previous;
457+
}
458+
}
459+
}
460+
461+
return () => {
462+
for (const [source, { v, wv }] of current_values) {
463+
// reset the source to the current value (unless
464+
// it got a newer value as a result of effects running)
465+
if (source.wv <= wv) {
466+
source.v = v;
467+
}
468+
}
469+
470+
batch_deriveds = null;
471+
};
472+
}
447473
}
448474

449475
/**
@@ -615,6 +641,26 @@ function flush_queued_effects(effects) {
615641
eager_block_effects = null;
616642
}
617643

644+
/**
645+
* This is similar to `mark_reactions`, but it only marks async/block effects
646+
* so that these can re-run after another batch has been committed
647+
* @param {Value} value
648+
*/
649+
function mark_effects(value) {
650+
if (value.reactions !== null) {
651+
for (const reaction of value.reactions) {
652+
const flags = reaction.f;
653+
654+
if ((flags & DERIVED) !== 0) {
655+
mark_effects(/** @type {Derived} */ (reaction));
656+
} else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0) {
657+
set_signal_status(reaction, DIRTY);
658+
schedule_effect(/** @type {Effect} */ (reaction));
659+
}
660+
}
661+
}
662+
}
663+
618664
/**
619665
* @param {Effect} signal
620666
* @returns {void}

0 commit comments

Comments
 (0)