diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index db173ba78d..92f6c2a578 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -155,8 +155,7 @@ export function build_render_statement(state) { : b.block(state.update) ), all.length > 0 && b.array(sync.map(({ expression }) => b.thunk(expression))), - async.length > 0 && b.array(async.map(({ expression }) => b.thunk(expression, true))), - !state.analysis.runes && sync.length > 0 && b.id('$.derived_safe_equal') + async.length > 0 && b.array(async.map(({ expression }) => b.thunk(expression, true))) ) ); } diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index a68736281d..2eac6c55e0 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -1,37 +1,24 @@ -/** @import { Effect, TemplateNode, Value } from '#client' */ -import { DESTROYED } from '#client/constants'; -import { async_derived } from '../../reactivity/deriveds.js'; -import { active_effect, get } from '../../runtime.js'; -import { capture, get_pending_boundary } from './boundary.js'; +/** @import { TemplateNode, Value } from '#client' */ +import { flatten } from '../../reactivity/async.js'; +import { get } from '../../runtime.js'; +import { get_pending_boundary } from './boundary.js'; /** * @param {TemplateNode} node * @param {Array<() => Promise>} expressions * @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn */ -export async function async(node, expressions, fn) { - // TODO handle hydration - - var parent = /** @type {Effect} */ (active_effect); - - var restore = capture(); +export function async(node, expressions, fn) { var boundary = get_pending_boundary(); + // TODO why is this necessary? doesn't it happen inside `async_derived` inside `flatten`? boundary.update_pending_count(1); - try { - const deriveds = await Promise.all(expressions.map((fn) => async_derived(fn))); - - // get deriveds eagerly to avoid creating blocks if they reject - for (const d of deriveds) get(d); - - if ((parent.f & DESTROYED) !== 0) return; + flatten([], expressions, (values) => { + // get values eagerly to avoid creating blocks if they reject + for (const d of values) get(d); - restore(); - fn(node, ...deriveds); - } catch (error) { - boundary.error(error); - } finally { + fn(node, ...values); boundary.update_pending_count(-1); - } + }); } diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 83035cc6a1..fa3f982838 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -23,6 +23,7 @@ import { ATTACHMENT_KEY, NAMESPACE_HTML } from '../../../../constants.js'; import { block, branch, destroy_effect } from '../../reactivity/effects.js'; import { derived } from '../../reactivity/deriveds.js'; import { init_select, select_option } from './bindings/select.js'; +import { flatten } from '../../reactivity/async.js'; export const CLASS = Symbol('class'); export const STYLE = Symbol('style'); @@ -473,72 +474,54 @@ export function attribute_effect( sync = [], async = [], css_hash, - skip_warning = false, - d = derived -) { - const deriveds = sync.map(d); - - create_attribute_effect(element, fn, deriveds, css_hash, skip_warning); -} - -/** - * @param {Element & ElementCSSInlineStyle} element - * @param {(...expressions: any) => Record} fn - * @param {Value[]} deriveds - * @param {string} [css_hash] - * @param {boolean} [skip_warning] - */ -export function create_attribute_effect( - element, - fn, - deriveds = [], - css_hash, skip_warning = false ) { - /** @type {Record | undefined} */ - var prev = undefined; + flatten(sync, async, (values) => { + /** @type {Record | undefined} */ + var prev = undefined; - /** @type {Record} */ - var effects = {}; + /** @type {Record} */ + var effects = {}; - var is_select = element.nodeName === 'SELECT'; - var inited = false; + var is_select = element.nodeName === 'SELECT'; + var inited = false; - block(() => { - var next = fn(...deriveds.map(get)); - /** @type {Record} */ - var current = set_attributes(element, prev, next, css_hash, skip_warning); + block(() => { + var next = fn(...values.map(get)); + /** @type {Record} */ + var current = set_attributes(element, prev, next, css_hash, skip_warning); - if (inited && is_select && 'value' in next) { - select_option(/** @type {HTMLSelectElement} */ (element), next.value, false); - } + if (inited && is_select && 'value' in next) { + select_option(/** @type {HTMLSelectElement} */ (element), next.value, false); + } - for (let symbol of Object.getOwnPropertySymbols(effects)) { - if (!next[symbol]) destroy_effect(effects[symbol]); - } + for (let symbol of Object.getOwnPropertySymbols(effects)) { + if (!next[symbol]) destroy_effect(effects[symbol]); + } - for (let symbol of Object.getOwnPropertySymbols(next)) { - var n = next[symbol]; + for (let symbol of Object.getOwnPropertySymbols(next)) { + var n = next[symbol]; - if (symbol.description === ATTACHMENT_KEY && (!prev || n !== prev[symbol])) { - if (effects[symbol]) destroy_effect(effects[symbol]); - effects[symbol] = branch(() => attach(element, () => n)); + if (symbol.description === ATTACHMENT_KEY && (!prev || n !== prev[symbol])) { + if (effects[symbol]) destroy_effect(effects[symbol]); + effects[symbol] = branch(() => attach(element, () => n)); + } + + current[symbol] = n; } - current[symbol] = n; + prev = current; + }); + + if (is_select) { + init_select( + /** @type {HTMLSelectElement} */ (element), + () => /** @type {Record} */ (prev).value + ); } - prev = current; + inited = true; }); - - if (is_select) { - init_select( - /** @type {HTMLSelectElement} */ (element), - () => /** @type {Record} */ (prev).value - ); - } - - inited = true; } /** diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js new file mode 100644 index 0000000000..2708d9139a --- /dev/null +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -0,0 +1,50 @@ +/** @import { Effect, Value } from '#client' */ + +import { DESTROYED } from '#client/constants'; +import { is_runes } from '../context.js'; +import { capture, get_pending_boundary } from '../dom/blocks/boundary.js'; +import { invoke_error_boundary } from '../error-handling.js'; +import { active_effect } from '../runtime.js'; +import { current_batch } from './batch.js'; +import { async_derived, derived, derived_safe_equal } from './deriveds.js'; + +/** + * + * @param {Array<() => any>} sync + * @param {Array<() => Promise>} async + * @param {(values: Value[]) => any} fn + */ +export function flatten(sync, async, fn) { + const d = is_runes() ? derived : derived_safe_equal; + + if (async.length > 0) { + var batch = current_batch; + var parent = /** @type {Effect} */ (active_effect); + + var restore = capture(); + + var boundary = get_pending_boundary(); + + Promise.all(async.map((expression) => async_derived(expression))) + .then((result) => { + if ((parent.f & DESTROYED) !== 0) return; + + batch?.restore(); + + restore(); + + try { + fn([...sync.map(d), ...result]); + } catch (error) { + invoke_error_boundary(error, parent); + } + + batch?.flush(); + }) + .catch((error) => { + boundary.error(error); + }); + } else { + fn(sync.map(d)); + } +} diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 1bef217476..fdb136d503 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -1,4 +1,4 @@ -/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager, Value } from '#client' */ +/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager } from '#client' */ import { check_dirtiness, active_effect, @@ -38,11 +38,9 @@ import * as e from '../errors.js'; import { DEV } from 'esm-env'; import { define_property } from '../../shared/utils.js'; import { get_next_sibling } from '../dom/operations.js'; -import { async_derived, derived } from './deriveds.js'; -import { capture } from '../dom/blocks/boundary.js'; import { component_context, dev_current_component_function } from '../context.js'; -import { Batch, current_batch } from './batch.js'; -import { invoke_error_boundary } from '../error-handling.js'; +import { Batch } from './batch.js'; +import { flatten } from './async.js'; /** * @param {'$effect' | '$effect.pre' | '$inspect'} rune @@ -338,43 +336,11 @@ export function render_effect(fn, flags = 0) { * @param {(...expressions: any) => void | (() => void)} fn * @param {Array<() => any>} sync * @param {Array<() => Promise>} async - * @param {(fn: () => T) => Derived} d */ -export function template_effect(fn, sync = [], async = [], d = derived) { - if (async.length > 0) { - var batch = current_batch; - var parent = /** @type {Effect} */ (active_effect); - - var restore = capture(); - - Promise.all(async.map((expression) => async_derived(expression))).then((result) => { - if ((parent.f & DESTROYED) !== 0) return; - - // TODO probably need to do this in async.js as well - batch?.restore(); - - restore(); - - try { - create_template_effect(fn, [...sync.map(d), ...result]); - } catch (error) { - invoke_error_boundary(error, parent); - } - - batch?.flush(); - }); - } else { - create_template_effect(fn, sync.map(d)); - } -} - -/** - * @param {(...expressions: any) => void | (() => void)} fn - * @param {Value[]} deriveds - */ -function create_template_effect(fn, deriveds) { - var effect = () => fn(...deriveds.map(get)); - create_effect(RENDER_EFFECT, effect, true); +export function template_effect(fn, sync = [], async = []) { + flatten(sync, async, (values) => { + create_effect(RENDER_EFFECT, () => fn(...values.map(get)), true); + }); } /**