From a793d914ba306a8ca11558882c80fd7ecf3b7ccb Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 7 Nov 2025 22:45:52 +0100 Subject: [PATCH 1/3] restore each context upon commit - prevents `run` getting undefined boundaries --- packages/svelte/src/internal/client/dom/blocks/each.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index a0fae3713305..04a6ceba6b15 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -42,6 +42,7 @@ import { active_effect, get } from '../../runtime.js'; import { DEV } from 'esm-env'; import { derived_safe_equal } from '../../reactivity/deriveds.js'; import { current_batch } from '../../reactivity/batch.js'; +import { capture } from '../../reactivity/async.js'; /** * The row of a keyed each block that is currently updating. We track this @@ -159,7 +160,14 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f /** @type {Effect} */ var each_effect; + /** @type {ReturnType} */ + var restore; + function commit() { + // If async work was pending commit could happen long after the block has ran. + // Restore so batch etc are created in its correct parent context. + restore?.(false); + reconcile( each_effect, array, @@ -188,6 +196,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f } block(() => { + restore ??= capture(); // store a reference to the effect so that we can update the start/end nodes in reconciliation each_effect ??= /** @type {Effect} */ (active_effect); From 51bd24ea5c82af4def9ee8e4824e23604db9079c Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 7 Nov 2025 22:45:58 +0100 Subject: [PATCH 2/3] split effect collection and execution when rebasing - prevents each loops breaking in async when certain race conditions occur --- .../src/internal/client/reactivity/batch.js | 60 +++++++++++++------ 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 57aa185a31db..ad147a992bbd 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -348,15 +348,13 @@ export class Batch { var previous_batch_values = batch_values; var is_earlier = true; - /** @type {EffectTarget} */ - var dummy_target = { - parent: null, - effect: null, - effects: [], - render_effects: [], - block_effects: [] - }; + /** @type {Map>} */ + const batch_effects = new Map(); + // First loop: collect effects to run for each batch + // Do this before running them because rerunning an effect + // might change its dependencies and so other batches could + // run effects when they shouldn't or not when they should. for (const batch of batches) { if (batch === this) { is_earlier = false; @@ -388,18 +386,43 @@ export class Batch { // Re-run async/block effects that depend on distinct values changed in both batches const others = [...batch.current.keys()].filter((s) => !this.current.has(s)); if (others.length > 0) { + /** @type {Set} */ + const effects = new Set(); /** @type {Set} */ const marked = new Set(); /** @type {Map} */ const checked = new Map(); for (const source of sources) { - mark_effects(source, others, marked, checked); + mark_effects(source, others, effects, marked, checked); } - if (queued_root_effects.length > 0) { - current_batch = batch; - batch.apply(); + if (effects.size > 0) { + batch_effects.set(batch, effects); + } + } + } + // Second loop: schedule effects and traverse effect trees + if (batch_effects.size > 0) { + /** @type {EffectTarget} */ + var dummy_target = { + parent: null, + effect: null, + effects: [], + render_effects: [], + block_effects: [] + }; + + for (const [batch, effects] of batch_effects) { + current_batch = batch; + batch.apply(); + + for (const effect of effects) { + set_signal_status(effect, DIRTY); + schedule_effect(effect); + } + + if (queued_root_effects.length > 0) { for (const root of queued_root_effects) { batch.#traverse_effect_tree(root, dummy_target); } @@ -407,8 +430,9 @@ export class Batch { // TODO do we need to do anything with `target`? defer block effects? queued_root_effects = []; - batch.deactivate(); } + + batch.deactivate(); } } @@ -710,10 +734,11 @@ function flush_queued_effects(effects) { * these effects can re-run after another batch has been committed * @param {Value} value * @param {Source[]} sources + * @param {Set} effects * @param {Set} marked * @param {Map} checked */ -function mark_effects(value, sources, marked, checked) { +function mark_effects(value, sources, effects, marked, checked) { if (marked.has(value)) return; marked.add(value); @@ -722,14 +747,13 @@ function mark_effects(value, sources, marked, checked) { const flags = reaction.f; if ((flags & DERIVED) !== 0) { - mark_effects(/** @type {Derived} */ (reaction), sources, marked, checked); + mark_effects(/** @type {Derived} */ (reaction), sources, effects, marked, checked); } else if ( (flags & (ASYNC | BLOCK_EFFECT)) !== 0 && - (flags & DIRTY) === 0 && // we may have scheduled this one already + !effects.has(/** @type {Effect} */ (reaction)) && depends_on(reaction, sources, checked) ) { - set_signal_status(reaction, DIRTY); - schedule_effect(/** @type {Effect} */ (reaction)); + effects.add(/** @type {Effect} */ (reaction)); } } } From 34e0ac43b808e50bacf0e6d866159803245834d8 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 7 Nov 2025 23:21:55 +0100 Subject: [PATCH 3/3] almost-fix --- packages/svelte/src/internal/client/dom/blocks/each.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 04a6ceba6b15..4d92edbae3ca 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -161,12 +161,14 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f var each_effect; /** @type {ReturnType} */ - var restore; + var restore_block_context; function commit() { // If async work was pending commit could happen long after the block has ran. // Restore so batch etc are created in its correct parent context. - restore?.(false); + // After that we gotta go back to where we were. + var restore_current = capture(); + restore_block_context?.(false); reconcile( each_effect, @@ -193,10 +195,12 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f }); } } + + restore_current(); } block(() => { - restore ??= capture(); + restore_block_context ??= capture(); // store a reference to the effect so that we can update the start/end nodes in reconciliation each_effect ??= /** @type {Effect} */ (active_effect);