Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dirty-cycles-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: send `$effect.pending` count to the correct boundary
4 changes: 2 additions & 2 deletions packages/svelte/src/internal/client/dom/blocks/async.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
/** @import { TemplateNode, Value } from '#client' */
import { flatten } from '../../reactivity/async.js';
import { get } from '../../runtime.js';
import { get_pending_boundary } from './boundary.js';
import { get_boundary } from './boundary.js';

/**
* @param {TemplateNode} node
* @param {Array<() => Promise<any>>} expressions
* @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn
*/
export function async(node, expressions, fn) {
var boundary = get_pending_boundary();
var boundary = get_boundary();

boundary.update_pending_count(1);

Expand Down
56 changes: 35 additions & 21 deletions packages/svelte/src/internal/client/dom/blocks/boundary.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ export function boundary(node, props, children) {
}

export class Boundary {
pending = false;

/** @type {Boundary | null} */
parent;

#pending = false;

/** @type {TemplateNode} */
#anchor;

Expand Down Expand Up @@ -81,6 +81,7 @@ export class Boundary {
/** @type {DocumentFragment | null} */
#offscreen_fragment = null;

#local_pending_count = 0;
#pending_count = 0;
#is_creating_fallback = false;

Expand All @@ -95,12 +96,12 @@ export class Boundary {

#effect_pending_update = () => {
if (this.#effect_pending) {
internal_set(this.#effect_pending, this.#pending_count);
internal_set(this.#effect_pending, this.#local_pending_count);
}
};

#effect_pending_subscriber = createSubscriber(() => {
this.#effect_pending = source(this.#pending_count);
this.#effect_pending = source(this.#local_pending_count);

if (DEV) {
tag(this.#effect_pending, '$effect.pending()');
Expand All @@ -125,7 +126,7 @@ export class Boundary {

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

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

this.#effect = block(() => {
/** @type {Effect} */ (active_effect).b = this;
Expand Down Expand Up @@ -156,7 +157,7 @@ export class Boundary {
this.#pending_effect = null;
});

this.pending = false;
this.#pending = false;
}
});
} else {
Expand All @@ -169,7 +170,7 @@ export class Boundary {
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
this.pending = false;
this.#pending = false;
}
}
}, flags);
Expand All @@ -179,6 +180,14 @@ export class Boundary {
}
}

/**
* Returns `true` 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());
}

has_pending_snippet() {
return !!this.#props.pending;
}
Expand Down Expand Up @@ -220,12 +229,24 @@ export class Boundary {
}
}

/** @param {1 | -1} d */
/**
* Updates the pending count associated with the currently visible pending snippet,
* if any, such that we can replace the snippet with content once work is done
* @param {1 | -1} d
*/
#update_pending_count(d) {
if (!this.has_pending_snippet()) {
if (this.parent) {
this.parent.#update_pending_count(d);
}

return;
}

this.#pending_count += d;

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

if (this.#pending_effect) {
pause_effect(this.#pending_effect, () => {
Expand All @@ -242,12 +263,9 @@ export class Boundary {

/** @param {1 | -1} d */
update_pending_count(d) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSDoc to not call this inside class

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

if (this.has_pending_snippet()) {
this.#update_pending_count(d);
} else if (this.parent) {
this.parent.#update_pending_count(d);
}
this.#update_pending_count(d);

this.#local_pending_count += d;
effect_pending_updates.add(this.#effect_pending_update);
}

Expand Down Expand Up @@ -308,7 +326,7 @@ export class Boundary {
});
}

this.pending = true;
this.#pending = true;

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

Expand Down Expand Up @@ -384,13 +402,9 @@ function move_effect(effect, fragment) {
}
}

export function get_pending_boundary() {
export function get_boundary() {
var boundary = /** @type {Effect} */ (active_effect).b;

while (boundary !== null && !boundary.has_pending_snippet()) {
boundary = boundary.parent;
}

if (boundary === null) {
e.await_outside_boundary();
}
Expand Down
4 changes: 2 additions & 2 deletions packages/svelte/src/internal/client/reactivity/async.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { DESTROYED } from '#client/constants';
import { DEV } from 'esm-env';
import { component_context, is_runes, set_component_context } from '../context.js';
import { get_pending_boundary } from '../dom/blocks/boundary.js';
import { get_boundary } from '../dom/blocks/boundary.js';
import { invoke_error_boundary } from '../error-handling.js';
import {
active_effect,
Expand Down Expand Up @@ -39,7 +39,7 @@ export function flatten(sync, async, fn) {
var parent = /** @type {Effect} */ (active_effect);

var restore = capture();
var boundary = get_pending_boundary();
var boundary = get_boundary();

Promise.all(async.map((expression) => async_derived(expression)))
.then((result) => {
Expand Down
11 changes: 7 additions & 4 deletions packages/svelte/src/internal/client/reactivity/batch.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js';
import { get_pending_boundary } from '../dom/blocks/boundary.js';
import { get_boundary } from '../dom/blocks/boundary.js';
import {
active_effect,
is_dirty,
Expand Down Expand Up @@ -298,7 +298,10 @@ export class Batch {
this.#render_effects.push(effect);
} else if ((flags & CLEAN) === 0) {
if ((flags & ASYNC) !== 0) {
var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects;
var effects = effect.b?.is_pending()
? this.#boundary_async_effects
: this.#async_effects;

effects.push(effect);
} else if (is_dirty(effect)) {
if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect);
Expand Down Expand Up @@ -668,9 +671,9 @@ export function schedule_effect(signal) {
}

export function suspend() {
var boundary = get_pending_boundary();
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var pending = boundary.pending;
var pending = boundary.is_pending();

boundary.update_pending_count(1);
if (!pending) batch.increment();
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/reactivity/deriveds.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export function async_derived(fn, location) {
prev = promise;

var batch = /** @type {Batch} */ (current_batch);
var pending = boundary.pending;
var pending = boundary.is_pending();

if (should_suspend) {
boundary.update_pending_count(1);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { tick } from 'svelte';
import { test } from '../../test';

export default test({
async test({ assert, target }) {
const [increment, shift] = target.querySelectorAll('button');

assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>shift</button>
<p>loading...</p>
`
);

shift.click();
shift.click();
shift.click();

await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>shift</button>
<p>0</p>
<p>0</p>
<p>0</p>
<p>inner pending: 0</p>
<p>outer pending: 0</p>
`
);

increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>shift</button>
<p>0</p>
<p>0</p>
<p>0</p>
<p>inner pending: 3</p>
<p>outer pending: 0</p>
`
);

shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>shift</button>
<p>0</p>
<p>0</p>
<p>0</p>
<p>inner pending: 2</p>
<p>outer pending: 0</p>
`
);

shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>shift</button>
<p>0</p>
<p>0</p>
<p>0</p>
<p>inner pending: 1</p>
<p>outer pending: 0</p>
`
);

shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>shift</button>
<p>1</p>
<p>1</p>
<p>1</p>
<p>inner pending: 0</p>
<p>outer pending: 0</p>
`
);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script>
let value = $state(0);
let deferreds = [];

function push(value) {
const deferred = Promise.withResolvers();
deferreds.push({ value, deferred });
return deferred.promise;
}

function shift() {
const d = deferreds.shift();
d?.deferred.resolve(d.value);
}
</script>

<button onclick={() => value++}>increment</button>
<button onclick={() => shift()}>shift</button>

<svelte:boundary>
<svelte:boundary>
<p>{await push(value)}</p>
<p>{await push(value)}</p>
<p>{await push(value)}</p>
<p>inner pending: {$effect.pending()}</p>
</svelte:boundary>
<p>outer pending: {$effect.pending()}</p>

{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>


Loading