diff --git a/.changeset/red-spies-mix.md b/.changeset/red-spies-mix.md new file mode 100644 index 000000000000..da958ef0dbcd --- /dev/null +++ b/.changeset/red-spies-mix.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: run deferred effects in async components diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 706d2b4e1042..7979df5705f2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -355,7 +355,11 @@ export function client_component(analysis, options) { component_returned_object.push(b.spread(b.call(b.id('$.legacy_api')))); } - const push_args = [b.id('$$props'), b.literal(analysis.runes)]; + const push_args = [ + b.id('$$props'), + b.literal(analysis.runes), + b.literal(analysis.instance.has_await) + ]; if (dev) push_args.push(b.id(analysis.name)); let component_block = b.block([ @@ -368,7 +372,8 @@ export function client_component(analysis, options) { dev || analysis.needs_context || analysis.reactive_statements.size > 0 || - component_returned_object.length > 0; + component_returned_object.length > 0 || + analysis.instance.has_await; if (analysis.instance.has_await) { if (should_inject_context && component_returned_object.length > 0) { @@ -385,7 +390,7 @@ export function client_component(analysis, options) { .../** @type {ESTree.Statement[]} */ (template.body) ]); - component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true)))); + component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true), b.true))); } else { component_block.body.push( ...state.instance_level_snippets, @@ -450,15 +455,15 @@ export function client_component(analysis, options) { // we want the cleanup function for the stores to run as the very last thing // so that it can effectively clean up the store subscription even after the user effects runs if (should_inject_context) { - component_block.body.unshift(b.stmt(b.call('$.push', ...push_args))); + component_block.body.unshift(b.var('$$component', b.call('$.push', ...push_args))); let to_push; if (component_returned_object.length > 0) { - let pop_call = b.call('$.pop', b.id('$$exports')); + let pop_call = b.call('$.pop', b.id('$$component'), b.id('$$exports')); to_push = needs_store_cleanup ? b.var('$$pop', pop_call) : b.return(pop_call); } else { - to_push = b.stmt(b.call('$.pop')); + to_push = b.stmt(b.call('$.pop', b.id('$$component'))); } component_block.body.push(to_push); diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index cad75546d4b4..8a0e37faa89a 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -139,14 +139,15 @@ export function getAllContexts() { /** * @param {Record} props * @param {any} runes + * @param {boolean} [has_async_body] * @param {Function} [fn] - * @returns {void} */ -export function push(props, runes = false, fn) { +export function push(props, runes = false, has_async_body = false, fn) { component_context = { p: component_context, c: null, e: null, + a: has_async_body, s: props, x: null, l: legacy_mode_flag && !runes ? { s: null, u: null, $: [] } : null @@ -157,19 +158,31 @@ export function push(props, runes = false, fn) { component_context.function = fn; dev_current_component_function = fn; } + + return component_context; } /** * @template {Record} T + * @param {ComponentContext} context * @param {T} [component] * @returns {T} */ -export function pop(component) { - var context = /** @type {ComponentContext} */ (component_context); - var effects = context.e; - +export function pop(context, component) { + var ctx = /** @type {ComponentContext} */ (component_context); + if (context !== ctx) { + console.log('h'); + return component ?? /** @type {T} */ ({}); + } + if (ctx.a) { + console.log('suspended'); + return component ?? /** @type {T} */ ({}); + } + var effects = ctx.e; + console.log('effects', effects); + console.log('context', context); if (effects !== null) { - context.e = null; + ctx.e = null; for (var fn of effects) { create_user_effect(fn); @@ -177,10 +190,10 @@ export function pop(component) { } if (component !== undefined) { - context.x = component; + ctx.x = component; } - component_context = context.p; + component_context = ctx.p; if (DEV) { dev_current_component_function = component_context?.function ?? null; diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 65d004137fcb..76570f019f94 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -2,7 +2,7 @@ import { DESTROYED } from '#client/constants'; import { DEV } from 'esm-env'; -import { component_context, is_runes, set_component_context } from '../context.js'; +import { component_context, is_runes, pop, set_component_context } from '../context.js'; import { get_pending_boundary } from '../dom/blocks/boundary.js'; import { invoke_error_boundary } from '../error-handling.js'; import { @@ -19,7 +19,7 @@ import { derived_safe_equal, set_from_async_derived } from './deriveds.js'; -import { aborted } from './effects.js'; +import { aborted, create_user_effect } from './effects.js'; /** * @@ -167,19 +167,26 @@ export async function* for_await_track_reactivity_loss(iterable) { } } -export function unset_context() { +/** + * @param {boolean} [is_component_body] + */ +export function unset_context(is_component_body = false) { set_active_effect(null); set_active_reaction(null); - set_component_context(null); + if (!is_component_body) set_component_context(null); if (DEV) set_from_async_derived(null); } +export let running_deferred_effects = false; + /** * @param {() => Promise} fn + * @param {boolean} [is_component] */ -export async function async_body(fn) { +export async function async_body(fn, is_component = false) { var unsuspend = suspend(); var active = /** @type {Effect} */ (active_effect); + var ctx = is_component ? component_context : null; try { await fn(); @@ -188,6 +195,14 @@ export async function async_body(fn) { invoke_error_boundary(error, active); } } finally { - unsuspend(); + unsuspend(is_component); + console.log(ctx, component_context, ctx === component_context); + if (ctx !== null) { + console.log('hi'); + ctx.a = false; + running_deferred_effects = true; + pop(ctx); + running_deferred_effects = false; + } } } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 82f1de67a98e..407798cdeef3 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -675,7 +675,7 @@ export function suspend() { boundary.update_pending_count(1); if (!pending) batch.increment(); - return function unsuspend() { + return function unsuspend(is_component_body = false) { boundary.update_pending_count(-1); if (!pending) { @@ -685,7 +685,7 @@ export function suspend() { batch.deactivate(); } - unset_context(); + unset_context(is_component_body); }; } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 2c9e4db911aa..8abc93974747 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -41,7 +41,7 @@ import { define_property } from '../../shared/utils.js'; import { get_next_sibling } from '../dom/operations.js'; import { component_context, dev_current_component_function, dev_stack } from '../context.js'; import { Batch, schedule_effect } from './batch.js'; -import { flatten } from './async.js'; +import { flatten, running_deferred_effects } from './async.js'; import { without_reactive_context } from '../dom/elements/bindings/shared.js'; /** @@ -207,9 +207,14 @@ export function user_effect(fn) { // Non-nested `$effect(...)` in a component should be deferred // until the component is mounted var flags = /** @type {Effect} */ (active_effect).f; - var defer = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & EFFECT_RAN) === 0; - + var defer = + !active_reaction && + (flags & BRANCH_EFFECT) !== 0 && + (flags & EFFECT_RAN) === 0 && + !running_deferred_effects; + console.log({ defer, flags, component_context }); if (defer) { + console.log('deferring'); // Top-level `$effect(...)` in an unmounted component — defer until mount var context = /** @type {ComponentContext} */ (component_context); (context.e ??= []).push(fn); diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index c5015875a8a3..b7cf77124550 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -219,9 +219,9 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro var anchor_node = anchor ?? target.appendChild(create_text()); branch(() => { + var ctx; if (context) { - push({}); - var ctx = /** @type {ComponentContext} */ (component_context); + ctx = push({}); ctx.c = context; } @@ -243,8 +243,8 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro /** @type {Effect} */ (active_effect).nodes_end = hydrate_node; } - if (context) { - pop(); + if (context && ctx) { + pop(ctx); } }); diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index d24218c4d3b0..a999154c8821 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -16,6 +16,8 @@ export type ComponentContext = { c: null | Map; /** deferred effects */ e: null | Array<() => void | (() => void)>; + /** whether the component is suspending */ + a: boolean; /** * props — needed for legacy mode lifecycle functions, and for `createEventDispatcher` * @deprecated remove in 6.0