svelte 5.47.1 → 5.48.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/compiler/index.js +1 -1
  2. package/package.json +1 -1
  3. package/src/compiler/errors.js +2 -2
  4. package/src/compiler/index.js +25 -1
  5. package/src/compiler/phases/1-parse/index.js +14 -0
  6. package/src/compiler/phases/1-parse/read/style.js +17 -13
  7. package/src/compiler/phases/3-transform/client/transform-client.js +1 -0
  8. package/src/compiler/phases/3-transform/client/visitors/ConstTag.js +11 -5
  9. package/src/compiler/phases/3-transform/client/visitors/Fragment.js +4 -3
  10. package/src/compiler/phases/3-transform/client/visitors/RegularElement.js +2 -1
  11. package/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js +1 -1
  12. package/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js +1 -1
  13. package/src/compiler/phases/3-transform/client/visitors/SvelteElement.js +1 -1
  14. package/src/compiler/phases/3-transform/client/visitors/shared/component.js +1 -1
  15. package/src/compiler/phases/3-transform/server/visitors/ConstTag.js +4 -2
  16. package/src/compiler/phases/3-transform/server/visitors/HtmlTag.js +11 -1
  17. package/src/compiler/phases/3-transform/server/visitors/SvelteElement.js +1 -1
  18. package/src/compiler/phases/3-transform/shared/transform-async.js +15 -12
  19. package/src/internal/client/dom/blocks/async.js +7 -2
  20. package/src/internal/client/dom/blocks/boundary.js +13 -7
  21. package/src/internal/client/dom/elements/attributes.js +2 -2
  22. package/src/internal/client/reactivity/async.js +65 -40
  23. package/src/internal/client/reactivity/batch.js +27 -30
  24. package/src/internal/client/reactivity/effects.js +3 -3
  25. package/src/internal/client/reactivity/sources.js +9 -19
  26. package/src/internal/client/runtime.js +17 -14
  27. package/src/internal/client/validate.js +2 -1
  28. package/src/internal/server/dev.js +6 -1
  29. package/src/version.js +1 -1
  30. package/types/index.d.ts +6 -0
  31. package/types/index.d.ts.map +2 -1
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "svelte",
3
3
  "description": "Cybernetically enhanced web apps",
4
4
  "license": "MIT",
5
- "version": "5.47.1",
5
+ "version": "5.48.1",
6
6
  "type": "module",
7
7
  "types": "./types/index.d.ts",
8
8
  "engines": {
@@ -977,12 +977,12 @@ export function const_tag_invalid_expression(node) {
977
977
  }
978
978
 
979
979
  /**
980
- * `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>`, `<svelte:boundary` or `<Component>`
980
+ * `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>`, `<svelte:boundary>` or `<Component>`
981
981
  * @param {null | number | NodeLike} node
982
982
  * @returns {never}
983
983
  */
984
984
  export function const_tag_invalid_placement(node) {
985
- e(node, 'const_tag_invalid_placement', `\`{@const}\` must be the immediate child of \`{#snippet}\`, \`{#if}\`, \`{:else if}\`, \`{:else}\`, \`{#each}\`, \`{:then}\`, \`{:catch}\`, \`<svelte:fragment>\`, \`<svelte:boundary\` or \`<Component>\`\nhttps://svelte.dev/e/const_tag_invalid_placement`);
985
+ e(node, 'const_tag_invalid_placement', `\`{@const}\` must be the immediate child of \`{#snippet}\`, \`{#if}\`, \`{:else if}\`, \`{:else}\`, \`{#each}\`, \`{:then}\`, \`{:catch}\`, \`<svelte:fragment>\`, \`<svelte:boundary>\` or \`<Component>\`\nhttps://svelte.dev/e/const_tag_invalid_placement`);
986
986
  }
987
987
 
988
988
  /**
@@ -3,8 +3,9 @@
3
3
  /** @import { AST } from './public.js' */
4
4
  import { walk as zimmerframe_walk } from 'zimmerframe';
5
5
  import { convert } from './legacy.js';
6
- import { parse as _parse } from './phases/1-parse/index.js';
6
+ import { parse as _parse, Parser } from './phases/1-parse/index.js';
7
7
  import { remove_typescript_nodes } from './phases/1-parse/remove_typescript_nodes.js';
8
+ import { parse_stylesheet } from './phases/1-parse/read/style.js';
8
9
  import { analyze_component, analyze_module } from './phases/2-analyze/index.js';
9
10
  import { transform_component, transform_module } from './phases/3-transform/index.js';
10
11
  import { validate_component_options, validate_module_options } from './validate-options.js';
@@ -118,6 +119,29 @@ export function parse(source, { modern, loose } = {}) {
118
119
  return to_public_ast(source, ast, modern);
119
120
  }
120
121
 
122
+ /**
123
+ * The parseCss function parses a CSS stylesheet, returning its abstract syntax tree.
124
+ *
125
+ * @param {string} source The CSS source code
126
+ * @returns {Omit<AST.CSS.StyleSheet, 'attributes' | 'content'>}
127
+ */
128
+ export function parseCss(source) {
129
+ source = remove_bom(source);
130
+ state.reset({ warning: () => false, filename: undefined });
131
+
132
+ state.set_source(source);
133
+
134
+ const parser = Parser.forCss(source);
135
+ const children = parse_stylesheet(parser);
136
+
137
+ return {
138
+ type: 'StyleSheet',
139
+ start: 0,
140
+ end: source.length,
141
+ children
142
+ };
143
+ }
144
+
121
145
  /**
122
146
  * @param {string} source
123
147
  * @param {AST.Root} ast
@@ -34,6 +34,20 @@ export class Parser {
34
34
  /** */
35
35
  index = 0;
36
36
 
37
+ /**
38
+ * Creates a minimal parser instance for CSS-only parsing.
39
+ * Skips Svelte component parsing setup.
40
+ * @param {string} source
41
+ * @returns {Parser}
42
+ */
43
+ static forCss(source) {
44
+ const parser = Object.create(Parser.prototype);
45
+ parser.template = source;
46
+ parser.index = 0;
47
+ parser.loose = false;
48
+ return parser;
49
+ }
50
+
37
51
  /** Whether we're parsing in TypeScript mode */
38
52
  ts = false;
39
53
 
@@ -24,10 +24,11 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/;
24
24
  */
25
25
  export default function read_style(parser, start, attributes) {
26
26
  const content_start = parser.index;
27
- const children = read_body(parser, '</style');
27
+ const children = read_body(parser, (p) => p.match('</style') || p.index >= p.template.length);
28
28
  const content_end = parser.index;
29
29
 
30
- parser.read(/^<\/style\s*>/);
30
+ parser.eat('</style', true);
31
+ parser.read(/^\s*>/);
31
32
 
32
33
  return {
33
34
  type: 'StyleSheet',
@@ -46,20 +47,14 @@ export default function read_style(parser, start, attributes) {
46
47
 
47
48
  /**
48
49
  * @param {Parser} parser
49
- * @param {string} close
50
- * @returns {any[]}
50
+ * @param {(parser: Parser) => boolean} finished
51
+ * @returns {Array<AST.CSS.Rule | AST.CSS.Atrule>}
51
52
  */
52
- function read_body(parser, close) {
53
+ function read_body(parser, finished) {
53
54
  /** @type {Array<AST.CSS.Rule | AST.CSS.Atrule>} */
54
55
  const children = [];
55
56
 
56
- while (parser.index < parser.template.length) {
57
- allow_comment_or_whitespace(parser);
58
-
59
- if (parser.match(close)) {
60
- return children;
61
- }
62
-
57
+ while ((allow_comment_or_whitespace(parser), !finished(parser))) {
63
58
  if (parser.match('@')) {
64
59
  children.push(read_at_rule(parser));
65
60
  } else {
@@ -67,7 +62,7 @@ function read_body(parser, close) {
67
62
  }
68
63
  }
69
64
 
70
- e.expected_token(parser.template.length, close);
65
+ return children;
71
66
  }
72
67
 
73
68
  /**
@@ -627,3 +622,12 @@ function allow_comment_or_whitespace(parser) {
627
622
  parser.allow_whitespace();
628
623
  }
629
624
  }
625
+
626
+ /**
627
+ * Parse standalone CSS content (not wrapped in `<style>`).
628
+ * @param {Parser} parser
629
+ * @returns {Array<AST.CSS.Rule | AST.CSS.Atrule>}
630
+ */
631
+ export function parse_stylesheet(parser) {
632
+ return read_body(parser, (p) => p.index >= p.template.length);
633
+ }
@@ -170,6 +170,7 @@ export function client_component(analysis, options) {
170
170
  // these are set inside the `Fragment` visitor, and cannot be used until then
171
171
  init: /** @type {any} */ (null),
172
172
  consts: /** @type {any} */ (null),
173
+ snippets: /** @type {any} */ (null),
173
174
  let_directives: /** @type {any} */ (null),
174
175
  update: /** @type {any} */ (null),
175
176
  after_update: /** @type {any} */ (null),
@@ -1,4 +1,4 @@
1
- /** @import { Pattern } from 'estree' */
1
+ /** @import { Expression, Identifier, Pattern } from 'estree' */
2
2
  /** @import { AST } from '#compiler' */
3
3
  /** @import { ComponentContext } from '../types' */
4
4
  /** @import { ExpressionMetadata } from '../../../nodes.js' */
@@ -88,8 +88,8 @@ export function ConstTag(node, context) {
88
88
 
89
89
  /**
90
90
  * @param {ComponentContext['state']} state
91
- * @param {import('estree').Identifier} id
92
- * @param {import('estree').Expression} expression
91
+ * @param {Identifier} id
92
+ * @param {Expression} expression
93
93
  * @param {ExpressionMetadata} metadata
94
94
  * @param {import('#compiler').Binding[]} bindings
95
95
  */
@@ -99,7 +99,9 @@ function add_const_declaration(state, id, expression, metadata, bindings) {
99
99
  const after = dev ? [b.stmt(b.call('$.get', id))] : [];
100
100
 
101
101
  const has_await = metadata.has_await;
102
- const blockers = [...metadata.dependencies].map((dep) => dep.blocker).filter((b) => b !== null);
102
+ const blockers = [...metadata.dependencies]
103
+ .map((dep) => dep.blocker)
104
+ .filter((b) => b !== null && b.object !== state.async_consts?.id);
103
105
 
104
106
  if (has_await || state.async_consts || blockers.length > 0) {
105
107
  const run = (state.async_consts ??= {
@@ -112,7 +114,11 @@ function add_const_declaration(state, id, expression, metadata, bindings) {
112
114
  const assignment = b.assignment('=', id, expression);
113
115
  const body = after.length === 0 ? assignment : b.block([b.stmt(assignment), ...after]);
114
116
 
115
- if (blockers.length > 0) run.thunks.push(b.thunk(b.call('Promise.all', b.array(blockers))));
117
+ if (blockers.length === 1) {
118
+ run.thunks.push(b.thunk(b.member(/** @type {Expression} */ (blockers[0]), 'promise')));
119
+ } else if (blockers.length > 0) {
120
+ run.thunks.push(b.thunk(b.call('$.wait', b.array(blockers))));
121
+ }
116
122
 
117
123
  run.thunks.push(b.thunk(body, has_await));
118
124
 
@@ -1,13 +1,13 @@
1
1
  /** @import { Expression, Statement } from 'estree' */
2
2
  /** @import { AST } from '#compiler' */
3
3
  /** @import { ComponentClientTransformState, ComponentContext } from '../types' */
4
- import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../../../constants.js';
5
4
  import * as b from '#compiler/builders';
5
+ import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../../../constants.js';
6
6
  import { clean_nodes, infer_namespace } from '../../utils.js';
7
7
  import { transform_template } from '../transform-template/index.js';
8
+ import { Template } from '../transform-template/template.js';
8
9
  import { process_children } from './shared/fragment.js';
9
10
  import { build_render_statement, Memoizer } from './shared/utils.js';
10
- import { Template } from '../transform-template/template.js';
11
11
 
12
12
  /**
13
13
  * @param {AST.Fragment} node
@@ -60,6 +60,7 @@ export function Fragment(node, context) {
60
60
  const state = {
61
61
  ...context.state,
62
62
  init: [],
63
+ snippets: [],
63
64
  consts: [],
64
65
  let_directives: [],
65
66
  update: [],
@@ -150,7 +151,7 @@ export function Fragment(node, context) {
150
151
  }
151
152
  }
152
153
 
153
- body.push(...state.let_directives, ...state.consts);
154
+ body.push(...state.snippets, ...state.let_directives, ...state.consts);
154
155
 
155
156
  if (state.async_consts && state.async_consts.thunks.length > 0) {
156
157
  body.push(b.var(state.async_consts.id, b.call('$.run', b.array(state.async_consts.thunks))));
@@ -329,7 +329,7 @@ export function RegularElement(node, context) {
329
329
  );
330
330
 
331
331
  /** @type {typeof state} */
332
- const child_state = { ...state, init: [], update: [], after_update: [] };
332
+ const child_state = { ...state, init: [], update: [], after_update: [], snippets: [] };
333
333
 
334
334
  for (const node of hoisted) {
335
335
  context.visit(node, child_state);
@@ -441,6 +441,7 @@ export function RegularElement(node, context) {
441
441
  // Wrap children in `{...}` to avoid declaration conflicts
442
442
  context.state.init.push(
443
443
  b.block([
444
+ ...child_state.snippets,
444
445
  ...child_state.init,
445
446
  ...element_state.init,
446
447
  child_state.update.length > 0 ? build_render_statement(child_state) : b.empty,
@@ -89,6 +89,6 @@ export function SnippetBlock(node, context) {
89
89
  context.state.instance_level_snippets.push(declaration);
90
90
  }
91
91
  } else {
92
- context.state.init.push(declaration);
92
+ context.state.snippets.push(declaration);
93
93
  }
94
94
  }
@@ -77,7 +77,7 @@ export function SvelteBoundary(node, context) {
77
77
  /** @type {Statement[]} */
78
78
  const statements = [];
79
79
 
80
- context.visit(child, { ...context.state, init: statements });
80
+ context.visit(child, { ...context.state, snippets: statements });
81
81
 
82
82
  const snippet = /** @type {VariableDeclaration} */ (statements[0]);
83
83
 
@@ -117,10 +117,10 @@ export function SvelteElement(node, context) {
117
117
  );
118
118
 
119
119
  if (dev) {
120
+ statements.push(b.stmt(b.call('$.validate_dynamic_element_tag', get_tag)));
120
121
  if (node.fragment.nodes.length > 0) {
121
122
  statements.push(b.stmt(b.call('$.validate_void_dynamic_element', get_tag)));
122
123
  }
123
- statements.push(b.stmt(b.call('$.validate_dynamic_element_tag', get_tag)));
124
124
  }
125
125
 
126
126
  const location = dev && locator(node.start);
@@ -333,7 +333,7 @@ export function build_component(node, component_name, loc, context) {
333
333
  // can be used as props without creating conflicts
334
334
  context.visit(child, {
335
335
  ...context.state,
336
- init: snippet_declarations
336
+ snippets: snippet_declarations
337
337
  });
338
338
 
339
339
  push_prop(b.prop('init', child.expression, child.expression));
@@ -15,7 +15,7 @@ export function ConstTag(node, context) {
15
15
  const has_await = node.metadata.expression.has_await;
16
16
  const blockers = [...node.metadata.expression.dependencies]
17
17
  .map((dep) => dep.blocker)
18
- .filter((b) => b !== null);
18
+ .filter((b) => b !== null && b.object !== context.state.async_consts?.id);
19
19
 
20
20
  if (has_await || context.state.async_consts || blockers.length > 0) {
21
21
  const run = (context.state.async_consts ??= {
@@ -30,7 +30,9 @@ export function ConstTag(node, context) {
30
30
  context.state.init.push(b.let(identifier.name));
31
31
  }
32
32
 
33
- if (blockers.length > 0) {
33
+ if (blockers.length === 1) {
34
+ run.thunks.push(b.thunk(/** @type {Expression} */ (blockers[0])));
35
+ } else if (blockers.length > 0) {
34
36
  run.thunks.push(b.thunk(b.call('Promise.all', b.array(blockers))));
35
37
  }
36
38
 
@@ -2,7 +2,7 @@
2
2
  /** @import { AST } from '#compiler' */
3
3
  /** @import { ComponentContext } from '../types.js' */
4
4
  import * as b from '#compiler/builders';
5
- import { create_push } from './shared/utils.js';
5
+ import { block_close, block_open, create_push } from './shared/utils.js';
6
6
 
7
7
  /**
8
8
  * @param {AST.HtmlTag} node
@@ -12,5 +12,15 @@ export function HtmlTag(node, context) {
12
12
  const expression = /** @type {Expression} */ (context.visit(node.expression));
13
13
  const call = b.call('$.html', expression);
14
14
 
15
+ const has_await = node.metadata.expression.has_await;
16
+
17
+ if (has_await) {
18
+ context.state.template.push(block_open);
19
+ }
20
+
15
21
  context.state.template.push(create_push(call, node.metadata.expression, true));
22
+
23
+ if (has_await) {
24
+ context.state.template.push(block_close);
25
+ }
16
26
  }
@@ -29,10 +29,10 @@ export function SvelteElement(node, context) {
29
29
  tag = b.id(tag_id);
30
30
  }
31
31
 
32
+ context.state.init.push(b.stmt(b.call('$.validate_dynamic_element_tag', b.thunk(tag))));
32
33
  if (node.fragment.nodes.length > 0) {
33
34
  context.state.init.push(b.stmt(b.call('$.validate_void_dynamic_element', b.thunk(tag))));
34
35
  }
35
- context.state.init.push(b.stmt(b.call('$.validate_dynamic_element_tag', b.thunk(tag))));
36
36
  }
37
37
 
38
38
  const state = {
@@ -49,22 +49,25 @@ export function transform_body(instance_body, runner, transform) {
49
49
  if (instance_body.async.length > 0) {
50
50
  const thunks = instance_body.async.map((s) => {
51
51
  if (s.node.type === 'VariableDeclarator') {
52
- const visited = /** @type {ESTree.VariableDeclaration} */ (
52
+ const visited = /** @type {ESTree.VariableDeclaration | ESTree.EmptyStatement} */ (
53
53
  transform(b.var(s.node.id, s.node.init))
54
54
  );
55
55
 
56
- const statements = visited.declarations.map((node) => {
57
- if (
58
- node.id.type === 'Identifier' &&
59
- (node.id.name.startsWith('$$d') || node.id.name.startsWith('$$array'))
60
- ) {
61
- // this is an intermediate declaration created in VariableDeclaration.js;
62
- // subsequent statements depend on it
63
- return b.var(node.id, node.init);
64
- }
56
+ const statements =
57
+ visited.type === 'VariableDeclaration'
58
+ ? visited.declarations.map((node) => {
59
+ if (
60
+ node.id.type === 'Identifier' &&
61
+ (node.id.name.startsWith('$$d') || node.id.name.startsWith('$$array'))
62
+ ) {
63
+ // this is an intermediate declaration created in VariableDeclaration.js;
64
+ // subsequent statements depend on it
65
+ return b.var(node.id, node.init);
66
+ }
65
67
 
66
- return b.stmt(b.assignment('=', node.id, node.init ?? b.void0));
67
- });
68
+ return b.stmt(b.assignment('=', node.id, node.init ?? b.void0));
69
+ })
70
+ : [];
68
71
 
69
72
  if (statements.length === 1) {
70
73
  const statement = /** @type {ESTree.ExpressionStatement} */ (statements[0]);
@@ -1,4 +1,4 @@
1
- /** @import { TemplateNode, Value } from '#client' */
1
+ /** @import { Blocker, TemplateNode, Value } from '#client' */
2
2
  import { flatten } from '../../reactivity/async.js';
3
3
  import { Batch, current_batch } from '../../reactivity/batch.js';
4
4
  import { get } from '../../runtime.js';
@@ -14,11 +14,16 @@ import { get_boundary } from './boundary.js';
14
14
 
15
15
  /**
16
16
  * @param {TemplateNode} node
17
- * @param {Array<Promise<void>>} blockers
17
+ * @param {Blocker[]} blockers
18
18
  * @param {Array<() => Promise<any>>} expressions
19
19
  * @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn
20
20
  */
21
21
  export function async(node, blockers = [], expressions = [], fn) {
22
+ if (expressions.length === 0 && blockers.every((b) => b.settled)) {
23
+ fn(node);
24
+ return;
25
+ }
26
+
22
27
  var boundary = get_boundary();
23
28
  var batch = /** @type {Batch} */ (current_batch);
24
29
  var blocking = boundary.is_rendered();
@@ -102,6 +102,7 @@ export class Boundary {
102
102
 
103
103
  #local_pending_count = 0;
104
104
  #pending_count = 0;
105
+ #pending_count_update_queued = false;
105
106
 
106
107
  #is_creating_fallback = false;
107
108
 
@@ -202,12 +203,11 @@ export class Boundary {
202
203
 
203
204
  #hydrate_pending_content() {
204
205
  const pending = this.#props.pending;
205
- if (!pending) {
206
- return;
207
- }
206
+ if (!pending) return;
207
+
208
208
  this.#pending_effect = branch(() => pending(this.#anchor));
209
209
 
210
- Batch.enqueue(() => {
210
+ queue_micro_task(() => {
211
211
  var anchor = this.#get_anchor();
212
212
 
213
213
  this.#main_effect = this.#run(() => {
@@ -359,9 +359,15 @@ export class Boundary {
359
359
 
360
360
  this.#local_pending_count += d;
361
361
 
362
- if (this.#effect_pending) {
363
- internal_set(this.#effect_pending, this.#local_pending_count);
364
- }
362
+ if (!this.#effect_pending || this.#pending_count_update_queued) return;
363
+ this.#pending_count_update_queued = true;
364
+
365
+ queue_micro_task(() => {
366
+ this.#pending_count_update_queued = false;
367
+ if (this.#effect_pending) {
368
+ internal_set(this.#effect_pending, this.#local_pending_count);
369
+ }
370
+ });
365
371
  }
366
372
 
367
373
  get_effect_pending() {
@@ -1,4 +1,4 @@
1
- /** @import { Effect } from '#client' */
1
+ /** @import { Blocker, Effect } from '#client' */
2
2
  import { DEV } from 'esm-env';
3
3
  import { hydrating, set_hydrating } from '../hydration.js';
4
4
  import { get_descriptors, get_prototype_of } from '../../../shared/utils.js';
@@ -483,7 +483,7 @@ function set_attributes(
483
483
  * @param {(...expressions: any) => Record<string | symbol, any>} fn
484
484
  * @param {Array<() => any>} sync
485
485
  * @param {Array<() => Promise<any>>} async
486
- * @param {Array<Promise<void>>} blockers
486
+ * @param {Blocker[]} blockers
487
487
  * @param {string} [css_hash]
488
488
  * @param {boolean} [should_remove_defaults]
489
489
  * @param {boolean} [skip_warning]
@@ -1,4 +1,4 @@
1
- /** @import { Effect, TemplateNode, Value } from '#client' */
1
+ /** @import { Blocker, Effect, Value } from '#client' */
2
2
  import { DESTROYED, STALE_REACTION } from '#client/constants';
3
3
  import { DEV } from 'esm-env';
4
4
  import {
@@ -27,7 +27,7 @@ import {
27
27
  import { aborted } from './effects.js';
28
28
 
29
29
  /**
30
- * @param {Array<Promise<void>>} blockers
30
+ * @param {Blocker[]} blockers
31
31
  * @param {Array<() => any>} sync
32
32
  * @param {Array<() => Promise<any>>} async
33
33
  * @param {(values: Value[]) => any} fn
@@ -35,7 +35,10 @@ import { aborted } from './effects.js';
35
35
  export function flatten(blockers, sync, async, fn) {
36
36
  const d = is_runes() ? derived : derived_safe_equal;
37
37
 
38
- if (async.length === 0 && blockers.length === 0) {
38
+ // Filter out already-settled blockers - no need to wait for them
39
+ var pending = blockers.filter((b) => !b.settled);
40
+
41
+ if (async.length === 0 && pending.length === 0) {
39
42
  fn(sync.map(d));
40
43
  return;
41
44
  }
@@ -44,47 +47,52 @@ export function flatten(blockers, sync, async, fn) {
44
47
  var parent = /** @type {Effect} */ (active_effect);
45
48
 
46
49
  var restore = capture();
50
+ var blocker_promise =
51
+ pending.length === 1
52
+ ? pending[0].promise
53
+ : pending.length > 1
54
+ ? Promise.all(pending.map((b) => b.promise))
55
+ : null;
56
+
57
+ /** @param {Value[]} values */
58
+ function finish(values) {
59
+ restore();
47
60
 
48
- function run() {
49
- Promise.all(async.map((expression) => async_derived(expression)))
50
- .then((result) => {
51
- restore();
61
+ try {
62
+ fn(values);
63
+ } catch (error) {
64
+ if ((parent.f & DESTROYED) === 0) {
65
+ invoke_error_boundary(error, parent);
66
+ }
67
+ }
52
68
 
53
- try {
54
- fn([...sync.map(d), ...result]);
55
- } catch (error) {
56
- // ignore errors in blocks that have already been destroyed
57
- if ((parent.f & DESTROYED) === 0) {
58
- invoke_error_boundary(error, parent);
59
- }
60
- }
69
+ batch?.deactivate();
70
+ unset_context();
71
+ }
61
72
 
62
- batch?.deactivate();
63
- unset_context();
64
- })
65
- .catch((error) => {
66
- invoke_error_boundary(error, parent);
67
- });
73
+ // Fast path: blockers but no async expressions
74
+ if (async.length === 0) {
75
+ /** @type {Promise<any>} */ (blocker_promise).then(() => finish(sync.map(d)));
76
+ return;
68
77
  }
69
78
 
70
- if (blockers.length > 0) {
71
- Promise.all(blockers).then(() => {
72
- restore();
79
+ // Full path: has async expressions
80
+ function run() {
81
+ restore();
82
+ Promise.all(async.map((expression) => async_derived(expression)))
83
+ .then((result) => finish([...sync.map(d), ...result]))
84
+ .catch((error) => invoke_error_boundary(error, parent));
85
+ }
73
86
 
74
- try {
75
- return run();
76
- } finally {
77
- batch?.deactivate();
78
- unset_context();
79
- }
80
- });
87
+ if (blocker_promise) {
88
+ blocker_promise.then(run);
81
89
  } else {
82
90
  run();
83
91
  }
84
92
  }
85
93
 
86
94
  /**
87
- * @param {Array<Promise<void>>} blockers
95
+ * @param {Blocker[]} blockers
88
96
  * @param {(values: Value[]) => any} fn
89
97
  */
90
98
  export function run_after_blockers(blockers, fn) {
@@ -239,7 +247,13 @@ export function run(thunks) {
239
247
 
240
248
  var promise = Promise.resolve(thunks[0]()).catch(handle_error);
241
249
 
242
- var promises = [promise];
250
+ /** @type {Blocker} */
251
+ var blocker = { promise, settled: false };
252
+ var blockers = [blocker];
253
+
254
+ promise.finally(() => {
255
+ blocker.settled = true;
256
+ });
243
257
 
244
258
  for (const fn of thunks.slice(1)) {
245
259
  promise = promise
@@ -255,13 +269,17 @@ export function run(thunks) {
255
269
  restore();
256
270
  return fn();
257
271
  })
258
- .catch(handle_error)
259
- .finally(() => {
260
- unset_context();
261
- current_batch?.deactivate();
262
- });
272
+ .catch(handle_error);
273
+
274
+ const blocker = { promise, settled: false };
275
+ blockers.push(blocker);
263
276
 
264
- promises.push(promise);
277
+ promise.finally(() => {
278
+ blocker.settled = true;
279
+
280
+ unset_context();
281
+ current_batch?.deactivate();
282
+ });
265
283
  }
266
284
 
267
285
  promise
@@ -273,5 +291,12 @@ export function run(thunks) {
273
291
  batch.decrement(blocking);
274
292
  });
275
293
 
276
- return promises;
294
+ return blockers;
295
+ }
296
+
297
+ /**
298
+ * @param {Blocker[]} blockers
299
+ */
300
+ export function wait(blockers) {
301
+ return Promise.all(blockers.map((b) => b.promise));
277
302
  }