svelte 5.55.4 → 5.55.6

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.
@@ -16,7 +16,8 @@ import {
16
16
  EAGER_EFFECT,
17
17
  ERROR_VALUE,
18
18
  MANAGED_EFFECT,
19
- REACTION_RAN
19
+ REACTION_RAN,
20
+ DESTROYING
20
21
  } from '#client/constants';
21
22
  import { async_mode_flag } from '../../flags/index.js';
22
23
  import { deferred, define_property, includes } from '../../shared/utils.js';
@@ -33,7 +34,7 @@ import { flush_tasks, queue_micro_task } from '../dom/task.js';
33
34
  import { DEV } from 'esm-env';
34
35
  import { invoke_error_boundary } from '../error-handling.js';
35
36
  import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js';
36
- import { eager_effect, unlink_effect } from './effects.js';
37
+ import { eager_effect, teardown, unlink_effect } from './effects.js';
37
38
  import { defer_effect } from './utils.js';
38
39
  import { UNINITIALIZED } from '../../../constants.js';
39
40
  import { set_signal_status } from './status.js';
@@ -41,8 +42,11 @@ import { legacy_is_updating_store } from './store.js';
41
42
  import { invariant } from '../../shared/dev.js';
42
43
  import { log_effect_tree } from '../dev/debug.js';
43
44
 
44
- /** @type {Set<Batch>} */
45
- const batches = new Set();
45
+ /** @type {Batch | null} */
46
+ let first_batch = null;
47
+
48
+ /** @type {Batch | null} */
49
+ let last_batch = null;
46
50
 
47
51
  /** @type {Batch | null} */
48
52
  export let current_batch = null;
@@ -85,13 +89,29 @@ export let collected_effects = null;
85
89
  export let legacy_updates = null;
86
90
 
87
91
  var flush_count = 0;
88
- var source_stacks = DEV ? new Set() : null;
92
+
93
+ /** @type {Set<Value>} */
94
+ var source_stacks = new Set();
89
95
 
90
96
  let uid = 1;
91
97
 
92
98
  export class Batch {
93
99
  id = uid++;
94
100
 
101
+ /** True as soon as `#process` was called */
102
+ #started = false;
103
+
104
+ linked = true;
105
+
106
+ /** @type {Batch | null} */
107
+ #prev = null;
108
+
109
+ /** @type {Batch | null} */
110
+ #next = null;
111
+
112
+ /** @type {Map<Effect, ReturnType<typeof deferred<any>>>} */
113
+ async_deriveds = new Map();
114
+
95
115
  /**
96
116
  * The current values of any signals that are updated in this batch.
97
117
  * Tuple format: [value, is_derived] (note: is_derived is false for deriveds, too, if they were overridden via assignment)
@@ -107,6 +127,13 @@ export class Batch {
107
127
  */
108
128
  previous = new Map();
109
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
+
110
137
  /**
111
138
  * When the batch is committed (and the DOM is updated), we need to remove old branches
112
139
  * and append new ones by calling the functions added inside (if/each/key/etc) blocks
@@ -127,10 +154,9 @@ export class Batch {
127
154
  #fork_commit_callbacks = new Set();
128
155
 
129
156
  /**
130
- * Async effects that are currently in flight
131
- * @type {Map<Effect, number>}
157
+ * The number of async effects that are currently in flight
132
158
  */
133
- #pending = new Map();
159
+ #pending = 0;
134
160
 
135
161
  /**
136
162
  * Async effects that are currently in flight, _not_ inside a pending boundary
@@ -188,31 +214,24 @@ export class Batch {
188
214
 
189
215
  #decrement_queued = false;
190
216
 
191
- /** @type {Set<Batch>} */
192
- #blockers = new Set();
193
-
194
217
  #is_deferred() {
195
- return this.is_fork || this.#blocking_pending.size > 0;
196
- }
218
+ if (this.is_fork) return true;
197
219
 
198
- #is_blocked() {
199
- for (const batch of this.#blockers) {
200
- for (const effect of batch.#blocking_pending.keys()) {
201
- var skipped = false;
202
- var e = effect;
220
+ for (const effect of this.#blocking_pending.keys()) {
221
+ var e = effect;
222
+ var skipped = false;
203
223
 
204
- while (e.parent !== null) {
205
- if (this.#skipped_branches.has(e)) {
206
- skipped = true;
207
- break;
208
- }
209
-
210
- e = e.parent;
224
+ while (e.parent !== null) {
225
+ if (this.#skipped_branches.has(e)) {
226
+ skipped = true;
227
+ break;
211
228
  }
212
229
 
213
- if (!skipped) {
214
- return true;
215
- }
230
+ e = e.parent;
231
+ }
232
+
233
+ if (!skipped) {
234
+ return true;
216
235
  }
217
236
  }
218
237
 
@@ -255,11 +274,21 @@ export class Batch {
255
274
  }
256
275
 
257
276
  #process() {
277
+ this.#started = true;
278
+
258
279
  if (flush_count++ > 1000) {
259
- batches.delete(this);
280
+ this.#unlink();
260
281
  infinite_loop_guard();
261
282
  }
262
283
 
284
+ if (DEV) {
285
+ // track all the values that were updated during this flush,
286
+ // so that they can be reset afterwards
287
+ for (const value of this.current.keys()) {
288
+ source_stacks.add(value);
289
+ }
290
+ }
291
+
263
292
  // we only reschedule previously-deferred effects if we expect
264
293
  // to be able to run them after processing the batch
265
294
  if (!this.#is_deferred()) {
@@ -314,61 +343,76 @@ export class Batch {
314
343
  collected_effects = null;
315
344
  legacy_updates = null;
316
345
 
317
- if (this.#is_deferred() || this.#is_blocked()) {
346
+ // if the batch has outstanding pending work, stash effects and bail
347
+ if (this.#is_deferred()) {
318
348
  this.#defer_effects(render_effects);
319
349
  this.#defer_effects(effects);
320
350
 
321
351
  for (const [e, t] of this.#skipped_branches) {
322
352
  reset_branch(e, t);
323
353
  }
324
- } else {
325
- if (this.#pending.size === 0) {
326
- batches.delete(this);
327
- }
328
354
 
329
- // clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches.
330
- this.#dirty_effects.clear();
331
- this.#maybe_dirty_effects.clear();
355
+ if (updates.length > 0) {
356
+ /** @type {Batch} */ (/** @type {unknown} */ (current_batch)).#process();
357
+ }
332
358
 
333
- // append/remove branches
334
- for (const fn of this.#commit_callbacks) fn(this);
335
- this.#commit_callbacks.clear();
359
+ return;
360
+ }
336
361
 
337
- previous_batch = this;
338
- flush_queued_effects(render_effects);
339
- flush_queued_effects(effects);
340
- previous_batch = null;
362
+ const earlier_batch = this.#find_earlier_batch();
341
363
 
342
- this.#deferred?.resolve();
364
+ if (earlier_batch) {
365
+ earlier_batch.#merge(this);
366
+ return;
343
367
  }
344
368
 
369
+ // clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches.
370
+ this.#dirty_effects.clear();
371
+ this.#maybe_dirty_effects.clear();
372
+
373
+ // append/remove branches
374
+ for (const fn of this.#commit_callbacks) fn(this);
375
+ this.#commit_callbacks.clear();
376
+
377
+ previous_batch = this;
378
+ flush_queued_effects(render_effects);
379
+ flush_queued_effects(effects);
380
+ previous_batch = null;
381
+
382
+ this.#deferred?.resolve();
383
+
345
384
  var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch));
346
385
 
386
+ if (this.linked && this.#pending === 0) {
387
+ this.#unlink();
388
+ }
389
+
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
+ }
399
+
347
400
  // Edge case: During traversal new branches might create effects that run immediately and set state,
348
401
  // causing an effect and therefore a root to be scheduled again. We need to traverse the current batch
349
402
  // once more in that case - most of the time this will just clean up dirty branches.
350
403
  if (this.#roots.length > 0) {
351
- const batch = (next_batch ??= this);
404
+ if (next_batch === null) {
405
+ next_batch = this;
406
+ this.#link();
407
+ }
408
+
409
+ const batch = next_batch;
352
410
  batch.#roots.push(...this.#roots.filter((r) => !batch.#roots.includes(r)));
353
411
  }
354
412
 
355
413
  if (next_batch !== null) {
356
- batches.add(next_batch);
357
-
358
- if (DEV) {
359
- for (const source of this.current.keys()) {
360
- /** @type {Set<Source>} */ (source_stacks).add(source);
361
- }
362
- }
363
-
364
414
  next_batch.#process();
365
415
  }
366
-
367
- // 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
368
- // TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed
369
- if (async_mode_flag && !batches.has(this)) {
370
- this.#commit();
371
- }
372
416
  }
373
417
 
374
418
  /**
@@ -423,6 +467,82 @@ export class Batch {
423
467
  }
424
468
  }
425
469
 
470
+ #find_earlier_batch() {
471
+ var batch = this.#prev;
472
+
473
+ while (batch !== null) {
474
+ if (!batch.is_fork) {
475
+ // if the batches are connected, break
476
+ for (const [value, [, is_derived]] of this.current) {
477
+ if (batch.current.has(value) && !is_derived) {
478
+ return batch;
479
+ }
480
+ }
481
+ }
482
+
483
+ batch = batch.#prev;
484
+ }
485
+
486
+ return null;
487
+ }
488
+
489
+ /**
490
+ * @param {Batch} batch
491
+ */
492
+ #merge(batch) {
493
+ for (const [source, value] of batch.current) {
494
+ if (!this.previous.has(source) && batch.previous.has(source)) {
495
+ this.previous.set(source, batch.previous.get(source));
496
+ }
497
+
498
+ this.current.set(source, value);
499
+ }
500
+
501
+ for (const [effect, deferred] of batch.async_deriveds) {
502
+ const d = this.async_deriveds.get(effect);
503
+ if (d) deferred.promise.then(d.resolve);
504
+ }
505
+
506
+ /**
507
+ * mark all effects that depend on `batch.current`, except the
508
+ * async effects that we just resolved (TODO unless they depend
509
+ * on values in this batch that are NOT in the later batch?).
510
+ * Through this we also will populate the correct #skipped_branches,
511
+ * oncommit callbacks etc, so we don't need to merge them separately.
512
+ * @param {Value} value
513
+ */
514
+ const mark = (value) => {
515
+ var reactions = value.reactions;
516
+ if (reactions === null) return;
517
+
518
+ for (const reaction of reactions) {
519
+ var flags = reaction.f;
520
+
521
+ if ((flags & DERIVED) !== 0) {
522
+ mark(/** @type {Derived} */ (reaction));
523
+ } else {
524
+ var effect = /** @type {Effect} */ (reaction);
525
+
526
+ if (flags & (ASYNC | BLOCK_EFFECT) && !this.async_deriveds.has(effect)) {
527
+ this.#maybe_dirty_effects.delete(effect);
528
+ set_signal_status(effect, DIRTY);
529
+ this.schedule(effect);
530
+ }
531
+ }
532
+ }
533
+ };
534
+
535
+ for (const source of this.current.keys()) {
536
+ mark(source);
537
+ }
538
+
539
+ this.oncommit(() => batch.discard());
540
+ batch.#unlink();
541
+
542
+ current_batch = this;
543
+ this.#process();
544
+ }
545
+
426
546
  /**
427
547
  * @param {Effect[]} effects
428
548
  */
@@ -465,9 +585,11 @@ export class Batch {
465
585
  }
466
586
 
467
587
  flush() {
468
- var source_stacks = DEV ? new Set() : null;
469
-
470
588
  try {
589
+ if (DEV) {
590
+ source_stacks.clear();
591
+ }
592
+
471
593
  is_processing = true;
472
594
  current_batch = this;
473
595
 
@@ -485,7 +607,7 @@ export class Batch {
485
607
  old_values.clear();
486
608
 
487
609
  if (DEV) {
488
- for (const source of /** @type {Set<Source>} */ (source_stacks)) {
610
+ for (const source of source_stacks) {
489
611
  source.updated = null;
490
612
  }
491
613
  }
@@ -497,7 +619,7 @@ export class Batch {
497
619
  this.#discard_callbacks.clear();
498
620
  this.#fork_commit_callbacks.clear();
499
621
 
500
- batches.delete(this);
622
+ this.#unlink();
501
623
  }
502
624
 
503
625
  /**
@@ -508,11 +630,13 @@ export class Batch {
508
630
  }
509
631
 
510
632
  #commit() {
633
+ this.#unlink();
634
+
511
635
  // If there are other pending batches, they now need to be 'rebased' —
512
636
  // in other words, we re-run block/async effects with the newly
513
637
  // committed state, unless the batch in question has a more
514
638
  // recent value for a given source
515
- for (const batch of batches) {
639
+ for (let batch = first_batch; batch !== null; batch = batch.#next) {
516
640
  var is_earlier = batch.id < this.id;
517
641
 
518
642
  /** @type {Source[]} */
@@ -535,6 +659,17 @@ export class Batch {
535
659
  sources.push(source);
536
660
  }
537
661
 
662
+ if (is_earlier) {
663
+ // TODO do we need to restart these in some cases, instead of
664
+ // immediately resolving them? Likely not because of how this.apply() works.
665
+ for (const [effect, deferred] of this.async_deriveds) {
666
+ const d = batch.async_deriveds.get(effect);
667
+ if (d) deferred.promise.then(d.resolve);
668
+ }
669
+ }
670
+
671
+ if (!batch.#started) continue;
672
+
538
673
  // Re-run async/block effects that depend on distinct values changed in both batches
539
674
  var others = [...batch.current.keys()].filter((s) => !this.current.has(s));
540
675
 
@@ -575,19 +710,23 @@ export class Batch {
575
710
 
576
711
  checked = new Map();
577
712
  var current_unequal = [...batch.current.keys()].filter((c) =>
578
- this.current.has(c) ? /** @type {[any, boolean]} */ (this.current.get(c))[0] !== c : true
713
+ this.current.has(c)
714
+ ? /** @type {[any, boolean]} */ (this.current.get(c))[0] !== c.v
715
+ : true
579
716
  );
580
717
 
581
- for (const effect of this.#new_effects) {
582
- if (
583
- (effect.f & (DESTROYED | INERT | EAGER_EFFECT)) === 0 &&
584
- depends_on(effect, current_unequal, checked)
585
- ) {
586
- if ((effect.f & (ASYNC | BLOCK_EFFECT)) !== 0) {
587
- set_signal_status(effect, DIRTY);
588
- batch.schedule(effect);
589
- } else {
590
- batch.#dirty_effects.add(effect);
718
+ if (current_unequal.length > 0) {
719
+ for (const effect of this.#new_effects) {
720
+ if (
721
+ (effect.f & (DESTROYED | INERT | EAGER_EFFECT)) === 0 &&
722
+ depends_on(effect, current_unequal, checked)
723
+ ) {
724
+ if ((effect.f & (ASYNC | BLOCK_EFFECT)) !== 0) {
725
+ set_signal_status(effect, DIRTY);
726
+ batch.schedule(effect);
727
+ } else {
728
+ batch.#dirty_effects.add(effect);
729
+ }
591
730
  }
592
731
  }
593
732
  }
@@ -606,17 +745,6 @@ export class Batch {
606
745
  batch.deactivate();
607
746
  }
608
747
  }
609
-
610
- for (const batch of batches) {
611
- if (batch.#blockers.has(this)) {
612
- batch.#blockers.delete(this);
613
-
614
- if (batch.#blockers.size === 0 && !batch.#is_deferred()) {
615
- batch.activate();
616
- batch.#process();
617
- }
618
- }
619
- }
620
748
  }
621
749
 
622
750
  /**
@@ -624,8 +752,7 @@ export class Batch {
624
752
  * @param {Effect} effect
625
753
  */
626
754
  increment(blocking, effect) {
627
- let pending_count = this.#pending.get(effect) ?? 0;
628
- this.#pending.set(effect, pending_count + 1);
755
+ this.#pending += 1;
629
756
 
630
757
  if (blocking) {
631
758
  let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0;
@@ -636,16 +763,9 @@ export class Batch {
636
763
  /**
637
764
  * @param {boolean} blocking
638
765
  * @param {Effect} effect
639
- * @param {boolean} skip - whether to skip updates (because this is triggered by a stale reaction)
640
766
  */
641
- decrement(blocking, effect, skip) {
642
- let pending_count = this.#pending.get(effect) ?? 0;
643
-
644
- if (pending_count === 1) {
645
- this.#pending.delete(effect);
646
- } else {
647
- this.#pending.set(effect, pending_count - 1);
648
- }
767
+ decrement(blocking, effect) {
768
+ this.#pending -= 1;
649
769
 
650
770
  if (blocking) {
651
771
  let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0;
@@ -657,12 +777,15 @@ export class Batch {
657
777
  }
658
778
  }
659
779
 
660
- if (this.#decrement_queued || skip) return;
780
+ if (this.#decrement_queued) return;
661
781
  this.#decrement_queued = true;
662
782
 
663
783
  queue_micro_task(() => {
664
784
  this.#decrement_queued = false;
665
- this.flush();
785
+
786
+ if (this.linked) {
787
+ this.flush();
788
+ }
666
789
  });
667
790
  }
668
791
 
@@ -710,20 +833,14 @@ export class Batch {
710
833
  static ensure() {
711
834
  if (current_batch === null) {
712
835
  const batch = (current_batch = new Batch());
836
+ batch.#link();
713
837
 
714
- if (!is_processing) {
715
- batches.add(current_batch);
716
-
717
- if (!is_flushing_sync) {
718
- queue_micro_task(() => {
719
- if (current_batch !== batch) {
720
- // a flushSync happened in the meantime
721
- return;
722
- }
723
-
838
+ if (!is_processing && !is_flushing_sync) {
839
+ queue_micro_task(() => {
840
+ if (!batch.#started) {
724
841
  batch.flush();
725
- });
726
- }
842
+ }
843
+ });
727
844
  }
728
845
  }
729
846
 
@@ -731,7 +848,7 @@ export class Batch {
731
848
  }
732
849
 
733
850
  apply() {
734
- if (!async_mode_flag || (!this.is_fork && batches.size === 1)) {
851
+ if (!async_mode_flag || (!this.is_fork && this.#prev === null && this.#next === null)) {
735
852
  batch_values = null;
736
853
  return;
737
854
  }
@@ -743,28 +860,33 @@ export class Batch {
743
860
  batch_values.set(source, value);
744
861
  }
745
862
 
746
- // ...and undo changes belonging to other batches unless they block this one
747
- for (const batch of batches) {
863
+ // ...and undo changes belonging to other batches unless they intersect
864
+ for (let batch = first_batch; batch !== null; batch = batch.#next) {
748
865
  if (batch === this || batch.is_fork) continue;
749
866
 
750
- // A batch is blocked on an earlier batch if it overlaps with the earlier batch's changes but is not a superset
867
+ // If two batches intersect, the latter batch will be merged into the earlier batch,
868
+ // and we should treat them as a single set of changes
751
869
  var intersects = false;
752
- var differs = false;
753
870
 
754
871
  if (batch.id < this.id) {
755
872
  for (const [source, [, is_derived]] of batch.current) {
756
- // Derived values don't partake in the blocking mechanism, because a derived could
873
+ // Derived values don't partake in the intersection mechanism, because a derived could
757
874
  // be triggered in one batch already but not the other one yet, causing a false-positive
758
875
  if (is_derived) continue;
759
876
 
760
- intersects ||= this.current.has(source);
761
- differs ||= !this.current.has(source);
877
+ if (this.current.has(source)) {
878
+ intersects = true;
879
+ break;
880
+ }
762
881
  }
763
882
  }
764
883
 
765
- if (intersects && differs) {
766
- this.#blockers.add(batch);
767
- } else {
884
+ // Since the latter batch merges into the earlier (if it resolves before the earlier one),
885
+ // we treat the earlier values as "already applied". This way we don't need to rerun async
886
+ // effects of the earlier batch in case they are merged.
887
+ // As a result you can think of batch_values as having the latest values of all intersecting
888
+ // batches up until this batch.
889
+ if (!intersects) {
768
890
  for (const [source, previous] of batch.previous) {
769
891
  if (!batch_values.has(source)) {
770
892
  batch_values.set(source, previous);
@@ -830,6 +952,36 @@ export class Batch {
830
952
 
831
953
  this.#roots.push(e);
832
954
  }
955
+
956
+ #link() {
957
+ if (last_batch === null) {
958
+ first_batch = last_batch = this;
959
+ } else {
960
+ last_batch.#next = this;
961
+ this.#prev = last_batch;
962
+ }
963
+
964
+ last_batch = this;
965
+ }
966
+
967
+ #unlink() {
968
+ var prev = this.#prev;
969
+ var next = this.#next;
970
+
971
+ if (prev === null) {
972
+ first_batch = next;
973
+ } else {
974
+ prev.#next = next;
975
+ }
976
+
977
+ if (next === null) {
978
+ last_batch = prev;
979
+ } else {
980
+ next.#prev = prev;
981
+ }
982
+
983
+ this.linked = false;
984
+ }
833
985
  }
834
986
 
835
987
  // TODO Svelte@6 think about removing the callback argument.
@@ -1083,6 +1235,9 @@ function eager_flush() {
1083
1235
  });
1084
1236
  }
1085
1237
 
1238
+ /** @type {Map<Reaction, Source<number>>} */
1239
+ var version_map = new Map();
1240
+
1086
1241
  /**
1087
1242
  * Implementation of `$state.eager(fn())`
1088
1243
  * @template T
@@ -1090,10 +1245,22 @@ function eager_flush() {
1090
1245
  * @returns {T}
1091
1246
  */
1092
1247
  export function eager(fn) {
1093
- var version = source(0);
1094
1248
  var initial = true;
1095
1249
  var value = /** @type {T} */ (undefined);
1096
1250
 
1251
+ if (active_reaction === null) {
1252
+ return fn();
1253
+ }
1254
+
1255
+ let parent = active_reaction;
1256
+
1257
+ let version = version_map.get(parent) ?? source(0);
1258
+ version_map.set(parent, version);
1259
+
1260
+ teardown(() => {
1261
+ if (parent.f & DESTROYING) version_map.delete(parent);
1262
+ });
1263
+
1097
1264
  get(version);
1098
1265
 
1099
1266
  eager_effect(() => {
@@ -1213,7 +1380,7 @@ export function fork(fn) {
1213
1380
  return;
1214
1381
  }
1215
1382
 
1216
- if (!batches.has(batch)) {
1383
+ if (!batch.linked) {
1217
1384
  e.fork_discarded();
1218
1385
  }
1219
1386
 
@@ -1259,7 +1426,7 @@ export function fork(fn) {
1259
1426
  source.wv = increment_write_version();
1260
1427
  }
1261
1428
 
1262
- if (!committed && batches.has(batch)) {
1429
+ if (!committed && batch.linked) {
1263
1430
  batch.discard();
1264
1431
  }
1265
1432
  }
@@ -1270,5 +1437,5 @@ export function fork(fn) {
1270
1437
  * Forcibly remove all current batches, to prevent cross-talk between tests
1271
1438
  */
1272
1439
  export function clear() {
1273
- batches.clear();
1440
+ first_batch = last_batch = null;
1274
1441
  }