svelte 5.55.9 → 5.56.0

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 (49) hide show
  1. package/compiler/index.js +1 -1
  2. package/package.json +8 -5
  3. package/src/compiler/errors.js +18 -0
  4. package/src/compiler/legacy.js +4 -0
  5. package/src/compiler/phases/1-parse/acorn.js +44 -1
  6. package/src/compiler/phases/1-parse/index.js +4 -1
  7. package/src/compiler/phases/1-parse/state/tag.js +91 -3
  8. package/src/compiler/phases/2-analyze/index.js +5 -0
  9. package/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +1 -2
  10. package/src/compiler/phases/2-analyze/visitors/CallExpression.js +3 -0
  11. package/src/compiler/phases/2-analyze/visitors/ConstTag.js +2 -25
  12. package/src/compiler/phases/2-analyze/visitors/DeclarationTag.js +58 -0
  13. package/src/compiler/phases/2-analyze/visitors/Identifier.js +1 -1
  14. package/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js +1 -1
  15. package/src/compiler/phases/3-transform/client/transform-client.js +5 -15
  16. package/src/compiler/phases/3-transform/client/transform-template/index.js +40 -3
  17. package/src/compiler/phases/3-transform/client/utils.js +21 -0
  18. package/src/compiler/phases/3-transform/client/visitors/ConstTag.js +13 -24
  19. package/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js +87 -0
  20. package/src/compiler/phases/3-transform/client/visitors/Fragment.js +2 -5
  21. package/src/compiler/phases/3-transform/client/visitors/RegularElement.js +32 -10
  22. package/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js +7 -2
  23. package/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +14 -11
  24. package/src/compiler/phases/3-transform/client/visitors/shared/utils.js +1 -1
  25. package/src/compiler/phases/3-transform/server/transform-server.js +2 -0
  26. package/src/compiler/phases/3-transform/server/visitors/ConstTag.js +9 -24
  27. package/src/compiler/phases/3-transform/server/visitors/DeclarationTag.js +85 -0
  28. package/src/compiler/phases/3-transform/server/visitors/RegularElement.js +24 -7
  29. package/src/compiler/phases/3-transform/utils.js +1 -0
  30. package/src/compiler/phases/nodes.js +3 -2
  31. package/src/compiler/print/index.js +42 -0
  32. package/src/compiler/utils/builders.js +2 -1
  33. package/src/compiler/warnings.js +6 -5
  34. package/src/internal/client/dom/blocks/boundary.js +1 -1
  35. package/src/internal/client/dom/blocks/branches.js +2 -0
  36. package/src/internal/client/dom/blocks/svelte-element.js +5 -3
  37. package/src/internal/client/dom/elements/events.js +5 -10
  38. package/src/internal/client/dom/operations.js +12 -2
  39. package/src/internal/client/reactivity/async.js +4 -4
  40. package/src/internal/client/reactivity/batch.js +70 -78
  41. package/src/internal/client/reactivity/deriveds.js +7 -4
  42. package/src/internal/client/reactivity/effects.js +5 -2
  43. package/src/internal/client/reactivity/props.js +6 -6
  44. package/src/internal/client/reactivity/sources.js +1 -2
  45. package/src/internal/client/runtime.js +4 -8
  46. package/src/utils.js +1 -1
  47. package/src/version.js +1 -1
  48. package/types/index.d.ts +7 -0
  49. package/types/index.d.ts.map +1 -1
@@ -603,6 +603,48 @@ const svelte_visitors = (comments) => ({
603
603
  context.write('}');
604
604
  },
605
605
 
606
+ DeclarationTag(node, context) {
607
+ context.write('{');
608
+
609
+ // This is duplicated from esrap's handling of VariableDeclaration,
610
+ // which we need to do in order to omit the trailing semicolon that esrap would add.
611
+ const open = context.new();
612
+ const join = context.new();
613
+ const child_context = context.new();
614
+
615
+ context.append(child_context);
616
+
617
+ child_context.write(`${node.declaration.kind} `);
618
+ child_context.append(open);
619
+
620
+ const declarations = node.declaration.declarations;
621
+ let first = true;
622
+
623
+ for (const d of declarations) {
624
+ if (!first) child_context.append(join);
625
+ first = false;
626
+
627
+ child_context.visit(d);
628
+ }
629
+
630
+ const length = child_context.measure() + 2 * (declarations.length - 1);
631
+
632
+ const multiline = child_context.multiline || (declarations.length > 1 && length > 50);
633
+
634
+ if (multiline) {
635
+ context.multiline = true;
636
+
637
+ if (declarations.length > 1) open.indent();
638
+ join.write(',');
639
+ join.newline();
640
+ if (declarations.length > 1) context.dedent();
641
+ } else {
642
+ join.write(', ');
643
+ }
644
+
645
+ context.write('}');
646
+ },
647
+
606
648
  DebugTag(node, context) {
607
649
  context.write('{@debug ');
608
650
  let started = false;
@@ -686,7 +686,8 @@ export {
686
686
  if_builder as if,
687
687
  this_instance as this,
688
688
  null_instance as null,
689
- debugger_builder as debugger
689
+ debugger_builder as debugger,
690
+ new_builder as new
690
691
  };
691
692
 
692
693
  /**
@@ -166,11 +166,12 @@ export function a11y_autofocus(node) {
166
166
  }
167
167
 
168
168
  /**
169
- * Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
169
+ * Visible, non-interactive element `<%element%>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
170
170
  * @param {null | NodeLike} node
171
+ * @param {string} element
171
172
  */
172
- export function a11y_click_events_have_key_events(node) {
173
- w(node, 'a11y_click_events_have_key_events', `Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as \`<button type="button">\` or \`<a>\` might be more appropriate\nhttps://svelte.dev/e/a11y_click_events_have_key_events`);
173
+ export function a11y_click_events_have_key_events(node, element) {
174
+ w(node, 'a11y_click_events_have_key_events', `Visible, non-interactive element \`<${element}>\` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as \`<button type="button">\` or \`<a>\` might be more appropriate\nhttps://svelte.dev/e/a11y_click_events_have_key_events`);
174
175
  }
175
176
 
176
177
  /**
@@ -803,11 +804,11 @@ export function script_context_deprecated(node) {
803
804
  }
804
805
 
805
806
  /**
806
- * Unrecognized attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it
807
+ * Unrecognised attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it
807
808
  * @param {null | NodeLike} node
808
809
  */
809
810
  export function script_unknown_attribute(node) {
810
- w(node, 'script_unknown_attribute', `Unrecognized attribute — should be one of \`generics\`, \`lang\` or \`module\`. If this exists for a preprocessor, ensure that the preprocessor removes it\nhttps://svelte.dev/e/script_unknown_attribute`);
811
+ w(node, 'script_unknown_attribute', `Unrecognised attribute — should be one of \`generics\`, \`lang\` or \`module\`. If this exists for a preprocessor, ensure that the preprocessor removes it\nhttps://svelte.dev/e/script_unknown_attribute`);
811
812
  }
812
813
 
813
814
  /**
@@ -396,7 +396,7 @@ export class Boundary {
396
396
  if (this.#pending_effect) current_batch.skip_effect(this.#pending_effect);
397
397
  if (this.#failed_effect) current_batch.skip_effect(this.#failed_effect);
398
398
 
399
- current_batch.on_fork_commit(() => {
399
+ current_batch.oncommit(() => {
400
400
  this.#handle_error(error);
401
401
  });
402
402
  } else {
@@ -90,6 +90,8 @@ export class BranchManager {
90
90
  var offscreen = this.#offscreen.get(key);
91
91
 
92
92
  if (offscreen) {
93
+ // effect could have been outro'ed before through a prior batch — resume if necessary
94
+ resume_effect(offscreen.effect);
93
95
  this.#onscreen.set(key, offscreen.effect);
94
96
  this.#offscreen.delete(key);
95
97
 
@@ -88,9 +88,11 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
88
88
  assign_nodes(element, element);
89
89
 
90
90
  if (render_fn) {
91
+ var tmp_comment = null;
92
+
91
93
  if (hydrating && is_raw_text_element(next_tag)) {
92
- // prevent hydration glitches
93
- element.append(document.createComment(''));
94
+ // prevent hydration glitches (code just below expects an anchor)
95
+ element.append((tmp_comment = document.createComment('')));
94
96
  }
95
97
 
96
98
  // If hydrating, use the existing ssr comment as the anchor so that the
@@ -114,7 +116,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
114
116
  // contains children, it's a user error (which is warned on elsewhere)
115
117
  // and the DOM will be silently discarded
116
118
  render_fn(element, child_anchor);
117
-
119
+ tmp_comment?.remove();
118
120
  set_animation_effect_override(null);
119
121
  }
120
122
 
@@ -257,12 +257,7 @@ export function handle_event_propagation(event) {
257
257
  var other_errors = [];
258
258
 
259
259
  while (current_target !== null) {
260
- /** @type {null | Element} */
261
- var parent_element =
262
- current_target.assignedSlot ||
263
- current_target.parentNode ||
264
- /** @type {any} */ (current_target).host ||
265
- null;
260
+ if (current_target === handler_element) break;
266
261
 
267
262
  try {
268
263
  // @ts-expect-error
@@ -284,10 +279,10 @@ export function handle_event_propagation(event) {
284
279
  throw_error = error;
285
280
  }
286
281
  }
287
- if (event.cancelBubble || parent_element === handler_element || parent_element === null) {
288
- break;
289
- }
290
- current_target = parent_element;
282
+ if (event.cancelBubble) break;
283
+
284
+ path_idx++;
285
+ current_target = path_idx < path.length ? /** @type {Element} */ (path[path_idx]) : null;
291
286
  }
292
287
 
293
288
  if (throw_error) {
@@ -233,6 +233,12 @@ export function should_defer_append() {
233
233
  }
234
234
 
235
235
  /**
236
+ * Branching here is intentional and load-bearing for perf. `createElement(tag)`
237
+ * hits a fast path in Blink that `createElementNS(NAMESPACE_HTML, tag)` doesn't,
238
+ * and passing an explicit `undefined` as the trailing options arg measurably
239
+ * slows both APIs. Funnelling every case through a single `createElementNS(ns,
240
+ * tag, options)` call would be smaller but slower on the HTML path.
241
+ *
236
242
  * @template {keyof HTMLElementTagNameMap | string} T
237
243
  * @param {T} tag
238
244
  * @param {string} [namespace]
@@ -240,9 +246,13 @@ export function should_defer_append() {
240
246
  * @returns {T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element}
241
247
  */
242
248
  export function create_element(tag, namespace, is) {
243
- let options = is ? { is } : undefined;
249
+ if (namespace == null || namespace === NAMESPACE_HTML) {
250
+ return /** @type {T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element} */ (
251
+ is ? document.createElement(tag, { is }) : document.createElement(tag)
252
+ );
253
+ }
244
254
  return /** @type {T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element} */ (
245
- document.createElementNS(namespace ?? NAMESPACE_HTML, tag, options)
255
+ is ? document.createElementNS(namespace, tag, { is }) : document.createElementNS(namespace, tag)
246
256
  );
247
257
  }
248
258
 
@@ -352,15 +352,15 @@ export function wait(blockers) {
352
352
  */
353
353
  export function increment_pending() {
354
354
  var effect = /** @type {Effect} */ (active_effect);
355
- var boundary = /** @type {Boundary} */ (effect.b);
355
+ var boundary = effect.b; // undefined if called outside the render tree, e.g. a standalone $effect.root
356
356
  var batch = /** @type {Batch} */ (current_batch);
357
- var blocking = boundary.is_rendered();
357
+ var blocking = !!boundary?.is_rendered();
358
358
 
359
- boundary.update_pending_count(1, batch);
359
+ boundary?.update_pending_count(1, batch);
360
360
  batch.increment(blocking, effect);
361
361
 
362
362
  return () => {
363
- boundary.update_pending_count(-1, batch);
363
+ boundary?.update_pending_count(-1, batch);
364
364
  batch.decrement(blocking, effect);
365
365
  };
366
366
  }
@@ -127,13 +127,6 @@ export class Batch {
127
127
  */
128
128
  previous = new Map();
129
129
 
130
- /**
131
- * Async effects which this batch doesn't take into account anymore when calculating blockers,
132
- * as it has a value for it already.
133
- * @type {Set<Effect>}
134
- */
135
- unblocked = new Set();
136
-
137
130
  /**
138
131
  * When the batch is committed (and the DOM is updated), we need to remove old branches
139
132
  * and append new ones by calling the functions added inside (if/each/key/etc) blocks
@@ -147,12 +140,6 @@ export class Batch {
147
140
  */
148
141
  #discard_callbacks = new Set();
149
142
 
150
- /**
151
- * Callbacks that should run only when a fork is committed.
152
- * @type {Set<(batch: Batch) => void>}
153
- */
154
- #fork_commit_callbacks = new Set();
155
-
156
143
  /**
157
144
  * The number of async effects that are currently in flight
158
145
  */
@@ -214,6 +201,18 @@ export class Batch {
214
201
 
215
202
  #decrement_queued = false;
216
203
 
204
+ constructor() {
205
+ // link batch
206
+ if (last_batch === null) {
207
+ first_batch = last_batch = this;
208
+ } else {
209
+ last_batch.#next = this;
210
+ this.#prev = last_batch;
211
+ }
212
+
213
+ last_batch = this;
214
+ }
215
+
217
216
  #is_deferred() {
218
217
  if (this.is_fork) return true;
219
218
 
@@ -289,19 +288,19 @@ export class Batch {
289
288
  }
290
289
  }
291
290
 
292
- // we only reschedule previously-deferred effects if we expect
293
- // to be able to run them after processing the batch
294
- if (!this.#is_deferred()) {
295
- for (const e of this.#dirty_effects) {
296
- this.#maybe_dirty_effects.delete(e);
297
- set_signal_status(e, DIRTY);
298
- this.schedule(e);
299
- }
291
+ // We always reschedule previously-deferred effects, not just when
292
+ // #is_deferred() is true, because traversing the tree could make
293
+ // an if block that contains the last blocking pending effect falsy,
294
+ // causing the block to no longer be deferred.
295
+ for (const e of this.#dirty_effects) {
296
+ this.#maybe_dirty_effects.delete(e);
297
+ set_signal_status(e, DIRTY);
298
+ this.schedule(e);
299
+ }
300
300
 
301
- for (const e of this.#maybe_dirty_effects) {
302
- set_signal_status(e, MAYBE_DIRTY);
303
- this.schedule(e);
304
- }
301
+ for (const e of this.#maybe_dirty_effects) {
302
+ set_signal_status(e, MAYBE_DIRTY);
303
+ this.schedule(e);
305
304
  }
306
305
 
307
306
  const roots = this.#roots;
@@ -326,6 +325,12 @@ export class Batch {
326
325
  this.#traverse(root, effects, render_effects);
327
326
  } catch (e) {
328
327
  reset_all(root);
328
+ // If there's no async work left, this branch is now dead and needs
329
+ // to be discarded to not become a zombie that is never cleaned up.
330
+ // See https://github.com/sveltejs/svelte/issues/18221#issuecomment-4497918414
331
+ // for a (non-minimal) reproduction that demonstrates a case where this is necessary
332
+ // to not get follow-up false-positives via "batch has scheduled roots" invariant errors.
333
+ if (!this.#is_deferred()) this.discard();
329
334
  throw e;
330
335
  }
331
336
  }
@@ -362,6 +367,10 @@ export class Batch {
362
367
  const earlier_batch = this.#find_earlier_batch();
363
368
 
364
369
  if (earlier_batch) {
370
+ // If this batch collected deferred effects during traversal, they still need
371
+ // to run after being merged into the earlier batch.
372
+ this.#defer_effects(render_effects);
373
+ this.#defer_effects(effects);
365
374
  earlier_batch.#merge(this);
366
375
  return;
367
376
  }
@@ -383,31 +392,30 @@ export class Batch {
383
392
 
384
393
  var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch));
385
394
 
386
- if (this.linked && this.#pending === 0) {
395
+ if (this.#pending === 0 && (this.#roots.length === 0 || next_batch !== null)) {
387
396
  this.#unlink();
388
- }
389
397
 
390
- // Order matters here - we need to commit and THEN continue flushing new batches, not the other way around,
391
- // else we could start flushing a new batch and then, if it has pending work, rebase it right afterwards, which is wrong.
392
- // In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode
393
- // TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed
394
- if (async_mode_flag && !this.linked) {
395
- this.#commit();
396
- // Rebases can activate other batches or null it out, therefore restore the new one here
397
- current_batch = next_batch;
398
+ // Order matters here - we need to commit and THEN continue flushing new batches, not the other way around,
399
+ // else we could start flushing a new batch and then, if it has pending work, rebase it right afterwards, which is wrong.
400
+ // In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode
401
+ // TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed
402
+ if (async_mode_flag) {
403
+ this.#commit();
404
+ // Rebases can activate other batches or null it out, therefore restore the new one here
405
+ current_batch = next_batch;
406
+ }
398
407
  }
399
408
 
400
409
  // Edge case: During traversal new branches might create effects that run immediately and set state,
401
410
  // causing an effect and therefore a root to be scheduled again. We need to traverse the current batch
402
411
  // once more in that case - most of the time this will just clean up dirty branches.
403
412
  if (this.#roots.length > 0) {
404
- if (next_batch === null) {
413
+ if (next_batch !== null) {
414
+ const batch = next_batch;
415
+ batch.#roots.push(...this.#roots.filter((r) => !batch.#roots.includes(r)));
416
+ } else {
405
417
  next_batch = this;
406
- this.#link();
407
418
  }
408
-
409
- const batch = next_batch;
410
- batch.#roots.push(...this.#roots.filter((r) => !batch.#roots.includes(r)));
411
419
  }
412
420
 
413
421
  if (next_batch !== null) {
@@ -500,9 +508,12 @@ export class Batch {
500
508
 
501
509
  for (const [effect, deferred] of batch.async_deriveds) {
502
510
  const d = this.async_deriveds.get(effect);
503
- if (d) deferred.promise.then(d.resolve);
511
+ if (d) deferred.promise.then(d.resolve).catch(d.reject);
504
512
  }
505
513
 
514
+ // Mark is not guaranteed not touch these, so we transfer them
515
+ this.transfer_effects(batch.#dirty_effects, batch.#maybe_dirty_effects);
516
+
506
517
  /**
507
518
  * mark all effects that depend on `batch.current`, except the
508
519
  * async effects that we just resolved (TODO unless they depend
@@ -617,9 +628,9 @@ export class Batch {
617
628
  discard() {
618
629
  for (const fn of this.#discard_callbacks) fn(this);
619
630
  this.#discard_callbacks.clear();
620
- this.#fork_commit_callbacks.clear();
621
631
 
622
632
  this.#unlink();
633
+ this.#deferred?.resolve();
623
634
  }
624
635
 
625
636
  /**
@@ -630,8 +641,6 @@ export class Batch {
630
641
  }
631
642
 
632
643
  #commit() {
633
- this.#unlink();
634
-
635
644
  // If there are other pending batches, they now need to be 'rebased' —
636
645
  // in other words, we re-run block/async effects with the newly
637
646
  // committed state, unless the batch in question has a more
@@ -664,14 +673,16 @@ export class Batch {
664
673
  // immediately resolving them? Likely not because of how this.apply() works.
665
674
  for (const [effect, deferred] of this.async_deriveds) {
666
675
  const d = batch.async_deriveds.get(effect);
667
- if (d) deferred.promise.then(d.resolve);
676
+ if (d) deferred.promise.then(d.resolve).catch(d.reject);
668
677
  }
669
678
  }
670
679
 
671
680
  if (!batch.#started) continue;
672
681
 
673
- // Re-run async/block effects that depend on distinct values changed in both batches
674
- var others = [...batch.current.keys()].filter((s) => !this.current.has(s));
682
+ // Re-run async/block effects that depend on distinct values changed in both batches (ignoring deriveds)
683
+ var others = [...batch.current.keys()].filter(
684
+ (s) => !(/** @type {[any, boolean]} */ (batch.current.get(s))[1]) && !this.current.has(s)
685
+ );
675
686
 
676
687
  if (others.length === 0) {
677
688
  if (is_earlier) {
@@ -711,11 +722,14 @@ export class Batch {
711
722
  }
712
723
 
713
724
  checked = new Map();
714
- var current_unequal = [...batch.current.keys()].filter((c) =>
715
- this.current.has(c)
716
- ? /** @type {[any, boolean]} */ (this.current.get(c))[0] !== c.v
717
- : true
718
- );
725
+ var current_unequal = [...batch.current]
726
+ .filter(([c, v1]) => {
727
+ const v2 = this.current.get(c);
728
+ if (!v2) return true;
729
+ // Either their values are different or one is a derived but not the other
730
+ return v2[0] !== v1[0] || v2[1] !== v1[1];
731
+ })
732
+ .map(([c]) => c);
719
733
 
720
734
  if (current_unequal.length > 0) {
721
735
  for (const effect of this.#new_effects) {
@@ -819,16 +833,6 @@ export class Batch {
819
833
  this.#discard_callbacks.add(fn);
820
834
  }
821
835
 
822
- /** @param {(batch: Batch) => void} fn */
823
- on_fork_commit(fn) {
824
- this.#fork_commit_callbacks.add(fn);
825
- }
826
-
827
- run_fork_commit_callbacks() {
828
- for (const fn of this.#fork_commit_callbacks) fn(this);
829
- this.#fork_commit_callbacks.clear();
830
- }
831
-
832
836
  settled() {
833
837
  return (this.#deferred ??= deferred()).promise;
834
838
  }
@@ -836,7 +840,6 @@ export class Batch {
836
840
  static ensure() {
837
841
  if (current_batch === null) {
838
842
  const batch = (current_batch = new Batch());
839
- batch.#link();
840
843
 
841
844
  if (!is_processing && !is_flushing_sync) {
842
845
  queue_micro_task(() => {
@@ -956,18 +959,11 @@ export class Batch {
956
959
  this.#roots.push(e);
957
960
  }
958
961
 
959
- #link() {
960
- if (last_batch === null) {
961
- first_batch = last_batch = this;
962
- } else {
963
- last_batch.#next = this;
964
- this.#prev = last_batch;
965
- }
966
-
967
- last_batch = this;
968
- }
969
-
970
962
  #unlink() {
963
+ // #merge calls #unlink, discard later on does it again - prevent
964
+ // running it multiple times to not corrupt the linked list
965
+ if (!this.linked) return;
966
+
971
967
  var prev = this.#prev;
972
968
  var next = this.#next;
973
969
 
@@ -1397,10 +1393,6 @@ export function fork(fn) {
1397
1393
  source.wv = increment_write_version();
1398
1394
  }
1399
1395
 
1400
- batch.activate();
1401
- batch.run_fork_commit_callbacks();
1402
- batch.deactivate();
1403
-
1404
1396
  // trigger any `$state.eager(...)` expressions with the new state.
1405
1397
  // eager effects don't get scheduled like other effects, so we
1406
1398
  // can't just encounter them during traversal, we need to
@@ -187,7 +187,10 @@ export function async_derived(fn, label, location) {
187
187
  var decrement_pending = increment_pending();
188
188
  }
189
189
 
190
- if (/** @type {Boundary} */ (parent.b).is_rendered()) {
190
+ if (
191
+ // boundary can be null if the async derived is inside an $effect.root not connected to the component render tree
192
+ parent.b?.is_rendered()
193
+ ) {
191
194
  batch.async_deriveds.get(effect)?.reject(OBSOLETE);
192
195
  } else {
193
196
  // While the boundary is still showing pending, a new run supersedes all older in-flight runs
@@ -227,9 +230,7 @@ export function async_derived(fn, label, location) {
227
230
  signal.f ^= ERROR_VALUE;
228
231
  }
229
232
 
230
- internal_set(signal, value);
231
-
232
- if (DEV && location !== undefined) {
233
+ if (DEV && location !== undefined && !signal.equals(value)) {
233
234
  recent_async_deriveds.add(signal);
234
235
 
235
236
  setTimeout(() => {
@@ -239,6 +240,8 @@ export function async_derived(fn, label, location) {
239
240
  }
240
241
  });
241
242
  }
243
+
244
+ internal_set(signal, value);
242
245
  }
243
246
 
244
247
  batch.deactivate();
@@ -20,7 +20,6 @@ import {
20
20
  EFFECT,
21
21
  DESTROYED,
22
22
  INERT,
23
- REACTION_RAN,
24
23
  BLOCK_EFFECT,
25
24
  ROOT_EFFECT,
26
25
  EFFECT_TRANSPARENT,
@@ -213,7 +212,11 @@ export function user_effect(fn) {
213
212
  // Non-nested `$effect(...)` in a component should be deferred
214
213
  // until the component is mounted
215
214
  var flags = /** @type {Effect} */ (active_effect).f;
216
- var defer = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & REACTION_RAN) === 0;
215
+ var defer =
216
+ !active_reaction &&
217
+ (flags & BRANCH_EFFECT) !== 0 &&
218
+ component_context !== null &&
219
+ !component_context.i;
217
220
 
218
221
  if (defer) {
219
222
  // Top-level `$effect(...)` in an unmounted component — defer until mount
@@ -49,11 +49,11 @@ export function update_pre_prop(fn, d = 1) {
49
49
  /**
50
50
  * The proxy handler for rest props (i.e. `const { x, ...rest } = $props()`).
51
51
  * Is passed the full `$$props` object and excludes the named props.
52
- * @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol>, name?: string }>}}
52
+ * @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Set<string | symbol>, name?: string }>}}
53
53
  */
54
54
  const rest_props_handler = {
55
55
  get(target, key) {
56
- if (target.exclude.includes(key)) return;
56
+ if (target.exclude.has(key)) return;
57
57
  return target.props[key];
58
58
  },
59
59
  set(target, key) {
@@ -65,7 +65,7 @@ const rest_props_handler = {
65
65
  return false;
66
66
  },
67
67
  getOwnPropertyDescriptor(target, key) {
68
- if (target.exclude.includes(key)) return;
68
+ if (target.exclude.has(key)) return;
69
69
  if (key in target.props) {
70
70
  return {
71
71
  enumerable: true,
@@ -75,17 +75,17 @@ const rest_props_handler = {
75
75
  }
76
76
  },
77
77
  has(target, key) {
78
- if (target.exclude.includes(key)) return false;
78
+ if (target.exclude.has(key)) return false;
79
79
  return key in target.props;
80
80
  },
81
81
  ownKeys(target) {
82
- return Reflect.ownKeys(target.props).filter((key) => !target.exclude.includes(key));
82
+ return Reflect.ownKeys(target.props).filter((key) => !target.exclude.has(key));
83
83
  }
84
84
  };
85
85
 
86
86
  /**
87
87
  * @param {Record<string, unknown>} props
88
- * @param {string[]} exclude
88
+ * @param {Set<string>} exclude
89
89
  * @param {string} [name]
90
90
  * @returns {Record<string, unknown>}
91
91
  */
@@ -32,7 +32,6 @@ import {
32
32
  } from '#client/constants';
33
33
  import * as e from '../errors.js';
34
34
  import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
35
- import { includes } from '../../shared/utils.js';
36
35
  import { tag_proxy } from '../dev/tracing.js';
37
36
  import { get_error } from '../../shared/dev.js';
38
37
  import { component_context, is_runes } from '../context.js';
@@ -158,7 +157,7 @@ export function set(source, value, should_proxy = false) {
158
157
  (!untracking || (active_reaction.f & EAGER_EFFECT) !== 0) &&
159
158
  is_runes() &&
160
159
  (active_reaction.f & (DERIVED | BLOCK_EFFECT | ASYNC | EAGER_EFFECT)) !== 0 &&
161
- (current_sources === null || !includes.call(current_sources, source))
160
+ (current_sources === null || !current_sources.has(source))
162
161
  ) {
163
162
  e.state_unsafe_mutation();
164
163
  }
@@ -90,18 +90,14 @@ export function set_active_effect(effect) {
90
90
  /**
91
91
  * When sources are created within a reaction, reading and writing
92
92
  * them within that reaction should not cause a re-run
93
- * @type {null | Source[]}
93
+ * @type {null | Set<Source>}
94
94
  */
95
95
  export let current_sources = null;
96
96
 
97
97
  /** @param {Value} value */
98
98
  export function push_reaction_value(value) {
99
99
  if (active_reaction !== null && (!async_mode_flag || (active_reaction.f & DERIVED) !== 0)) {
100
- if (current_sources === null) {
101
- current_sources = [value];
102
- } else {
103
- current_sources.push(value);
104
- }
100
+ (current_sources ??= new Set()).add(value);
105
101
  }
106
102
  }
107
103
 
@@ -202,7 +198,7 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true)
202
198
  var reactions = signal.reactions;
203
199
  if (reactions === null) return;
204
200
 
205
- if (!async_mode_flag && current_sources !== null && includes.call(current_sources, signal)) {
201
+ if (!async_mode_flag && current_sources !== null && current_sources.has(signal)) {
206
202
  return;
207
203
  }
208
204
 
@@ -540,7 +536,7 @@ export function get(signal) {
540
536
  // we don't add the dependency, because that would create a memory leak
541
537
  var destroyed = active_effect !== null && (active_effect.f & DESTROYED) !== 0;
542
538
 
543
- if (!destroyed && (current_sources === null || !includes.call(current_sources, signal))) {
539
+ if (!destroyed && (current_sources === null || !current_sources.has(signal))) {
544
540
  var deps = active_reaction.deps;
545
541
 
546
542
  if ((active_reaction.f & REACTION_IS_UPDATING) !== 0) {
package/src/utils.js CHANGED
@@ -434,7 +434,7 @@ const STATE_CREATION_RUNES = /** @type {const} */ ([
434
434
  '$derived.by'
435
435
  ]);
436
436
 
437
- const RUNES = /** @type {const} */ ([
437
+ export const RUNES = /** @type {const} */ ([
438
438
  ...STATE_CREATION_RUNES,
439
439
  '$state.eager',
440
440
  '$state.snapshot',
package/src/version.js CHANGED
@@ -4,5 +4,5 @@
4
4
  * The current version, as set in package.json.
5
5
  * @type {string}
6
6
  */
7
- export const VERSION = '5.55.9';
7
+ export const VERSION = '5.56.0';
8
8
  export const PUBLIC_VERSION = '5';