Skip to content
5 changes: 5 additions & 0 deletions .changeset/tall-clocks-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: clear batch between runs
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/dom/blocks/async.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { get_boundary } from './boundary.js';
export function async(node, blockers = [], expressions = [], fn) {
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var blocking = !boundary.is_pending();
var blocking = boundary.is_rendered();

boundary.update_pending_count(1);
batch.increment(blocking);
Expand Down
65 changes: 50 additions & 15 deletions packages/svelte/src/internal/client/dom/blocks/boundary.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
import {
BOUNDARY_EFFECT,
COMMENT_NODE,
DIRTY,
EFFECT_PRESERVED,
EFFECT_TRANSPARENT
EFFECT_TRANSPARENT,
MAYBE_DIRTY
} from '#client/constants';
import { HYDRATION_START_ELSE } from '../../../../constants.js';
import { component_context, set_component_context } from '../../context.js';
Expand All @@ -20,7 +22,8 @@ import {
active_reaction,
get,
set_active_effect,
set_active_reaction
set_active_reaction,
set_signal_status
} from '../../runtime.js';
import {
hydrate_next,
Expand All @@ -34,11 +37,12 @@ import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { DEV } from 'esm-env';
import { Batch } from '../../reactivity/batch.js';
import { Batch, schedule_effect } from '../../reactivity/batch.js';
import { internal_set, source } from '../../reactivity/sources.js';
import { tag } from '../../dev/tracing.js';
import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
import { create_text } from '../operations.js';
import { defer_effect } from '../../reactivity/utils.js';

/**
* @typedef {{
Expand All @@ -64,7 +68,7 @@ export class Boundary {
/** @type {Boundary | null} */
parent;

#pending = false;
is_pending = false;

/** @type {TemplateNode} */
#anchor;
Expand Down Expand Up @@ -101,6 +105,12 @@ export class Boundary {

#is_creating_fallback = false;

/** @type {Set<Effect>} */
#dirty_effects = new Set();

/** @type {Set<Effect>} */
#maybe_dirty_effects = new Set();

/**
* A source containing the number of pending async deriveds/expressions.
* Only created if `$effect.pending()` is used inside the boundary,
Expand Down Expand Up @@ -134,7 +144,7 @@ export class Boundary {

this.parent = /** @type {Effect} */ (active_effect).b;

this.#pending = !!this.#props.pending;
this.is_pending = !!this.#props.pending;

this.#effect = block(() => {
/** @type {Effect} */ (active_effect).b = this;
Expand Down Expand Up @@ -164,7 +174,7 @@ export class Boundary {
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
this.#pending = false;
this.is_pending = false;
}
}

Expand All @@ -187,7 +197,7 @@ export class Boundary {

// Since server rendered resolved content, we never show pending state
// Even if client-side async operations are still running, the content is already displayed
this.#pending = false;
this.is_pending = false;
}

#hydrate_pending_content() {
Expand All @@ -212,15 +222,15 @@ export class Boundary {
this.#pending_effect = null;
});

this.#pending = false;
this.is_pending = false;
}
});
}

#get_anchor() {
var anchor = this.#anchor;

if (this.#pending) {
if (this.is_pending) {
this.#pending_anchor = create_text();
this.#anchor.before(this.#pending_anchor);

Expand All @@ -231,11 +241,19 @@ export class Boundary {
}

/**
* Returns `true` if the effect exists inside a boundary whose pending snippet is shown
* Defer an effect inside a pending boundary until the boundary resolves
* @param {Effect} effect
*/
defer_effect(effect) {
defer_effect(effect, this.#dirty_effects, this.#maybe_dirty_effects);
}

/**
* Returns `false` if the effect exists inside a boundary whose pending snippet is shown
* @returns {boolean}
*/
is_pending() {
return this.#pending || (!!this.parent && this.parent.is_pending());
is_rendered() {
return !this.is_pending && (!this.parent || this.parent.is_rendered());
}

has_pending_snippet() {
Expand Down Expand Up @@ -298,7 +316,24 @@ export class Boundary {
this.#pending_count += d;

if (this.#pending_count === 0) {
this.#pending = false;
this.is_pending = false;

// any effects that were encountered and deferred during traversal
// should be rescheduled — after the next traversal (which will happen
// immediately, due to the same update that brought us here)
// the effects will be flushed
for (const e of this.#dirty_effects) {
set_signal_status(e, DIRTY);
schedule_effect(e);
}

for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
schedule_effect(e);
}

this.#dirty_effects.clear();
this.#maybe_dirty_effects.clear();

if (this.#pending_effect) {
pause_effect(this.#pending_effect, () => {
Expand Down Expand Up @@ -394,7 +429,7 @@ export class Boundary {

// we intentionally do not try to find the nearest pending boundary. If this boundary has one, we'll render it on reset
// but it would be really weird to show the parent's boundary on a child reset.
this.#pending = this.has_pending_snippet();
this.is_pending = this.has_pending_snippet();

this.#main_effect = this.#run(() => {
this.#is_creating_fallback = false;
Expand All @@ -404,7 +439,7 @@ export class Boundary {
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
this.#pending = false;
this.is_pending = false;
}
};

Expand Down
12 changes: 4 additions & 8 deletions packages/svelte/src/internal/client/reactivity/async.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ export function run(thunks) {

var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var blocking = !boundary.is_pending();
var blocking = boundary.is_rendered();

boundary.update_pending_count(1);
batch.increment(blocking);
Expand Down Expand Up @@ -252,17 +252,13 @@ export function run(thunks) {
throw STALE_REACTION;
}

try {
restore();
return fn();
} finally {
// TODO do we need it here as well as below?
unset_context();
}
restore();
return fn();
})
.catch(handle_error)
.finally(() => {
unset_context();
current_batch?.deactivate();
});

promises.push(promise);
Expand Down
Loading
Loading