diff --git a/.changeset/spicy-hotels-applaud.md b/.changeset/spicy-hotels-applaud.md new file mode 100644 index 0000000000..343a8baf4e --- /dev/null +++ b/.changeset/spicy-hotels-applaud.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: parallelize more async work 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 706d2b4e10..35f54a57cb 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 @@ -176,7 +176,9 @@ export function client_component(analysis, options) { update: /** @type {any} */ (null), after_update: /** @type {any} */ (null), template: /** @type {any} */ (null), - memoizer: /** @type {any} */ (null) + memoizer: /** @type {any} */ (null), + parallelized_chunks: [], + current_parallelized_chunk: null }; const module = /** @type {ESTree.Program} */ ( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 59c024dfb7..46c922aa33 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -7,9 +7,9 @@ import type { AssignmentExpression, UpdateExpression, VariableDeclaration, - Declaration + Pattern } from 'estree'; -import type { AST, Namespace, ValidatedCompileOptions } from '#compiler'; +import type { AST, Binding, Namespace, ValidatedCompileOptions } from '#compiler'; import type { TransformState } from '../types.js'; import type { ComponentAnalysis } from '../../types.js'; import type { Template } from './transform-template/template.js'; @@ -83,6 +83,20 @@ export interface ComponentClientTransformState extends ClientTransformState { readonly instance_level_snippets: VariableDeclaration[]; /** Snippets hoisted to the module */ readonly module_level_snippets: VariableDeclaration[]; + /** async deriveds and certain awaited variables are chunked so they can be parallelized via `Promise.all` */ + readonly parallelized_chunks: ParallelizedChunk[]; + current_parallelized_chunk: ParallelizedChunk | null; +} + +export interface ParallelizedChunk { + declarators: Array<{ + id: Pattern | null; + init: Expression; + }>; + kind: VariableDeclaration['kind'] | null; + /** index in instance body */ + position: number; + bindings: Binding[]; } export type Context = import('zimmerframe').Context; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 19a4342b5e..aae95add35 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -1,7 +1,7 @@ -/** @import { ArrowFunctionExpression, AssignmentExpression, BlockStatement, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */ +/** @import { ArrowFunctionExpression, AssignmentExpression, BlockStatement, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */ /** @import { Binding } from '#compiler' */ /** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */ -/** @import { Analysis } from '../../types.js' */ +/** @import { Analysis, ComponentAnalysis } from '../../types.js' */ /** @import { Scope } from '../../scope.js' */ import * as b from '#compiler/builders'; import { is_simple_expression } from '../../../utils/ast.js'; @@ -15,6 +15,7 @@ import { import { dev } from '../../../state.js'; import { walk } from 'zimmerframe'; import { validate_mutation } from './visitors/shared/utils.js'; +import is_reference from 'is-reference'; /** * @param {Binding} binding @@ -28,6 +29,79 @@ export function is_state_source(binding, analysis) { ); } +/** + * @param {Expression} expression + * @param {Scope} scope + * @param {Analysis | ComponentAnalysis} analysis + * @param {Binding[]} bindings bindings currently being parallelized (and cannot be accessed) + * @returns {boolean} + */ +export function can_be_parallelized(expression, scope, analysis, bindings) { + let has_closures = false; + let should_stop = false; + /** @type {Set} */ + const references = new Set(); + walk(/** @type {Node} */ (expression), null, { + ArrowFunctionExpression(_, { stop }) { + has_closures = true; + stop(); + }, + FunctionExpression(_, { stop }) { + has_closures = true; + stop(); + }, + Identifier(node, { path }) { + if (is_reference(node, /** @type {Node} */ (path.at(-1)))) { + references.add(node.name); + } + }, + MemberExpression(node, { stop }) { + should_stop = true; + stop(); + }, + CallExpression(node, { stop }) { + should_stop = true; + stop(); + }, + NewExpression(node, { stop }) { + should_stop = true; + stop(); + }, + StaticBlock(node, { stop }) { + has_closures = true; + stop(); + } + }); + if (has_closures || should_stop) { + return false; + } + for (const reference of references) { + const binding = scope.get(reference); + if (!binding || binding.declaration_kind === 'import') { + return false; + } + if ('template' in analysis) { + if (binding.scope !== analysis.instance.scope) { + return false; + } + } else if (binding.scope !== analysis.module.scope) { + return false; + } + + if (bindings.includes(binding)) { + return false; + } + + if (binding.kind === 'derived') { + const init = /** @type {CallExpression} */ (binding.initial); + if (analysis.async_deriveds.has(init)) { + return false; + } + } + } + return true; +} + /** * @param {Identifier} node * @param {ClientTransformState} state diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js index 859842ebc3..90e40f7277 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js @@ -1,13 +1,17 @@ -/** @import { Expression, ExpressionStatement } from 'estree' */ -/** @import { ComponentContext } from '../types' */ +/** @import { Expression, ExpressionStatement, Node, Program } from 'estree' */ +/** @import { ComponentContext, ParallelizedChunk } from '../types' */ import * as b from '#compiler/builders'; +import { is_expression_async } from '../../../../utils/ast.js'; import { get_rune } from '../../../scope.js'; +import { can_be_parallelized } from '../utils.js'; /** * @param {ExpressionStatement} node * @param {ComponentContext} context */ export function ExpressionStatement(node, context) { + const parent = /** @type {Node} */ (context.path.at(-1)); + const position = /** @type {Program} */ (parent).body?.indexOf?.(node); if (node.expression.type === 'CallExpression') { const rune = get_rune(node.expression, context.state.scope); @@ -25,6 +29,40 @@ export function ExpressionStatement(node, context) { return b.empty; } } + if ( + node.expression.type === 'AwaitExpression' && + !is_expression_async(node.expression.argument) && + context.state.analysis.instance?.scope === context.state.scope + ) { + const current_chunk = context.state.current_parallelized_chunk; + const parallelize = can_be_parallelized( + node.expression.argument, + context.state.scope, + context.state.analysis, + current_chunk?.bindings ?? [] + ); + if (parallelize) { + const declarator = { + id: null, + init: /** @type {Expression} */ (context.visit(node.expression.argument)) + }; + if (current_chunk) { + current_chunk.declarators.push(declarator); + current_chunk.position = position; + } else { + /** @type {ParallelizedChunk} */ + const chunk = { + kind: null, + declarators: [declarator], + position, + bindings: [] + }; + context.state.current_parallelized_chunk = chunk; + context.state.parallelized_chunks.push(chunk); + } + return b.empty; + } + } context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js index b01ed01bd7..3df8d24cba 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js @@ -1,12 +1,12 @@ /** @import { Identifier, Node } from 'estree' */ -/** @import { Context } from '../types' */ +/** @import { ComponentContext } from '../types' */ import is_reference from 'is-reference'; import * as b from '#compiler/builders'; import { build_getter } from '../utils.js'; /** * @param {Identifier} node - * @param {Context} context + * @param {ComponentContext} context */ export function Identifier(node, context) { const parent = /** @type {Node} */ (context.path.at(-1)); @@ -35,6 +35,9 @@ export function Identifier(node, context) { return b.id('$$props'); } } + if (binding && context.state.current_parallelized_chunk?.bindings?.includes(binding)) { + context.state.current_parallelized_chunk = null; + } return build_getter(node, context.state); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index 07342da314..cee3a7aaf1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -5,10 +5,10 @@ import * as b from '#compiler/builders'; import { add_state_transformers } from './shared/declarations.js'; /** - * @param {Program} _ + * @param {Program} node * @param {ComponentContext} context */ -export function Program(_, context) { +export function Program(node, context) { if (!context.state.analysis.runes) { context.state.transform['$$props'] = { read: (node) => ({ ...node, name: '$$sanitized_props' }) @@ -137,5 +137,52 @@ export function Program(_, context) { add_state_transformers(context); - context.next(); + /** @type {Program['body']} */ + const body = []; + for (let i = 0; i < node.body.length; i++) { + const transformed = /** @type {Program['body'][number]} */ (context.visit(node.body[i])); + body.push(transformed); + } + if (context.state.parallelized_chunks) { + let offset = 0; + for (const chunk of context.state.parallelized_chunks) { + if (chunk.declarators.length === 1) { + const declarator = chunk.declarators[0]; + if (declarator.id === null || chunk.kind === null) { + body.splice( + chunk.position + offset, + 0, + b.stmt(b.call(b.await(b.call('$.save', declarator.init)))) + ); + } else { + body.splice( + chunk.position + offset, + 0, + b.declaration(chunk.kind, [ + b.declarator(declarator.id, b.call(b.await(b.call('$.save', declarator.init)))) + ]) + ); + } + } else { + const pattern = b.array_pattern(chunk.declarators.map(({ id }) => id)); + const init = b.call( + b.await(b.call('$.save', b.call('$.all', ...chunk.declarators.map(({ init }) => init)))) + ); + if (pattern.elements.every((element) => element === null)) { + body.splice(chunk.position + offset, 0, b.stmt(init)); + } else { + body.splice( + chunk.position + offset, + 0, + b.declaration(chunk.kind ?? 'const', [b.declarator(pattern, init)]) + ); + } + } + offset++; + } + } + return { + ...node, + body + }; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 0998dc4778..92120b01bb 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -1,12 +1,18 @@ -/** @import { CallExpression, Expression, Identifier, Literal, VariableDeclaration, VariableDeclarator } from 'estree' */ +/** @import { AwaitExpression, CallExpression, Expression, Identifier, Literal, Node, Program, VariableDeclaration, VariableDeclarator } from 'estree' */ /** @import { Binding } from '#compiler' */ -/** @import { ComponentContext } from '../types' */ +/** @import { ComponentContext, ParallelizedChunk } from '../types' */ import { dev, is_ignored, locate_node } from '../../../../state.js'; -import { extract_paths } from '../../../../utils/ast.js'; +import { extract_paths, is_expression_async } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import * as assert from '../../../../utils/assert.js'; import { get_rune } from '../../../scope.js'; -import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js'; +import { + can_be_parallelized, + get_prop_source, + is_prop_source, + is_state_source, + should_proxy +} from '../utils.js'; import { is_hoisted_function } from '../../utils.js'; import { get_value } from './shared/declarations.js'; @@ -17,12 +23,14 @@ import { get_value } from './shared/declarations.js'; export function VariableDeclaration(node, context) { /** @type {VariableDeclarator[]} */ const declarations = []; + const parent = /** @type {Node} */ (context.path.at(-1)); + const position = /** @type {Program} */ (parent).body?.indexOf?.(node); if (context.state.analysis.runes) { for (const declarator of node.declarations) { const init = /** @type {Expression} */ (declarator.init); const rune = get_rune(init, context.state.scope); - + const bindings = context.state.scope.get_bindings(declarator); if ( !rune || rune === '$effect.tracking' || @@ -39,6 +47,51 @@ export function VariableDeclaration(node, context) { continue; } + if ( + init?.type === 'AwaitExpression' && + context.state.analysis.instance?.scope === context.state.scope && + !is_expression_async(init.argument) + ) { + const current_chunk = context.state.current_parallelized_chunk; + const parallelize = can_be_parallelized( + init.argument, + context.state.scope, + context.state.analysis, + [...(current_chunk?.bindings ?? []), ...bindings] + ); + if (parallelize) { + const { id, init: visited_init } = /** @type {VariableDeclarator} */ ( + context.visit({ + ...declarator, + init: init.argument + }) + ); + const _declarator = { + id, + init: /** @type {Expression} */ (visited_init) + }; + if ( + current_chunk && + (current_chunk.kind === node.kind || current_chunk.kind === null) + ) { + current_chunk.declarators.push(_declarator); + current_chunk.bindings.push(...bindings); + current_chunk.position = /** @type {Program} */ (parent).body.indexOf(node); + current_chunk.kind = node.kind; + } else { + /** @type {ParallelizedChunk} */ + const chunk = { + kind: node.kind, + declarators: [_declarator], + position, + bindings + }; + context.state.current_parallelized_chunk = chunk; + context.state.parallelized_chunks.push(chunk); + } + continue; + } + } declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator))); continue; } @@ -124,47 +177,85 @@ export function VariableDeclaration(node, context) { const value = /** @type {Expression} */ (args[0]) ?? b.void0; // TODO do we need the void 0? can we just omit it altogether? if (rune === '$state' || rune === '$state.raw') { + const state_declarators = []; + const current_chunk = context.state.current_parallelized_chunk; + const parallelize = + declarator.id.type === 'Identifier' && + context.state.analysis.instance?.scope === context.state.scope && + value.type === 'AwaitExpression' && + !is_expression_async(value.argument) && + can_be_parallelized(value.argument, context.state.scope, context.state.analysis, [ + ...(current_chunk?.bindings ?? []), + ...bindings + ]); /** * @param {Identifier} id + * @param {Expression} visited * @param {Expression} value */ - const create_state_declarator = (id, value) => { - const binding = /** @type {import('#compiler').Binding} */ ( - context.state.scope.get(id.name) - ); + const create_state_declarator = (id, visited, value) => { + const binding = /** @type {Binding} */ (context.state.scope.get(id.name)); const is_state = is_state_source(binding, context.state.analysis); - const is_proxy = should_proxy(value, context.state.scope); + const is_proxy = should_proxy(visited, context.state.scope); + const compose = []; + if (parallelize) { + if (rune === '$state' && is_proxy) { + compose.push(b.id('$.proxy')); + + if (dev && !is_state) { + compose.push( + b.arrow([b.id('proxy')], b.call('$.tag_proxy', b.id('proxy'), b.literal(id.name))) + ); + } + } - if (rune === '$state' && is_proxy) { - value = b.call('$.proxy', value); + if (is_state) { + compose.push(b.id('$.state')); + if (dev) { + compose.push( + b.arrow([b.id('source')], b.call('$.tag', b.id('source'), b.literal(id.name))) + ); + } + } + return b.call( + '$.async_compose', + /** @type {Expression} */ ( + context.visit(/** @type {AwaitExpression} */ (value).argument) + ), + ...compose + ); + } else { + let value = visited; + if (rune === '$state' && is_proxy) { + value = b.call('$.proxy', value); - if (dev && !is_state) { - value = b.call('$.tag_proxy', value, b.literal(id.name)); + if (dev && !is_state) { + value = b.call('$.tag_proxy', value, b.literal(id.name)); + } } - } - if (is_state) { - value = b.call('$.state', value); + if (is_state) { + value = b.call('$.state', value); - if (dev) { - value = b.call('$.tag', value, b.literal(id.name)); + if (dev) { + value = b.call('$.tag', value, b.literal(id.name)); + } } + return value; } - - return value; }; if (declarator.id.type === 'Identifier') { const expression = /** @type {Expression} */ (context.visit(value)); - declarations.push( - b.declarator(declarator.id, create_state_declarator(declarator.id, expression)) + state_declarators.push( + b.declarator(declarator.id, create_state_declarator(declarator.id, expression, value)) ); } else { const tmp = b.id(context.state.scope.generate('tmp')); const { inserts, paths } = extract_paths(declarator.id, tmp); - declarations.push( + state_declarators.push( b.declarator(tmp, /** @type {Expression} */ (context.visit(value))), ...inserts.map(({ id, value }) => { id.name = context.state.scope.generate('$$array'); @@ -186,12 +277,36 @@ export function VariableDeclaration(node, context) { return b.declarator( path.node, binding?.kind === 'state' || binding?.kind === 'raw_state' - ? create_state_declarator(binding.node, value) + ? create_state_declarator(binding.node, value, path.expression) : value ); }) ); } + if (!parallelize) { + declarations.push(...state_declarators); + } else { + const declarators = state_declarators.map(({ id, init }) => ({ + id, + init: /** @type {Expression} */ (init) + })); + if (current_chunk && (current_chunk.kind === node.kind || current_chunk.kind === null)) { + current_chunk.declarators.push(...declarators); + current_chunk.bindings.push(...bindings); + current_chunk.position = position; + current_chunk.kind = node.kind; + } else { + /** @type {ParallelizedChunk} */ + const chunk = { + kind: node.kind, + declarators, + position, + bindings + }; + context.state.current_parallelized_chunk = chunk; + context.state.parallelized_chunks.push(chunk); + } + } continue; } @@ -200,6 +315,25 @@ export function VariableDeclaration(node, context) { const is_async = context.state.analysis.async_deriveds.has( /** @type {CallExpression} */ (init) ); + let parallelize = false; + const current_chunk = context.state.current_parallelized_chunk; + if ( + is_async && + context.state.analysis.instance && + context.state.scope === context.state.analysis.instance.scope && + // TODO make it work without this + declarator.id.type === 'Identifier' + ) { + parallelize = can_be_parallelized(value, context.state.scope, context.state.analysis, [ + ...(current_chunk?.bindings ?? []), + ...context.state.scope.get_bindings(declarator) + ]); + } + + /** @type {VariableDeclarator[]} */ + const derived_declarators = []; + /** @type {Binding[]} */ + const bindings = []; if (declarator.id.type === 'Identifier') { let expression = /** @type {Expression} */ ( @@ -217,17 +351,22 @@ export function VariableDeclaration(node, context) { location ? b.literal(location) : undefined ); - call = b.call(b.await(b.call('$.save', call))); - if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name)); - - declarations.push(b.declarator(declarator.id, call)); + if (!parallelize) call = b.call(b.await(b.call('$.save', call))); + if (dev) { + call = b.call( + '$.tag' + (parallelize ? '_async' : ''), + call, + b.literal(declarator.id.name) + ); + } + bindings.push(/** @type {Binding} */ (context.state.scope.get(declarator.id.name))); + derived_declarators.push(b.declarator(declarator.id, call)); } else { if (rune === '$derived') expression = b.thunk(expression); let call = b.call('$.derived', expression); if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name)); - - declarations.push(b.declarator(declarator.id, call)); + derived_declarators.push(b.declarator(declarator.id, call)); } } else { const init = /** @type {CallExpression} */ (declarator.init); @@ -260,8 +399,7 @@ export function VariableDeclaration(node, context) { const label = `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`; call = b.call('$.tag', call, b.literal(label)); } - - declarations.push(b.declarator(id, call)); + derived_declarators.push(b.declarator(id, call)); } const { inserts, paths } = extract_paths(declarator.id, rhs); @@ -277,14 +415,13 @@ export function VariableDeclaration(node, context) { const label = `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`; call = b.call('$.tag', call, b.literal(label)); } - - declarations.push(b.declarator(id, call)); + derived_declarators.push(b.declarator(id, call)); } for (const path of paths) { const expression = /** @type {Expression} */ (context.visit(path.expression)); const call = b.call('$.derived', b.thunk(expression)); - declarations.push( + derived_declarators.push( b.declarator( path.node, dev @@ -295,12 +432,37 @@ export function VariableDeclaration(node, context) { } } + if (!parallelize) { + declarations.push(...derived_declarators); + } else if (derived_declarators.length > 0) { + const declarators = derived_declarators.map(({ id, init }) => ({ + id, + init: /** @type {Expression} */ (init) + })); + if (current_chunk && (current_chunk.kind === node.kind || current_chunk.kind === null)) { + current_chunk.declarators.push(...declarators); + current_chunk.bindings.push(...bindings); + current_chunk.position = position; + current_chunk.kind = node.kind; + } else { + /** @type {ParallelizedChunk} */ + const chunk = { + kind: node.kind, + declarators, + position, + bindings + }; + context.state.current_parallelized_chunk = chunk; + context.state.parallelized_chunks.push(chunk); + } + } + continue; } } } else { for (const declarator of node.declarations) { - const bindings = /** @type {Binding[]} */ (context.state.scope.get_bindings(declarator)); + const bindings = context.state.scope.get_bindings(declarator); const has_state = bindings.some((binding) => binding.kind === 'state'); const has_props = bindings.some((binding) => binding.kind === 'bindable_prop'); @@ -403,13 +565,9 @@ export function VariableDeclaration(node, context) { * @param {Expression} value */ function create_state_declarators(declarator, context, value) { + const immutable = context.state.analysis.immutable ? b.true : undefined; if (declarator.id.type === 'Identifier') { - return [ - b.declarator( - declarator.id, - b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined) - ) - ]; + return [b.declarator(declarator.id, b.call('$.mutable_source', value, immutable))]; } const tmp = b.id(context.state.scope.generate('tmp')); @@ -424,15 +582,13 @@ function create_state_declarators(declarator, context, value) { const expression = /** @type {Expression} */ (context.visit(b.thunk(value))); return b.declarator(id, b.call('$.derived', expression)); }), - ...paths.map((path) => { - const value = /** @type {Expression} */ (context.visit(path.expression)); - const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name); + ...paths.map(({ expression, node }) => { + const value = /** @type {Expression} */ (context.visit(expression)); + const binding = context.state.scope.get(/** @type {Identifier} */ (node).name); return b.declarator( - path.node, - binding?.kind === 'state' - ? b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined) - : value + node, + binding?.kind === 'state' ? b.call('$.mutable_source', value, immutable) : value ); }) ]; diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js index 673a710fac..f66e0a8106 100644 --- a/packages/svelte/src/internal/client/dev/tracing.js +++ b/packages/svelte/src/internal/client/dev/tracing.js @@ -184,6 +184,17 @@ export function tag(source, label) { return source; } +/** + * @template T + * @param {Promise>} promise + * @param {string} label + * @returns {Promise>} + */ +export async function tag_async(promise, label) { + const source = await promise; + return tag(source, label); +} + /** * @param {unknown} value * @param {string} label diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index c5b7bb845c..73628ef72f 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -7,7 +7,7 @@ export { add_locations } from './dev/elements.js'; export { hmr } from './dev/hmr.js'; export { create_ownership_validator } from './dev/ownership.js'; export { check_target, legacy_api } from './dev/legacy.js'; -export { trace, tag, tag_proxy } from './dev/tracing.js'; +export { trace, tag, tag_async, tag_proxy } from './dev/tracing.js'; export { inspect } from './dev/inspect.js'; export { async } from './dom/blocks/async.js'; export { validate_snippet_args } from './dev/validation.js'; @@ -99,7 +99,9 @@ export { with_script } from './dom/template.js'; export { + all, async_body, + async_compose, for_await_track_reactivity_loss, save, track_reactivity_loss diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 65d004137f..6dc1dce543 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -191,3 +191,28 @@ export async function async_body(fn) { unsuspend(); } } + +/** + * @template T + * @param {Array>} promises + * @returns {Promise>} + */ +export function all(...promises) { + return Promise.all( + promises.map((promise) => + promise instanceof Promise ? save(promise).then((restore) => restore()) : promise + ) + ); +} + +/** + * @param {Promise} promise + * @param {Array<(arg: any) => any>} fns + */ +export async function async_compose(promise, ...fns) { + let res = await promise; + for (const fn of fns) { + res = fn(res); + } + return res; +}