ripple 0.3.127 → 0.3.128

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 (39) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/package.json +2 -2
  3. package/src/runtime/internal/client/blocks.js +20 -24
  4. package/src/runtime/internal/client/constants.js +5 -5
  5. package/src/runtime/internal/client/expression.js +142 -101
  6. package/src/runtime/internal/client/for.js +217 -145
  7. package/src/runtime/internal/client/index.js +5 -1
  8. package/src/runtime/internal/client/operations.js +24 -10
  9. package/src/runtime/internal/client/render.js +22 -3
  10. package/src/runtime/internal/client/runtime.js +246 -182
  11. package/src/runtime/internal/client/selector.js +74 -0
  12. package/src/runtime/internal/client/types.d.ts +11 -0
  13. package/src/runtime/internal/server/index.js +2 -0
  14. package/tests/client/basic/basic.reactivity.test.tsrx +33 -0
  15. package/tests/client/for-selector.test.tsrx +221 -0
  16. package/tests/hydration/compiled/client/basic.js +44 -44
  17. package/tests/hydration/compiled/client/composite.js +3 -3
  18. package/tests/hydration/compiled/client/events.js +56 -29
  19. package/tests/hydration/compiled/client/for.js +76 -70
  20. package/tests/hydration/compiled/client/head.js +11 -8
  21. package/tests/hydration/compiled/client/hmr.js +4 -4
  22. package/tests/hydration/compiled/client/html-in-template.js +3 -3
  23. package/tests/hydration/compiled/client/html.js +141 -141
  24. package/tests/hydration/compiled/client/if-children.js +29 -29
  25. package/tests/hydration/compiled/client/if-fragment-controlflow.js +16 -16
  26. package/tests/hydration/compiled/client/if.js +10 -10
  27. package/tests/hydration/compiled/client/mixed-control-flow.js +26 -22
  28. package/tests/hydration/compiled/client/nested-control-flow.js +114 -92
  29. package/tests/hydration/compiled/client/portal.js +8 -8
  30. package/tests/hydration/compiled/client/reactivity.js +38 -15
  31. package/tests/hydration/compiled/client/streaming.js +16 -16
  32. package/tests/hydration/compiled/client/switch.js +4 -4
  33. package/tests/hydration/compiled/client/track-async-serialization.js +18 -18
  34. package/tests/hydration/compiled/client/try.js +8 -8
  35. package/tests/hydration/compiled/server/events.js +5 -53
  36. package/tests/hydration/compiled/server/for.js +2 -18
  37. package/tests/hydration/compiled/server/head.js +1 -9
  38. package/tests/hydration/compiled/server/html.js +1 -9
  39. package/tests/hydration/compiled/server/reactivity.js +2 -34
@@ -18,9 +18,9 @@ import {
18
18
  DERIVED,
19
19
  COMPUTED_PROPERTY,
20
20
  CONTAINS_TEARDOWN,
21
- CONTAINS_UPDATE,
22
21
  DESTROYED,
23
22
  EFFECT_BLOCK,
23
+ FOR_BLOCK,
24
24
  PAUSED,
25
25
  PRE_EFFECT_BLOCK,
26
26
  ROOT_BLOCK,
@@ -34,7 +34,8 @@ import {
34
34
  SUSPENSE_REJECTED,
35
35
  TRY_BLOCK,
36
36
  DIRECT_CHILD_BLOCK,
37
- UPDATE_SOURCE,
37
+ SCHEDULED,
38
+ SELECTOR,
38
39
  } from './constants.js';
39
40
  import {
40
41
  begin_boundary_request,
@@ -92,14 +93,18 @@ let scheduler_mode = FLUSH_MICROTASK;
92
93
  let is_micro_task_queued = false;
93
94
  /** @type {number} */
94
95
  let clock = 0;
95
- /** @type {Block[]} */
96
- let queued_root_blocks = [];
97
- // Flush normally only scans the subtree of each directly-scheduled block, since a
98
- // tracked's subscribers live inside the subtree of the block that owns it. If a
99
- // tracked is ever read from outside its owner's subtree (e.g. smuggled across
100
- // sibling subtrees via a module-level variable), that invariant breaks, so we
101
- // permanently fall back to scanning the whole root tree to stay correct.
102
- let disable_scoped_flush = false;
96
+ /**
97
+ * Blocks scheduled for the next flush. `sorted` tracks whether they were
98
+ * pushed in creation order, so the common case avoids a sort.
99
+ * @typedef {{ blocks: Block[]; sorted: boolean; last: number }} Queue
100
+ */
101
+ /** @type {Queue} */
102
+ let queue = create_queue();
103
+
104
+ /** @returns {Queue} */
105
+ function create_queue() {
106
+ return { blocks: [], sorted: true, last: 0 };
107
+ }
103
108
  /** @type {(() => void)[]} */
104
109
  let queued_microtasks = [];
105
110
  /** @type {number} */
@@ -213,6 +218,7 @@ function update_derived(computed) {
213
218
  function update_tracked_value_clock(tracked, value) {
214
219
  tracked.__v = value;
215
220
  tracked.c = increment_clock();
221
+ mark_subscribers(tracked);
216
222
  }
217
223
 
218
224
  /**
@@ -252,11 +258,11 @@ function run_derived(computed) {
252
258
 
253
259
  var value = computed.fn();
254
260
 
255
- computed.d = active_dependency;
261
+ finish_dependencies(computed, active_dependency);
256
262
 
257
263
  return value;
258
264
  } catch (error) {
259
- computed.d = active_dependency;
265
+ finish_dependencies(computed, active_dependency);
260
266
  if (error === ASYNC_DERIVED_READ_THROWN) {
261
267
  // Check if any dependency is rejected — if so, propagate rejection
262
268
  var dep = active_dependency;
@@ -296,8 +302,10 @@ export function handle_error(error, block) {
296
302
 
297
303
  /**
298
304
  * @param {Block} block
305
+ * @param {boolean} [first_run] true when the block has no children, teardown,
306
+ * or dependencies yet, so that cleanup can be skipped
299
307
  */
300
- export function run_block(block) {
308
+ export function run_block(block, first_run = false) {
301
309
  var previous_block = active_block;
302
310
  var previous_reaction = active_reaction;
303
311
  var previous_tracking = tracking;
@@ -309,8 +317,13 @@ export function run_block(block) {
309
317
  active_reaction = block;
310
318
  active_component = block.co;
311
319
 
312
- destroy_non_branch_children(block);
313
- run_teardown(block);
320
+ if (!first_run) {
321
+ // A list's children are all item branches, so there is nothing to sweep.
322
+ if ((block.f & FOR_BLOCK) === 0) {
323
+ destroy_non_branch_children(block);
324
+ }
325
+ run_teardown(block);
326
+ }
314
327
 
315
328
  tracking = (block.f & (ROOT_BLOCK | BRANCH_BLOCK)) === 0;
316
329
  active_dependency = null;
@@ -327,11 +340,11 @@ export function run_block(block) {
327
340
  }
328
341
  }
329
342
 
330
- block.d = active_dependency;
343
+ finish_dependencies(block, active_dependency);
331
344
  } catch (error) {
332
345
  var is_component_direct = false;
333
346
  var is_try_fn_block = false;
334
- block.d = active_dependency;
347
+ finish_dependencies(block, active_dependency);
335
348
  // When a derived read throws ASYNC_DERIVED_READ_THROWN, it means the
336
349
  // derived is still SUSPENSE_PENDING. The dependency was already registered,
337
350
  // so we swallow the throw and let the parent continue processing. When
@@ -420,8 +433,10 @@ class TrackedValue {
420
433
  this.d = null;
421
434
  /** @type {number} */
422
435
  this.f = TRACKED;
423
- /** @type {string | undefined} */
436
+ /** @type {any} hydration hash, or the key of a selector match flag */
424
437
  this.h = hash;
438
+ /** @type {Dependency | null} */
439
+ this.sb = null;
425
440
  /** @type {any} */
426
441
  this.__v = v;
427
442
  }
@@ -482,6 +497,8 @@ class DerivedValue {
482
497
  this.fn = fn;
483
498
  /** @type {string | undefined} */
484
499
  this.h = hash;
500
+ /** @type {Dependency | null} */
501
+ this.sb = null;
485
502
  /** @type {any} */
486
503
  this.__v = UNINITIALIZED;
487
504
  }
@@ -696,7 +713,6 @@ export function track_async(fn, b, hash) {
696
713
  // Set to pending before calling fn() in case it's sync.
697
714
  if (t.__v !== SUSPENSE_PENDING) {
698
715
  update_tracked_value_clock(t, SUSPENSE_PENDING);
699
- schedule_update(t.b);
700
716
  }
701
717
 
702
718
  // Temporarily allow mutations so set() doesn't throw inside the pre-effect
@@ -715,7 +731,6 @@ export function track_async(fn, b, hash) {
715
731
  while (dep !== null) {
716
732
  if (dep.t.__v === SUSPENSE_REJECTED) {
717
733
  update_tracked_value_clock(t, SUSPENSE_REJECTED);
718
- schedule_update(t.b);
719
734
  complete_deferred_boundaries(t, false);
720
735
  if (request_id > 0 && boundary !== null) {
721
736
  complete_boundary_request(boundary, request_id, false);
@@ -748,7 +763,6 @@ export function track_async(fn, b, hash) {
748
763
  if (async_result === null) {
749
764
  // Sync result
750
765
  update_tracked_value_clock(t, result);
751
- schedule_update(t.b);
752
766
  if (request_id > 0 && boundary !== null) {
753
767
  complete_boundary_request(boundary, request_id);
754
768
  request_id = 0;
@@ -769,7 +783,6 @@ export function track_async(fn, b, hash) {
769
783
  return;
770
784
  }
771
785
  update_tracked_value_clock(t, resolved);
772
- schedule_update(t.b);
773
786
  complete_deferred_boundaries(t);
774
787
  if (request_id > 0 && boundary !== null) {
775
788
  complete_boundary_request(boundary, request_id);
@@ -792,7 +805,6 @@ export function track_async(fn, b, hash) {
792
805
  }
793
806
 
794
807
  update_tracked_value_clock(t, SUSPENSE_REJECTED);
795
- schedule_update(t.b);
796
808
  complete_deferred_boundaries(t, false);
797
809
 
798
810
  // Route error to catch boundary
@@ -865,16 +877,151 @@ function create_dependency(tracked) {
865
877
  if (existing !== null) {
866
878
  reaction.d = existing.n;
867
879
  existing.c = tracked.c;
868
- existing.t = tracked;
869
880
  existing.n = null;
881
+ if (existing.t !== tracked) {
882
+ unlink_subscriber(existing);
883
+ existing.t = tracked;
884
+ link_subscriber(existing, tracked);
885
+ }
870
886
  return existing;
871
887
  }
872
888
 
873
- return {
889
+ /** @type {Dependency} */
890
+ var dependency = {
874
891
  c: tracked.c,
875
892
  t: tracked,
876
893
  n: null,
894
+ r: reaction,
895
+ sp: null,
896
+ sn: null,
877
897
  };
898
+ link_subscriber(dependency, tracked);
899
+ return dependency;
900
+ }
901
+
902
+ /**
903
+ * @param {Dependency} dependency
904
+ * @param {Tracked | Derived} tracked
905
+ */
906
+ function link_subscriber(dependency, tracked) {
907
+ var head = tracked.sb;
908
+ dependency.sn = head;
909
+ if (head !== null) {
910
+ head.sp = dependency;
911
+ }
912
+ tracked.sb = dependency;
913
+ }
914
+
915
+ /**
916
+ * @param {Dependency} dependency
917
+ */
918
+ export function unlink_subscriber(dependency) {
919
+ var prev = dependency.sp;
920
+ var next = dependency.sn;
921
+ if (prev !== null) {
922
+ prev.sn = next;
923
+ } else {
924
+ var tracked = dependency.t;
925
+ // Already unlinked (a destroyed block's dependency can be pruned by a
926
+ // write during its own teardown, then unlinked again by
927
+ // remove_dependencies); it must not be mistaken for the list head.
928
+ if (tracked.sb !== dependency) {
929
+ return;
930
+ }
931
+ tracked.sb = next;
932
+ if (next === null && (tracked.f & SELECTOR) !== 0) {
933
+ // Last subscriber gone: release the selector's per-key entry.
934
+ /** @type {SelectorAccessors} */ (tracked.a).m.delete(tracked.h);
935
+ }
936
+ }
937
+ if (next !== null) {
938
+ next.sp = prev;
939
+ }
940
+ dependency.sp = dependency.sn = null;
941
+ }
942
+
943
+ /**
944
+ * @typedef {{ get: undefined; set: undefined; m: Map<any, Tracked> }} SelectorAccessors
945
+ */
946
+
947
+ /**
948
+ * A tracked match flag for one selector key. The selector's shared accessor
949
+ * object carries the map and the key rides in the hash slot, so the entry can
950
+ * be released from `unlink_subscriber` without an object per key.
951
+ * @param {boolean} value
952
+ * @param {Block} block
953
+ * @param {SelectorAccessors} accessors
954
+ * @param {any} key
955
+ * @returns {Tracked}
956
+ */
957
+ export function selector_tracked(value, block, accessors, key) {
958
+ var t = /** @type {Tracked} */ (new TrackedValue(value, block, accessors, key));
959
+ t.f |= SELECTOR;
960
+ return t;
961
+ }
962
+
963
+ /**
964
+ * Install the dependency chain produced by a run, unsubscribing from any
965
+ * previous dependencies that were not read again.
966
+ * @param {Block | Derived} reaction
967
+ * @param {Dependency | null} dependencies
968
+ */
969
+ function finish_dependencies(reaction, dependencies) {
970
+ var stale = reaction.d;
971
+ while (stale !== null) {
972
+ unlink_subscriber(stale);
973
+ stale = stale.n;
974
+ }
975
+ reaction.d = dependencies;
976
+ }
977
+
978
+ /**
979
+ * Unsubscribe a block from every tracked value it read. Dependencies on
980
+ * values owned by an already-destroyed block are dropped without unlinking,
981
+ * since the owner's subscriber list dies with it.
982
+ * @param {Block} block
983
+ */
984
+ export function remove_dependencies(block) {
985
+ var dependency = block.d;
986
+ while (dependency !== null) {
987
+ var owner = dependency.t.b;
988
+ if (owner === null || (owner.f & DESTROYED) === 0) {
989
+ unlink_subscriber(dependency);
990
+ }
991
+ dependency = dependency.n;
992
+ }
993
+ block.d = null;
994
+ }
995
+
996
+ /**
997
+ * Schedule every reaction subscribed to `tracked`. Deriveds are lazy, so their
998
+ * own subscribers are marked instead; subscribers that were destroyed without
999
+ * unlinking are pruned here.
1000
+ * @param {Tracked | Derived} tracked
1001
+ */
1002
+ function mark_subscribers(tracked) {
1003
+ var dependency = tracked.sb;
1004
+ while (dependency !== null) {
1005
+ var next = dependency.sn;
1006
+ var reaction = dependency.r;
1007
+ var flags = reaction.f;
1008
+ if ((flags & DERIVED) !== 0) {
1009
+ var derived = /** @type {Derived} */ (reaction);
1010
+ var derived_owner = derived.b;
1011
+ // A derived whose owner is gone and that nothing reads any more is
1012
+ // pruned; one still read elsewhere keeps forwarding notifications.
1013
+ if (derived_owner !== null && (derived_owner.f & DESTROYED) !== 0 && derived.sb === null) {
1014
+ unlink_subscriber(dependency);
1015
+ } else {
1016
+ mark_subscribers(derived);
1017
+ }
1018
+ } else if ((flags & DESTROYED) !== 0) {
1019
+ unlink_subscriber(dependency);
1020
+ } else {
1021
+ schedule_update(/** @type {Block} */ (reaction));
1022
+ }
1023
+ dependency = next;
1024
+ }
878
1025
  }
879
1026
 
880
1027
  /**
@@ -941,101 +1088,74 @@ function trigger_track_get(fn, v) {
941
1088
  }
942
1089
 
943
1090
  /**
944
- * @param {Block} root_block
1091
+ * @param {Block} a
1092
+ * @param {Block} b
1093
+ * @returns {number}
945
1094
  */
946
- function flush_updates(root_block) {
947
- /** @type {Block | null} */
948
- var current = root_block;
949
- var pre_effects = [];
950
- var other_blocks = [];
951
- var effects = [];
952
- // The nearest enclosing directly-scheduled block ("update source"). While it is
953
- // non-null we are inside a source's subtree and scan every descendant. Above
954
- // sources we only follow the CONTAINS_UPDATE routing path, so sibling subtrees
955
- // that contain no update are skipped. When scoping is disabled we treat the
956
- // whole root tree as one source — the original full-tree scan.
957
- /** @type {Block | null} */
958
- var scope_root = disable_scoped_flush ? root_block : null;
1095
+ function by_block_id(a, b) {
1096
+ return a.i - b.i;
1097
+ }
959
1098
 
960
- while (current !== null) {
961
- var flags = current.f;
962
- var on_path = (flags & CONTAINS_UPDATE) !== 0;
1099
+ /**
1100
+ * Run every scheduled block, in creation order (so a parent runs before its
1101
+ * descendants), in three phases: pre-effects, render blocks, then effects.
1102
+ * @param {Queue} pending
1103
+ */
1104
+ function flush_queue(pending) {
1105
+ var blocks = pending.blocks;
963
1106
 
964
- if (on_path) {
965
- current.f ^= CONTAINS_UPDATE;
966
- }
1107
+ if (!pending.sorted) {
1108
+ blocks.sort(by_block_id);
1109
+ }
967
1110
 
968
- if ((flags & UPDATE_SOURCE) !== 0) {
969
- current.f ^= UPDATE_SOURCE;
970
- if (scope_root === null) {
971
- scope_root = current;
972
- }
973
- }
1111
+ /** @type {Block[]} */
1112
+ var pre_effects = [];
1113
+ /** @type {Block[]} */
1114
+ var other_blocks = [];
1115
+ /** @type {Block[]} */
1116
+ var effects = [];
974
1117
 
975
- if ((flags & PAUSED) === 0 && (on_path || scope_root !== null)) {
976
- if ((flags & PRE_EFFECT_BLOCK) !== 0) {
977
- pre_effects.push(current);
978
- } else if ((flags & EFFECT_BLOCK) !== 0) {
979
- effects.push(current);
980
- } else {
981
- other_blocks.push(current);
982
- }
983
- /** @type {Block | null} */
984
- var child = current.first;
1118
+ for (var i = 0; i < blocks.length; i++) {
1119
+ var block = blocks[i];
1120
+ var flags = block.f;
1121
+ block.f = flags & ~SCHEDULED;
985
1122
 
986
- if (child !== null) {
987
- current = child;
988
- continue;
989
- }
1123
+ // A paused block is re-checked when it resumes; a destroyed one is gone.
1124
+ if ((flags & (PAUSED | DESTROYED)) !== 0) {
1125
+ continue;
990
1126
  }
991
-
992
- /** @type {Block | null} */
993
- var parent = current.p;
994
- current = current.next;
995
-
996
- while (current === null && parent !== null) {
997
- if (parent === scope_root) {
998
- scope_root = null;
999
- }
1000
- current = parent.next;
1001
- parent = parent.p;
1127
+ if ((flags & PRE_EFFECT_BLOCK) !== 0) {
1128
+ pre_effects.push(block);
1129
+ } else if ((flags & EFFECT_BLOCK) !== 0) {
1130
+ effects.push(block);
1131
+ } else {
1132
+ other_blocks.push(block);
1002
1133
  }
1003
1134
  }
1004
1135
 
1005
- var arr_length = 0;
1136
+ blocks.length = 0;
1137
+ pending.sorted = true;
1138
+ pending.last = 0;
1006
1139
 
1007
- // Phase 1: pre-effects (e.g. update tracked values before render blocks read them)
1008
- arr_length = pre_effects.length;
1009
- for (var i = 0; i < arr_length; i++) {
1010
- var block = pre_effects[i];
1140
+ run_phase(pre_effects);
1141
+ run_phase(other_blocks);
1142
+ run_phase(effects);
1011
1143
 
1012
- try {
1013
- if ((block.f & (PAUSED | DESTROYED)) === 0 && is_block_dirty(block)) {
1014
- run_block(block);
1015
- }
1016
- } catch (error) {
1017
- handle_error(error, block);
1018
- }
1019
- }
1020
-
1021
- // Phase 2: all other blocks except effects
1022
- arr_length = other_blocks.length;
1023
- for (var i = 0; i < arr_length; i++) {
1024
- var block = other_blocks[i];
1025
-
1026
- try {
1027
- if ((block.f & (PAUSED | DESTROYED)) === 0 && is_block_dirty(block)) {
1028
- run_block(block);
1029
- }
1030
- } catch (error) {
1031
- handle_error(error, block);
1144
+ if (queued_post_block_flush.length > 0) {
1145
+ var callbacks = queued_post_block_flush;
1146
+ queued_post_block_flush = [];
1147
+ for (var j = 0; j < callbacks.length; j++) {
1148
+ callbacks[j]();
1032
1149
  }
1033
1150
  }
1151
+ }
1034
1152
 
1035
- // Phase 3: effects
1036
- arr_length = effects.length;
1037
- for (var i = 0; i < arr_length; i++) {
1038
- var block = effects[i];
1153
+ /**
1154
+ * @param {Block[]} blocks
1155
+ */
1156
+ function run_phase(blocks) {
1157
+ for (var i = 0; i < blocks.length; i++) {
1158
+ var block = blocks[i];
1039
1159
 
1040
1160
  try {
1041
1161
  if ((block.f & (PAUSED | DESTROYED)) === 0 && is_block_dirty(block)) {
@@ -1047,23 +1167,6 @@ function flush_updates(root_block) {
1047
1167
  }
1048
1168
  }
1049
1169
 
1050
- /**
1051
- * @param {Block[]} root_blocks
1052
- */
1053
- function flush_queued_root_blocks(root_blocks) {
1054
- for (let i = 0; i < root_blocks.length; i++) {
1055
- flush_updates(root_blocks[i]);
1056
- }
1057
-
1058
- if (queued_post_block_flush.length > 0) {
1059
- var callbacks = queued_post_block_flush;
1060
- queued_post_block_flush = [];
1061
- for (var j = 0; j < callbacks.length; j++) {
1062
- callbacks[j]();
1063
- }
1064
- }
1065
- }
1066
-
1067
1170
  /**
1068
1171
  * @returns {Promise<void>}
1069
1172
  */
@@ -1091,9 +1194,9 @@ function flush_microtasks() {
1091
1194
  'Maximum update depth exceeded. This typically indicates that an effect reads and writes the same piece of state.',
1092
1195
  );
1093
1196
  }
1094
- var previous_queued_root_blocks = queued_root_blocks;
1095
- queued_root_blocks = [];
1096
- flush_queued_root_blocks(previous_queued_root_blocks);
1197
+ var pending = queue;
1198
+ queue = create_queue();
1199
+ flush_queue(pending);
1097
1200
 
1098
1201
  if (!is_micro_task_queued) {
1099
1202
  flush_count = 0;
@@ -1128,64 +1231,27 @@ export function queue_post_block_flush_callback(fn) {
1128
1231
  * @param {Block} block
1129
1232
  */
1130
1233
  export function schedule_update(block) {
1234
+ if ((block.f & SCHEDULED) !== 0) {
1235
+ return;
1236
+ }
1237
+ block.f |= SCHEDULED;
1238
+
1131
1239
  if (scheduler_mode === FLUSH_MICROTASK) {
1132
1240
  queue_microtask();
1133
1241
  }
1134
- // The scheduled block roots the subtree that flush_updates scans for dirty
1135
- // subscribers. Mark it even if it (or an ancestor) is already on a routing
1136
- // path, so the early return below still records it as a scan root.
1137
- block.f |= UPDATE_SOURCE;
1138
- let current = block;
1139
-
1140
- while (current !== null) {
1141
- var flags = current.f;
1142
- if ((flags & CONTAINS_UPDATE) !== 0) return;
1143
- current.f ^= CONTAINS_UPDATE;
1144
- if ((flags & ROOT_BLOCK) !== 0) {
1145
- break;
1146
- }
1147
- current = /** @type {Block} */ (current.p);
1148
- }
1149
1242
 
1150
- queued_root_blocks.push(current);
1243
+ var id = block.i;
1244
+ if (id < queue.last) {
1245
+ queue.sorted = false;
1246
+ }
1247
+ queue.last = id;
1248
+ queue.blocks.push(block);
1151
1249
  }
1152
1250
 
1153
1251
  /**
1154
1252
  * @param {Tracked | Derived} tracked
1155
1253
  */
1156
1254
  function register_dependency(tracked) {
1157
- if (!disable_scoped_flush && active_block !== null && active_block !== tracked.b) {
1158
- // Scoped flush only scans the owner's subtree for dirty subscribers, valid
1159
- // only while every subscriber lives inside it. The subscriber↔owner
1160
- // ancestry is structural and stable for a block's lifetime (its `.p` chain
1161
- // never changes — DOM moves don't reparent blocks), so we only need to
1162
- // verify *new* dependency edges: if this reaction already depended on
1163
- // `tracked` on its previous run (it's in the prior chain), the ancestry
1164
- // walk was already done — skip it. This keeps the (possibly deep) walk off
1165
- // the hot path for the common case of stable subscriptions.
1166
- var already_seen = false;
1167
- var prev_dep = active_reaction === null ? null : active_reaction.d;
1168
- while (prev_dep !== null) {
1169
- if (prev_dep.t === tracked) {
1170
- already_seen = true;
1171
- break;
1172
- }
1173
- prev_dep = prev_dep.n;
1174
- }
1175
-
1176
- if (!already_seen) {
1177
- var owner = tracked.b;
1178
- /** @type {Block | null} */
1179
- var node = active_block;
1180
- while (node !== null && node !== owner) {
1181
- node = node.p;
1182
- }
1183
- if (node === null) {
1184
- disable_scoped_flush = true;
1185
- }
1186
- }
1187
- }
1188
-
1189
1255
  var dependency = active_dependency;
1190
1256
 
1191
1257
  if (dependency === null) {
@@ -1406,7 +1472,7 @@ export function set(tracked, value) {
1406
1472
 
1407
1473
  tracked.__v = value;
1408
1474
  tracked.c = increment_clock();
1409
- schedule_update(tracked_block);
1475
+ mark_subscribers(tracked);
1410
1476
  }
1411
1477
  }
1412
1478
 
@@ -1435,21 +1501,19 @@ export function untrack(fn) {
1435
1501
  */
1436
1502
  export function flush_sync(fn) {
1437
1503
  var previous_scheduler_mode = scheduler_mode;
1438
- var previous_queued_root_blocks = queued_root_blocks;
1504
+ var previous_queue = queue;
1439
1505
 
1440
1506
  try {
1441
- /** @type {Block[]} */
1442
- var root_blocks = [];
1443
-
1444
1507
  scheduler_mode = FLUSH_SYNC;
1445
- queued_root_blocks = root_blocks;
1508
+ queue = create_queue();
1446
1509
  is_micro_task_queued = false;
1447
1510
 
1448
- flush_queued_root_blocks(previous_queued_root_blocks);
1511
+ // Drains previous_queue, which then stays empty for the restore below.
1512
+ flush_queue(previous_queue);
1449
1513
 
1450
1514
  var result = fn?.();
1451
1515
 
1452
- if (queued_root_blocks.length > 0 || root_blocks.length > 0) {
1516
+ if (queue.blocks.length > 0) {
1453
1517
  flush_sync();
1454
1518
  }
1455
1519
 
@@ -1458,7 +1522,7 @@ export function flush_sync(fn) {
1458
1522
  return /** @type {T} */ (result);
1459
1523
  } finally {
1460
1524
  scheduler_mode = previous_scheduler_mode;
1461
- queued_root_blocks = previous_queued_root_blocks;
1525
+ queue = previous_queue;
1462
1526
  }
1463
1527
  }
1464
1528
 
@@ -0,0 +1,74 @@
1
+ /** @import { Block, Tracked } from '#client' */
2
+
3
+ import { render } from './blocks.js';
4
+ import { UNINITIALIZED } from './constants.js';
5
+ import { get_tracked, selector_tracked, set } from './runtime.js';
6
+
7
+ /**
8
+ * @typedef {{ v: any; m: Map<any, Tracked>; b: Block; a: import('./runtime.js').SelectorAccessors }} Selector
9
+ */
10
+
11
+ /**
12
+ * Shares one reactive source across many `source === key` comparisons: each
13
+ * key subscribes to its own match flag, so a change of the source only
14
+ * notifies the blocks for the previous and next key rather than every block
15
+ * that compares against it. Used by the compiler for comparisons between a
16
+ * loop item and outer state inside `@for` templates.
17
+ * @param {() => any} get_source
18
+ * @returns {Selector}
19
+ */
20
+ export function selector(get_source) {
21
+ var m = new Map();
22
+ /** @type {Selector} */
23
+ var s = {
24
+ v: UNINITIALIZED,
25
+ m,
26
+ b: /** @type {Block} */ (/** @type {unknown} */ (null)),
27
+ a: { get: undefined, set: undefined, m },
28
+ };
29
+
30
+ s.b = render(() => {
31
+ var next = get_source();
32
+ var prev = s.v;
33
+ if (next === prev) {
34
+ return;
35
+ }
36
+ s.v = next;
37
+ var m = s.m;
38
+ if (m.size === 0) {
39
+ return;
40
+ }
41
+ // NaN never matches with ===, so it never toggles a flag.
42
+ if (prev === prev) {
43
+ var previous_match = m.get(prev);
44
+ if (previous_match !== undefined) {
45
+ set(previous_match, false);
46
+ }
47
+ }
48
+ if (next === next) {
49
+ var next_match = m.get(next);
50
+ if (next_match !== undefined) {
51
+ set(next_match, true);
52
+ }
53
+ }
54
+ });
55
+
56
+ return s;
57
+ }
58
+
59
+ /**
60
+ * `source === key`, read inside a render block so the block re-runs only when
61
+ * the match for this key changes.
62
+ * @param {Selector} s
63
+ * @param {any} key
64
+ * @returns {boolean}
65
+ */
66
+ export function selector_match(s, key) {
67
+ var m = s.m;
68
+ var match = m.get(key);
69
+ if (match === undefined) {
70
+ match = selector_tracked(key === s.v, s.b, s.a, key);
71
+ m.set(key, match);
72
+ }
73
+ return get_tracked(match);
74
+ }