diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index df96f4899b..e81e68354f 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -2,6 +2,7 @@ export const DERIVED = 1 << 1; export const EFFECT = 1 << 2; export const RENDER_EFFECT = 1 << 3; +export const TEMPLATE_EFFECT = 1 << 26; /** * An effect that does not destroy its child effects when it reruns. * Runs as part of render effects, i.e. not eagerly as part of tree traversal or effect flushing. diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 82192c7687..faa7277644 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -100,6 +100,12 @@ export class Batch { // for debugging. TODO remove once async is stable id = uid++; + /** + * @type {Batch | null} + * If this batch is merged into another batch, successor points to the batch it was merged into. + */ + successor = null; + /** * The current values of any sources that are updated in this batch * They keys of this map are identical to `this.#previous` @@ -389,6 +395,11 @@ export class Batch { } flush() { + if (this.successor) { + this.successor.flush(); + return; + } + var source_stacks = DEV ? new Set() : null; try { @@ -554,6 +565,103 @@ export class Batch { return (this.#deferred ??= deferred()).promise; } + /** + * Ensure there is a current batch for scheduling work triggered by `reaction`. + * If both the current batch and the reaction batch are active, merge them. + * @param {Reaction} reaction + */ + static upsert(reaction) { + var reaction_batch = reaction.batch; + var has_reaction_batch = reaction_batch !== null && !reaction_batch.finished(); + + if (current_batch === null) { + if (has_reaction_batch) { + current_batch = reaction_batch; + return reaction_batch; + } + + return Batch.ensure(); + } + + var batch = current_batch; + + if (!has_reaction_batch || reaction_batch === batch) { + return batch; + } + + // Fork batches are isolated speculative environments and should not entangle. + if (batch.is_fork || /** @type {Batch} */ (reaction_batch).is_fork) { + return batch; + } + + Batch.merge(batch, /** @type {Batch} */ (reaction_batch)); + current_batch = reaction_batch; + return reaction_batch; + } + + /** + * Merge `from` into `to` and retire `from`. + * @param {Batch} from + * @param {Batch} to + */ + static merge(from, to) { + if (from === to) return; + + for (const [source, value] of from.previous) { + if (!to.previous.has(source)) { + to.previous.set(source, value); + } + } + + for (const [source, value] of from.current) { + to.current.set(source, value); + if (!to.is_fork) source.batch = to; + } + + to.#pending += from.#pending; + to.#blocking_pending += from.#blocking_pending; + + to.#roots.push(...from.#roots); + + for (const e of from.#dirty_effects) to.#dirty_effects.add(e); + for (const e of from.#maybe_dirty_effects) to.#maybe_dirty_effects.add(e); + + for (const fn of from.#commit_callbacks) to.#commit_callbacks.add(fn); + for (const fn of from.#discard_callbacks) to.#discard_callbacks.add(fn); + + for (const [effect, tracked] of from.#skipped_branches) { + var existing = to.#skipped_branches.get(effect); + if (existing) { + existing.d.push(...tracked.d); + existing.m.push(...tracked.m); + } else { + to.#skipped_branches.set(effect, tracked); + } + } + + if (from.#deferred !== null) { + var deferred = from.#deferred; + to.settled().then(deferred.resolve, deferred.reject); + } + + from.current.clear(); + from.previous.clear(); + from.#roots = []; + from.#dirty_effects.clear(); + from.#maybe_dirty_effects.clear(); + from.#commit_callbacks.clear(); + from.#discard_callbacks.clear(); + from.#skipped_branches.clear(); + from.#pending = 0; + from.#blocking_pending = 0; + from.#deferred = null; + from.#decrement_queued = false; + from.successor = to; + + batches.delete(from); + batches.add(to); + } + static ensure() { if (current_batch === null) { const batch = (current_batch = new Batch()); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index bc1555769f..2ebc5689a7 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -35,7 +35,8 @@ import { ASYNC, CONNECTED, MANAGED_EFFECT, - DESTROYING + DESTROYING, + TEMPLATE_EFFECT } from '#client/constants'; import * as e from '../errors.js'; import { DEV } from 'esm-env'; @@ -383,7 +384,7 @@ export function render_effect(fn, flags = 0) { */ export function template_effect(fn, sync = [], async = [], blockers = []) { flatten(blockers, sync, async, (values) => { - create_effect(RENDER_EFFECT, () => fn(...values.map(get))); + create_effect(RENDER_EFFECT | TEMPLATE_EFFECT, () => fn(...values.map(get))); }); } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 238052c9c9..7ce9598276 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -27,7 +27,10 @@ import { ROOT_EFFECT, ASYNC, WAS_MARKED, - CONNECTED + CONNECTED, + RENDER_EFFECT, + USER_EFFECT, + TEMPLATE_EFFECT } from '#client/constants'; import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; @@ -38,10 +41,8 @@ import { component_context, is_runes } from '../context.js'; import { Batch, batch_values, - current_batch, eager_block_effects, schedule_effect, - set_current_batch, legacy_updates } from './batch.js'; import { proxy } from '../proxy.js'; @@ -342,11 +343,8 @@ function mark_reactions(signal, status, updated_during_traversal) { for (var i = 0; i < length; i++) { var reaction = reactions[i]; var flags = reaction.f; - var reaction_batch = reaction.batch; - if (current_batch === null && reaction_batch !== null && !reaction_batch.finished()) { - set_current_batch(reaction_batch); - } + reaction.batch = Batch.upsert(reaction); // In legacy mode, skip the current effect to prevent infinite loops if (!runes && reaction === active_effect) continue;