From c80d165032f976879746db7e5dbbdccb7987e7fd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 25 Jun 2025 19:29:22 -0400 Subject: [PATCH 1/3] local effect pending --- .../client/visitors/CallExpression.js | 2 +- .../src/internal/client/dom/blocks/async.js | 4 +- .../internal/client/dom/blocks/boundary.js | 49 ++++++++++++++----- packages/svelte/src/internal/client/index.js | 12 +---- .../src/internal/client/reactivity/batch.js | 13 ----- .../internal/client/reactivity/deriveds.js | 10 ++-- .../src/internal/client/reactivity/sources.js | 3 -- 7 files changed, 49 insertions(+), 44 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index 532af08fd1..af3553861c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -64,7 +64,7 @@ export function CallExpression(node, context) { ); case '$effect.pending': - return b.call('$.get', b.id('$.pending')); + return b.call(b.id('$.pending')); case '$inspect': case '$inspect().with': diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 4e9f7c2b93..339c05ded7 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -17,7 +17,7 @@ export async function async(node, expressions, fn) { var restore = capture(); var boundary = get_pending_boundary(); - boundary.increment(); + boundary.update_pending_count(1); try { const result = await Promise.all(expressions.map((fn) => async_derived(fn))); @@ -29,6 +29,6 @@ export async function async(node, expressions, fn) { } catch (error) { boundary.error(error); } finally { - boundary.decrement(); + boundary.update_pending_count(-1); } } diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index a6dfc46057..8cf54490f4 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -7,6 +7,7 @@ import { block, branch, destroy_effect, pause_effect } from '../../reactivity/ef import { active_effect, active_reaction, + get, set_active_effect, set_active_reaction } from '../../runtime.js'; @@ -24,6 +25,8 @@ import * as e from '../../../shared/errors.js'; import { DEV } from 'esm-env'; import { from_async_derived, set_from_async_derived } from '../../reactivity/deriveds.js'; import { Batch } from '../../reactivity/batch.js'; +import { source, update } from '../../reactivity/sources.js'; +import { tag } from '../../dev/tracing.js'; /** * @typedef {{ @@ -82,6 +85,8 @@ export class Boundary { #pending_count = 0; #is_creating_fallback = false; + effect_pending = source(0); + /** * @param {TemplateNode} node * @param {BoundaryProps} props @@ -98,6 +103,10 @@ export class Boundary { this.pending = !!this.#props.pending; + if (DEV) { + tag(this.effect_pending, '$effect.pending()'); + } + this.#effect = block(() => { /** @type {Effect} */ (active_effect).b = this; @@ -210,19 +219,26 @@ export class Boundary { } } - increment() { - this.#pending_count++; - } + /** @param {1 | -1} d */ + #update_pending_count(d) { + this.#pending_count += d; - decrement() { - if (--this.#pending_count === 0) { + if (this.#pending_count === 0) { this.commit(); + } + } - if (this.#main_effect !== null) { - // TODO do we also need to `resume_effect` here? - // schedule_effect(this.#main_effect); - } + /** @param {1 | -1} d */ + update_pending_count(d) { + if (this.has_pending_snippet()) { + this.#update_pending_count(d); + } else if (this.parent) { + this.parent.#update_pending_count(d); } + + queueMicrotask(() => { + update(this.effect_pending, d); + }); } /** @param {unknown} error */ @@ -373,10 +389,10 @@ export function capture(track = true) { export function suspend() { let boundary = get_pending_boundary(); - boundary.increment(); + boundary.update_pending_count(-1); return function unsuspend() { - boundary.decrement(); + boundary.update_pending_count(-1); }; } @@ -401,3 +417,14 @@ function exit() { set_active_reaction(null); set_component_context(null); } + +export function pending() { + // TODO throw helpful error if called outside an effect + const boundary = /** @type {Effect} */ (active_effect).b; + + if (boundary === null) { + return 0; // TODO eventually we will need this to be global + } + + return get(boundary.effect_pending); +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index c300f00b3d..8f3b86536d 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -115,15 +115,7 @@ export { user_effect, user_pre_effect } from './reactivity/effects.js'; -export { - mutable_source, - mutate, - pending, - set, - state, - update, - update_pre -} from './reactivity/sources.js'; +export { mutable_source, mutate, set, state, update, update_pre } from './reactivity/sources.js'; export { prop, rest_props, @@ -143,7 +135,7 @@ export { update_store, mark_store_binding } from './reactivity/store.js'; -export { boundary, save, suspend } from './dom/blocks/boundary.js'; +export { boundary, pending, save, suspend } from './dom/blocks/boundary.js'; export { set_text } from './render.js'; export { get, diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index d48d225a56..02ae6f8e27 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -10,8 +10,6 @@ import { set_signal_status, update_effect } from '../runtime.js'; -import { raf } from '../timing.js'; -import { internal_set, pending } from './sources.js'; /** @type {Set} */ const batches = new Set(); @@ -19,11 +17,6 @@ const batches = new Set(); /** @type {Batch | null} */ export let current_batch = null; -/** Update `$effect.pending()` */ -function update_pending() { - internal_set(pending, batches.size > 0); -} - /** @type {Map | null} */ export let batch_deriveds = null; @@ -239,8 +232,6 @@ export class Batch { } this.#callbacks.clear(); - - raf.tick(update_pending); } increment() { @@ -295,10 +286,6 @@ export class Batch { static ensure() { if (current_batch === null) { - if (batches.size === 0) { - raf.tick(update_pending); - } - const batch = (current_batch = new Batch()); batches.add(current_batch); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 6a7a6bd240..92fd7b18e6 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -31,7 +31,7 @@ import { destroy_effect, render_effect } from './effects.js'; import { inspect_effects, internal_set, set_inspect_effects, source } from './sources.js'; import { get_stack } from '../dev/tracing.js'; import { tracing_mode_flag } from '../../flags/index.js'; -import { get_pending_boundary } from '../dom/blocks/boundary.js'; +import { Boundary, get_pending_boundary } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; import { current_batch } from './batch.js'; @@ -105,7 +105,7 @@ export function async_derived(fn, location) { throw new Error('TODO cannot create unowned async derived'); } - let boundary = get_pending_boundary(); + var boundary = /** @type {Boundary} */ (parent.b); var promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined)); var signal = source(/** @type {V} */ (UNINITIALIZED)); @@ -135,7 +135,8 @@ export function async_derived(fn, location) { var pending = boundary.pending; if (should_suspend) { - (pending ? boundary : batch).increment(); + boundary.update_pending_count(1); + if (!pending) batch.increment(); } /** @@ -148,7 +149,8 @@ export function async_derived(fn, location) { from_async_derived = null; if (should_suspend) { - (pending ? boundary : batch).decrement(); + boundary.update_pending_count(-1); + if (!pending) batch.decrement(); } if (!pending) batch.restore(); diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 2300baed91..48c8ecf575 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -43,9 +43,6 @@ export let inspect_effects = new Set(); /** @type {Map} */ export const old_values = new Map(); -/** Internal representation of `$effect.pending()` */ -export let pending = source(false); - /** * @param {Set} v */ From 8cd5635c88d24222d009e555d1047ad5e13f67f1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 25 Jun 2025 19:42:42 -0400 Subject: [PATCH 2/3] fix --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 8cf54490f4..e24d76800e 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -389,7 +389,7 @@ export function capture(track = true) { export function suspend() { let boundary = get_pending_boundary(); - boundary.update_pending_count(-1); + boundary.update_pending_count(1); return function unsuspend() { boundary.update_pending_count(-1); From c04a13b8d300293147d788e4a1e64931c67f687d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 25 Jun 2025 22:26:46 -0400 Subject: [PATCH 3/3] fix weird bug in tests --- .../svelte/src/internal/client/reactivity/batch.js | 10 ++++++++++ packages/svelte/tests/runtime-legacy/shared.ts | 3 +++ 2 files changed, 13 insertions(+) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 02ae6f8e27..e51fab23f9 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -302,3 +302,13 @@ export class Batch { return current_batch; } } + +/** + * Forcibly remove all current batches + * TODO investigate why we need this in tests + */ +export function clear() { + for (const batch of batches) { + batch.remove(); + } +} diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 25e89e7db8..4ccd602afc 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -11,6 +11,7 @@ import { assert_html_equal, assert_html_equal_with_options } from '../html_equal import { raf } from '../animation-helpers.js'; import type { CompileOptions } from '#compiler'; import { suite_with_variants, type BaseTest } from '../suite.js'; +import { clear } from '../../src/internal/client/reactivity/batch.js'; type Assert = typeof import('vitest').assert & { htmlEqual(a: string, b: string, description?: string): void; @@ -521,6 +522,8 @@ async function run_test_variant( console.log = console_log; console.warn = console_warn; console.error = console_error; + + clear(); } }