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..e24d76800e 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..e51fab23f9 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); @@ -315,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/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 */ 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(); } }