diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index ca6437e384..039eded21b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -87,7 +87,7 @@ export class Boundary { /** @type {DocumentFragment | null} */ #offscreen_fragment = null; - #local_pending_count = 0; + local_pending_count = 0; #pending_count = 0; #is_creating_fallback = false; @@ -103,12 +103,12 @@ export class Boundary { #effect_pending_update = () => { if (this.#effect_pending) { - internal_set(this.#effect_pending, this.#local_pending_count); + internal_set(this.#effect_pending, this.local_pending_count); } }; #effect_pending_subscriber = createSubscriber(() => { - this.#effect_pending = source(this.#local_pending_count); + this.#effect_pending = source(this.local_pending_count); if (DEV) { tag(this.#effect_pending, '$effect.pending()'); @@ -285,13 +285,6 @@ export class Boundary { this.#anchor.before(this.#offscreen_fragment); this.#offscreen_fragment = null; } - - // TODO this feels like a little bit of a kludge, but until we - // overhaul the boundary/batch relationship it's probably - // the most pragmatic solution available to us - queue_micro_task(() => { - Batch.ensure().flush(); - }); } } @@ -304,7 +297,7 @@ export class Boundary { update_pending_count(d) { this.#update_pending_count(d); - this.#local_pending_count += d; + this.local_pending_count += d; effect_pending_updates.add(this.#effect_pending_update); } @@ -363,7 +356,7 @@ export class Boundary { // If the failure happened while flushing effects, current_batch can be null Batch.ensure(); - this.#local_pending_count = 0; + this.local_pending_count = 0; if (this.#failed_effect !== null) { pause_effect(this.#failed_effect, () => { diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 45c78ff926..b1e8177949 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -202,10 +202,9 @@ export function unset_context() { export async function async_body(fn) { var boundary = get_boundary(); var batch = /** @type {Batch} */ (current_batch); - var pending = boundary.is_pending(); boundary.update_pending_count(1); - if (!pending) batch.increment(); + batch.increment(); var active = /** @type {Effect} */ (active_effect); @@ -238,12 +237,7 @@ export async function async_body(fn) { } boundary.update_pending_count(-1); - - if (pending) { - batch.flush(); - } else { - batch.decrement(); - } + batch.decrement(); unset_context(); } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index fd2a6d9f5d..e76c92119c 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -11,7 +11,8 @@ import { RENDER_EFFECT, ROOT_EFFECT, MAYBE_DIRTY, - DERIVED + DERIVED, + BOUNDARY_EFFECT } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property } from '../../shared/utils.js'; @@ -30,6 +31,16 @@ import { invoke_error_boundary } from '../error-handling.js'; import { old_values } from './sources.js'; import { unlink_effect } from './effects.js'; +/** + * @typedef {{ + * parent: EffectTarget | null; + * effect: Effect | null; + * effects: Effect[]; + * render_effects: Effect[]; + * block_effects: Effect[]; + * }} EffectTarget + */ + /** @type {Set} */ const batches = new Set(); @@ -97,26 +108,6 @@ export class Batch { */ #deferred = null; - /** - * Template effects and `$effect.pre` effects, which run when - * a batch is committed - * @type {Effect[]} - */ - #render_effects = []; - - /** - * The same as `#render_effects`, but for `$effect` (which runs after) - * @type {Effect[]} - */ - #effects = []; - - /** - * Block effects, which may need to re-run on subsequent flushes - * in order to update internal sources (e.g. each block items) - * @type {Effect[]} - */ - #block_effects = []; - /** * Deferred effects (which run after async work has completed) that are DIRTY * @type {Effect[]} @@ -155,33 +146,26 @@ export class Batch { if (this.#pending === 0) { // TODO we need this because we commit _then_ flush effects... // maybe there's a way we can reverse the order? - var previous_batch_sources = batch_values; + // var previous_batch_sources = batch_values; this.#commit(); - var render_effects = this.#render_effects; - var effects = this.#effects; - - this.#render_effects = []; - this.#effects = []; - this.#block_effects = []; - // If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with // newly updated sources, which could lead to infinite loops when effects run over and over again. previous_batch = this; current_batch = null; - batch_values = previous_batch_sources; - flush_queued_effects(render_effects); - flush_queued_effects(effects); + // batch_values = previous_batch_sources; + // flush_queued_effects(target.render_effects); + // flush_queued_effects(target.effects); previous_batch = null; this.#deferred?.resolve(); } else { - this.#defer_effects(this.#render_effects); - this.#defer_effects(this.#effects); - this.#defer_effects(this.#block_effects); + // this.#defer_effects(target.render_effects); + // this.#defer_effects(target.effects); + // this.#defer_effects(target.block_effects); } batch_values = null; @@ -195,6 +179,17 @@ export class Batch { #traverse_effect_tree(root) { root.f ^= CLEAN; + var should_defer = false; + + /** @type {EffectTarget} */ + var target = { + parent: null, + effect: null, + effects: [], + render_effects: [], + block_effects: [] + }; + var effect = root.first; while (effect !== null) { @@ -204,15 +199,25 @@ export class Batch { var skip = is_skippable_branch || (flags & INERT) !== 0 || this.skipped_effects.has(effect); + if ((effect.f & BOUNDARY_EFFECT) !== 0 && effect.b?.is_pending()) { + target = { + parent: target, + effect, + effects: [], + render_effects: [], + block_effects: [] + }; + } + if (!skip && effect.fn !== null) { if (is_branch) { effect.f ^= CLEAN; } else if ((flags & EFFECT) !== 0) { - this.#effects.push(effect); + target.effects.push(effect); } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { - this.#render_effects.push(effect); + target.render_effects.push(effect); } else if (is_dirty(effect)) { - if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect); + if ((effect.f & BLOCK_EFFECT) !== 0) target.block_effects.push(effect); update_effect(effect); } @@ -228,10 +233,41 @@ export class Batch { effect = effect.next; while (effect === null && parent !== null) { + if (parent.b !== null) { + var ready = parent.b.local_pending_count === 0; + + if (target.parent === null) { + should_defer ||= !ready; + } else if (parent === target.effect) { + if (ready) { + // TODO can this happen? + target.parent.effects.push(...target.effects); + target.parent.render_effects.push(...target.render_effects); + target.parent.block_effects.push(...target.block_effects); + } else { + this.#defer_effects(target.effects); + this.#defer_effects(target.render_effects); + this.#defer_effects(target.block_effects); + } + + target = /** @type {EffectTarget} */ (target.parent); + } + } + effect = parent.next; parent = parent.parent; } } + + if (should_defer) { + this.#defer_effects(target.effects); + this.#defer_effects(target.render_effects); + this.#defer_effects(target.block_effects); + } else { + // TODO append/detach blocks here as well + flush_queued_effects(target.render_effects); + flush_queued_effects(target.effects); + } } /** @@ -245,8 +281,6 @@ export class Batch { // mark as clean so they get scheduled if they depend on pending async state set_signal_status(e, CLEAN); } - - effects.length = 0; } /** diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 6aa9a1d9d9..e9d5edca05 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -136,17 +136,14 @@ export function async_derived(fn, location) { if (DEV) current_async_effect = null; var batch = /** @type {Batch} */ (current_batch); - var pending = boundary.is_pending(); if (should_suspend) { boundary.update_pending_count(1); - if (!pending) { - batch.increment(); + batch.increment(); - deferreds.get(batch)?.reject(STALE_REACTION); - deferreds.delete(batch); // delete to ensure correct order in Map iteration below - deferreds.set(batch, d); - } + deferreds.get(batch)?.reject(STALE_REACTION); + deferreds.delete(batch); // delete to ensure correct order in Map iteration below + deferreds.set(batch, d); } /** @@ -156,7 +153,7 @@ export function async_derived(fn, location) { const handler = (value, error = undefined) => { current_async_effect = null; - if (!pending) batch.activate(); + batch.activate(); if (error) { if (error !== STALE_REACTION) { @@ -193,7 +190,7 @@ export function async_derived(fn, location) { if (should_suspend) { boundary.update_pending_count(-1); - if (!pending) batch.decrement(); + batch.decrement(); } };