@solidjs/signals 2.0.0-rc.7 → 2.0.0-rc.9

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 (134) hide show
  1. package/dist/dev-shared.js +6357 -0
  2. package/dist/dev.attribution.js +2485 -0
  3. package/dist/dev.js +2851 -7027
  4. package/dist/observe/affects.js +129 -0
  5. package/dist/observe/attribution.js +7 -0
  6. package/dist/observe/boundaries.js +641 -0
  7. package/dist/observe/core/action.js +177 -0
  8. package/dist/observe/core/async.js +846 -0
  9. package/dist/observe/core/attribution-costs.js +66 -0
  10. package/dist/observe/core/attribution-feedback.js +282 -0
  11. package/dist/observe/core/attribution-hooks.js +82 -0
  12. package/dist/observe/core/attribution-queries.js +28 -0
  13. package/dist/observe/core/attribution.js +2095 -0
  14. package/dist/observe/core/constants.js +248 -0
  15. package/dist/{types-cjs/core/context.d.cts → observe/core/context.js} +37 -12
  16. package/dist/observe/core/core.js +1789 -0
  17. package/dist/observe/core/dev.js +293 -0
  18. package/dist/observe/core/effect.js +195 -0
  19. package/dist/observe/core/error-hooks.js +71 -0
  20. package/dist/observe/core/error.js +80 -0
  21. package/dist/observe/core/external.js +98 -0
  22. package/dist/observe/core/graph.js +155 -0
  23. package/dist/observe/core/heap.js +147 -0
  24. package/dist/observe/core/invariants.js +45 -0
  25. package/dist/observe/core/lanes.js +205 -0
  26. package/dist/observe/core/optimistic.js +585 -0
  27. package/dist/observe/core/owner.js +322 -0
  28. package/dist/observe/core/scheduler.js +1428 -0
  29. package/dist/observe/core/verdict.js +669 -0
  30. package/dist/observe/index.js +59 -0
  31. package/dist/observe/map.js +484 -0
  32. package/dist/{types-cjs/signals.d.cts → observe/signals.js} +311 -317
  33. package/dist/observe/store/index.js +40 -0
  34. package/dist/observe/store/next/optimistic.js +660 -0
  35. package/dist/observe/store/next/projection.js +255 -0
  36. package/dist/observe/store/next/reconcile.js +375 -0
  37. package/dist/observe/store/next/store.js +2327 -0
  38. package/dist/observe/store/next/target.js +51 -0
  39. package/dist/observe/store/store.js +325 -0
  40. package/dist/observe/store/storePath.js +103 -0
  41. package/dist/observe/store/utils.js +1023 -0
  42. package/dist/prod/attribution.js +41 -0
  43. package/dist/prod/boundaries.js +128 -76
  44. package/dist/prod/core/action.js +65 -37
  45. package/dist/prod/core/async.js +324 -172
  46. package/dist/prod/core/constants.js +57 -1
  47. package/dist/prod/core/context.js +3 -3
  48. package/dist/prod/core/core.js +1003 -364
  49. package/dist/prod/core/dev.js +17 -1
  50. package/dist/prod/core/effect.js +76 -42
  51. package/dist/prod/core/error-hooks.js +71 -0
  52. package/dist/prod/core/external.js +4 -4
  53. package/dist/prod/core/graph.js +37 -37
  54. package/dist/prod/core/heap.js +61 -56
  55. package/dist/prod/core/lanes.js +107 -63
  56. package/dist/prod/core/optimistic.js +374 -93
  57. package/dist/prod/core/owner.js +62 -46
  58. package/dist/prod/core/scheduler.js +653 -286
  59. package/dist/prod/core/verdict.js +245 -127
  60. package/dist/prod/index.js +15 -5
  61. package/dist/prod/map.js +292 -141
  62. package/dist/prod/signals.js +29 -16
  63. package/dist/prod/store/next/optimistic.js +77 -55
  64. package/dist/prod/store/next/projection.js +31 -20
  65. package/dist/prod/store/next/reconcile.js +87 -84
  66. package/dist/prod/store/next/store.js +718 -401
  67. package/dist/prod/store/next/target.js +32 -10
  68. package/dist/prod/store/store.js +12 -14
  69. package/dist/prod/store/utils.js +950 -130
  70. package/dist/types/attribution.d.ts +20 -0
  71. package/dist/types/attribution.prod.d.ts +22 -0
  72. package/dist/types/boundaries.d.ts +10 -1
  73. package/dist/types/core/action.d.ts +12 -5
  74. package/dist/types/core/async.d.ts +1 -1
  75. package/dist/types/core/attribution-costs.d.ts +35 -0
  76. package/dist/types/core/attribution-feedback.d.ts +133 -0
  77. package/dist/types/core/attribution-hooks.d.ts +147 -9
  78. package/dist/types/core/attribution-queries.d.ts +10 -0
  79. package/dist/types/core/attribution.d.ts +216 -182
  80. package/dist/types/core/constants.d.ts +56 -0
  81. package/dist/types/core/core.d.ts +190 -2
  82. package/dist/types/core/dev.d.ts +246 -28
  83. package/dist/types/core/effect.d.ts +8 -1
  84. package/dist/types/core/error-hooks.d.ts +71 -0
  85. package/dist/types/core/index.d.ts +4 -1
  86. package/dist/types/core/invariants.d.ts +4 -0
  87. package/dist/types/core/lanes.d.ts +39 -5
  88. package/dist/types/core/scheduler.d.ts +153 -0
  89. package/dist/types/core/types.d.ts +32 -9
  90. package/dist/types/index.d.ts +10 -3
  91. package/dist/types/map.d.ts +2 -0
  92. package/dist/types/signals.d.ts +8 -0
  93. package/dist/types/store/index.d.ts +2 -0
  94. package/dist/types/store/next/optimistic.d.ts +1 -1
  95. package/dist/types/store/next/store.d.ts +11 -5
  96. package/dist/types/store/next/target.d.ts +37 -10
  97. package/dist/types/store/utils.d.ts +178 -0
  98. package/package.json +21 -17
  99. package/dist/node.cjs +0 -10541
  100. package/dist/node.dev.cjs +0 -13724
  101. package/dist/types-cjs/affects.d.cts +0 -47
  102. package/dist/types-cjs/boundaries.d.cts +0 -175
  103. package/dist/types-cjs/core/action.d.cts +0 -64
  104. package/dist/types-cjs/core/async.d.cts +0 -23
  105. package/dist/types-cjs/core/attribution-hooks.d.cts +0 -127
  106. package/dist/types-cjs/core/attribution.d.cts +0 -495
  107. package/dist/types-cjs/core/constants.d.cts +0 -159
  108. package/dist/types-cjs/core/core.d.cts +0 -185
  109. package/dist/types-cjs/core/dev.d.cts +0 -136
  110. package/dist/types-cjs/core/effect.d.cts +0 -30
  111. package/dist/types-cjs/core/error.d.cts +0 -56
  112. package/dist/types-cjs/core/external.d.cts +0 -15
  113. package/dist/types-cjs/core/graph.d.cts +0 -28
  114. package/dist/types-cjs/core/heap.d.cts +0 -24
  115. package/dist/types-cjs/core/index.d.cts +0 -13
  116. package/dist/types-cjs/core/invariants.d.cts +0 -59
  117. package/dist/types-cjs/core/lanes.d.cts +0 -54
  118. package/dist/types-cjs/core/optimistic.d.cts +0 -6
  119. package/dist/types-cjs/core/owner.d.cts +0 -124
  120. package/dist/types-cjs/core/scheduler.d.cts +0 -236
  121. package/dist/types-cjs/core/types.d.cts +0 -204
  122. package/dist/types-cjs/core/verdict.d.cts +0 -2
  123. package/dist/types-cjs/index.d.cts +0 -10
  124. package/dist/types-cjs/map.d.cts +0 -69
  125. package/dist/types-cjs/package.json +0 -3
  126. package/dist/types-cjs/store/index.d.cts +0 -18
  127. package/dist/types-cjs/store/next/optimistic.d.cts +0 -25
  128. package/dist/types-cjs/store/next/projection.d.cts +0 -8
  129. package/dist/types-cjs/store/next/reconcile.d.cts +0 -10
  130. package/dist/types-cjs/store/next/store.d.cts +0 -121
  131. package/dist/types-cjs/store/next/target.d.cts +0 -166
  132. package/dist/types-cjs/store/store.d.cts +0 -143
  133. package/dist/types-cjs/store/storePath.d.cts +0 -58
  134. package/dist/types-cjs/store/utils.d.cts +0 -74
@@ -0,0 +1,98 @@
1
+ import { signal, setSignal, read } from "./core.js";
2
+
3
+ import { cleanup } from "./owner.js";
4
+
5
+ import { GlobalQueue } from "./scheduler.js";
6
+
7
+ let externalSourceConfig = null;
8
+
9
+ /**
10
+ * Registers a factory that bridges external reactive systems (e.g. MobX, Vue refs)
11
+ * into Solid's tracking graph. Every computation will be wrapped so that the
12
+ * external library can track its own dependencies alongside Solid's.
13
+ *
14
+ * Multiple calls pipe together: each new factory wraps the previous one.
15
+ *
16
+ * @param config.factory receives `(fn, trigger)` — wrap fn execution in external tracking,
17
+ * call trigger when external deps change. Return `{ track, dispose }`.
18
+ * @param config.untrack optional wrapper for `untrack` — disables external tracking too.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * // Bridge an external "subscribe / notify" library into Solid's graph.
23
+ * // `factory` wraps every Solid compute so the external library can attach
24
+ * // its own dependency tracker; `trigger` re-runs the compute on external
25
+ * // change. `untrack` mirrors Solid's `untrack()` into the external library
26
+ * // so that reads inside `untrack(...)` don't get tracked twice.
27
+ * enableExternalSource({
28
+ * factory: (compute, trigger) => {
29
+ * const sub = externalLib.subscribe(trigger);
30
+ * return {
31
+ * track: prev => externalLib.run(() => compute(prev)),
32
+ * dispose: () => sub.unsubscribe()
33
+ * };
34
+ * },
35
+ * untrack: fn => externalLib.untracked(fn)
36
+ * });
37
+ * ```
38
+ */
39
+ // Wires a freshly created computed through the active external-source bridge.
40
+ // Lives here (installed on GlobalQueue while a config is active) rather than
41
+ // inline in core: esbuild cannot literal-track the mutable config binding the
42
+ // way rollup does, so an inline `if (externalSourceConfig)` block ships in
43
+ // every bundle even though only enableExternalSource() can make it reachable.
44
+ function wireExternalSource(e) {
45
+ const n = signal(undefined, {
46
+ equals: false,
47
+ ownedWrite: true
48
+ });
49
+ const r = externalSourceConfig.factory(e.ht, () => {
50
+ setSignal(n, undefined);
51
+ });
52
+ cleanup(() => r.dispose());
53
+ e.ht = e => {
54
+ read(n);
55
+ return r.track(e);
56
+ };
57
+ }
58
+
59
+ function externalUntrack(e) {
60
+ return externalSourceConfig.untrack(e);
61
+ }
62
+
63
+ // The hooks mirror the config's liveness exactly (installed on enable,
64
+ // removed on reset) so core's null checks stay equivalent to the old
65
+ // `externalSourceConfig` truthiness checks.
66
+ function syncExternalHooks() {
67
+ GlobalQueue._n = externalSourceConfig ? wireExternalSource : null;
68
+ GlobalQueue.In = externalSourceConfig ? externalUntrack : null;
69
+ }
70
+
71
+ function enableExternalSource(e) {
72
+ const {factory: n, untrack: r = e => e()} = e;
73
+ if (externalSourceConfig) {
74
+ const {factory: e, untrack: o} = externalSourceConfig;
75
+ externalSourceConfig = {
76
+ factory: (r, o) => {
77
+ const t = e(r, o);
78
+ const a = n(e => t.track(e), o);
79
+ return {
80
+ track: e => a.track(e),
81
+ dispose() {
82
+ a.dispose();
83
+ t.dispose();
84
+ }
85
+ };
86
+ },
87
+ untrack: e => o(() => r(e))
88
+ };
89
+ } else {
90
+ externalSourceConfig = {
91
+ factory: n,
92
+ untrack: r
93
+ };
94
+ }
95
+ syncExternalHooks();
96
+ }
97
+
98
+ export { enableExternalSource, externalSourceConfig };
@@ -0,0 +1,155 @@
1
+ import { CONFIG_AUTO_DISPOSE, STATUS_PENDING, REACTIVE_DISPOSED, REACTIVE_ZOMBIE, CONFIG_SLOT_NODE, REACTIVE_RECOMPUTING_DEPS } from "./constants.js";
2
+
3
+ import { slotUnobservedHook } from "./core.js";
4
+
5
+ import { deleteFromHeap, queueFor } from "./heap.js";
6
+
7
+ import { disposeChildren } from "./owner.js";
8
+
9
+ import { bumpNotifyEpoch } from "./scheduler.js";
10
+
11
+ // https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L100
12
+ function unlinkSubs(e) {
13
+ const n = e.re;
14
+ const l = e.te;
15
+ const o = e.ie;
16
+ const s = e.bn;
17
+ if (o !== null) o.bn = s; else n.qt = s;
18
+ if (s !== null) s.ie = o; else {
19
+ n.u = o;
20
+ if (o === null) {
21
+ // Slot nodes (store leaves) dispatch to the ONE shared hook — no
22
+ // per-node unobserved closure, no NodeExtension to hold it.
23
+ if (n.C & CONFIG_SLOT_NODE) slotUnobservedHook(n); else n.o?.Kt?.();
24
+ // No more subscribers; only tear down if CONFIG_AUTO_DISPOSE is set.
25
+ // A pending node is exempt: its in-flight async work (or the
26
+ // transition holding it) is an observer — tearing down would orphan
27
+ // the work and re-execute it on the next read. The settle path runs
28
+ // this same last-one-out check when that observer releases (the
29
+ // untracked-read dormancy sweep guards on pending identically).
30
+ const e = n;
31
+ e.ht && e.C & CONFIG_AUTO_DISPOSE && !(e.ft & REACTIVE_ZOMBIE) && !(e.S & STATUS_PENDING) && unobserved(e);
32
+ }
33
+ }
34
+ return l;
35
+ }
36
+
37
+ function trimStaleDeps(e) {
38
+ const n = e.Fe;
39
+ let l = n !== null ? n.te : e.ee;
40
+ if (l !== null) {
41
+ do {
42
+ l = unlinkSubs(l);
43
+ } while (l !== null);
44
+ if (n !== null) n.te = null; else e.ee = null;
45
+ }
46
+ }
47
+
48
+ // Shared by unobserved() and the disposeChildren child loop. The truthy guard
49
+ // (not `!== null`) matters: plain Owners in a child chain have no _deps field,
50
+ // and skipping early also avoids adding one (hidden-class churn) via the
51
+ // null-out below.
52
+ function clearDeps(e) {
53
+ let n = e.ee;
54
+ if (!n) return;
55
+ do {
56
+ n = unlinkSubs(n);
57
+ } while (n !== null);
58
+ e.ee = null;
59
+ e.Fe = null;
60
+ }
61
+
62
+ function unobserved(e) {
63
+ deleteFromHeap(e, queueFor(e));
64
+ clearDeps(e);
65
+ disposeChildren(e, true);
66
+ }
67
+
68
+ /**
69
+ * Deferred dormancy for never-observed auto-dispose computeds (#3078).
70
+ *
71
+ * An untracked top-level read of a subscriber-less observation-lifecycle memo
72
+ * used to call unobserved() inline at the end of read(). That kept the leak
73
+ * closed (the compute links the memo into its deps' sub lists — without a
74
+ * teardown point a never-observed memo is retained by its sources forever;
75
+ * upstream alien-signals has exactly this retention), but it made reads
76
+ * destructive: each read disposed the node, the next read revived it with a
77
+ * full recompute in whatever ambient transition/lane context happened to be
78
+ * current, so consecutive reads could return different answers with no write
79
+ * in between.
80
+ *
81
+ * Instead, reads queue the node here and the scheduler sweeps at the top of
82
+ * the next flush (before runHeap, so a same-tick dirtying is reclaimed
83
+ * instead of recomputed). Reads become idempotent within a tick (the node
84
+ * stays alive and serves its cache, uniform with observed memos) while
85
+ * reclamation still happens within one microtask — the enqueue site arms
86
+ * schedule(), so a flush is guaranteed even when no other work is queued.
87
+ */ const dormantNodes = new Set;
88
+
89
+ function sweepDormant() {
90
+ if (dormantNodes.size === 0) return;
91
+ for (const e of dormantNodes) {
92
+ // Re-validate at sweep time: the node may have gained a subscriber (its
93
+ // lifecycle is the unlinkSubs cascade now), gone pending (in-flight async
94
+ // is an observer; the settle path re-runs last-one-out), lost its
95
+ // AUTO_DISPOSE bit (owner teardown strips it, #3024), or already been
96
+ // torn down.
97
+ if (!e.u && e.C & CONFIG_AUTO_DISPOSE && !(e.S & STATUS_PENDING) && !(e.ft & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))) {
98
+ unobserved(e);
99
+ }
100
+ }
101
+ dormantNodes.clear();
102
+ }
103
+
104
+ // https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L52
105
+ function link(e, n, l = false) {
106
+ // Repeat touches within one pass AND-combine `_pendingObserver`: a probe
107
+ // read (`isPending(() => x())`) beside a value read of the same dep must
108
+ // not relabel the value dependency as probe-only — the value read is what
109
+ // real-error propagation and affects() coverage key off, regardless of
110
+ // read order within the computation.
111
+ const o = n.Fe;
112
+ if (o !== null && o.re === e) {
113
+ o.Ce &&= l;
114
+ return;
115
+ }
116
+ let s = null;
117
+ const t = n.ft & REACTIVE_RECOMPUTING_DEPS;
118
+ if (t) {
119
+ s = o !== null ? o.te : n.ee;
120
+ if (s !== null && s.re === e) {
121
+ s.pe = n.ye;
122
+ n.Fe = s;
123
+ // First touch of this pass: the previous pass's label is stale.
124
+ s.Ce = l;
125
+ return;
126
+ }
127
+ }
128
+ // A link stamped with the current pass generation was created or reused
129
+ // in-order during this recompute, i.e. it already sits in the validated
130
+ // [deps.._depsTail] prefix — the O(1) equivalent of scanning the dep list
131
+ // (the old alien-signals `isValidLink` walk, O(n²) when a computation
132
+ // re-reads earlier deps non-consecutively, e.g. store leaf reads).
133
+ const r = e.qt;
134
+ if (r !== null && r.se === n && (!t || r.pe === n.ye)) {
135
+ // Gen-matched during a recompute = repeat touch this pass (AND); outside
136
+ // a recompute there is no pass boundary, so the latest read labels it.
137
+ if (t) r.Ce &&= l; else r.Ce = l;
138
+ return;
139
+ }
140
+ const u = n.Fe = e.qt = {
141
+ re: e,
142
+ se: n,
143
+ te: s,
144
+ bn: r,
145
+ ie: null,
146
+ pe: n.ye,
147
+ Ce: l
148
+ };
149
+ if (o !== null) o.te = u; else n.ee = u;
150
+ if (r !== null) r.ie = u; else e.u = u;
151
+ // New subscriber edge: staged-rewrite skips (§12d) must not miss it.
152
+ bumpNotifyEpoch();
153
+ }
154
+
155
+ export { clearDeps, dormantNodes, link, sweepDormant, trimStaleDeps, unlinkSubs, unobserved };
@@ -0,0 +1,147 @@
1
+ import "./core.js";
2
+
3
+ import { REACTIVE_IN_HEAP, REACTIVE_IN_HEAP_HEIGHT, REACTIVE_ZOMBIE, REACTIVE_RECOMPUTING_DEPS, REACTIVE_MANUAL_WRITE, REACTIVE_CHECK, REACTIVE_DIRTY, CONFIG_FW_CHILDREN } from "./constants.js";
4
+
5
+ import { zombieQueue, dirtyQueue } from "./scheduler.js";
6
+
7
+ /** The queue a node belongs to, picked from its own zombie flag. */ function queueFor(e) {
8
+ return e.ft & REACTIVE_ZOMBIE ? zombieQueue : dirtyQueue;
9
+ }
10
+
11
+ /**
12
+ * Schedule one subscriber to re-run on the next flush: inserted into its own
13
+ * (zombie-flag-routed) heap with the `_min` cursor pulled down. Tracked
14
+ * effects ride the heap too — the heap visit is their (empty) compute phase,
15
+ * which hands the callback to the user queue once the pass has committed
16
+ * (see GlobalQueue._update, #3291).
17
+ */ function enqueueSub(e) {
18
+ const E = queueFor(e);
19
+ if (E.Ct > e.Rt) E.Ct = e.Rt;
20
+ insertIntoHeap(e, E);
21
+ }
22
+
23
+ function actualInsertIntoHeap(e, E) {
24
+ const t = (e._parent?.cn ? e._parent.fn?.Rt : e._parent?.Rt) ?? -1;
25
+ if (t >= e.Rt) e.Rt = t + 1;
26
+ const n = e.Rt;
27
+ const I = E.eE[n];
28
+ if (I === undefined) E.eE[n] = e; else {
29
+ const E = I.Mt;
30
+ E.Wt = e;
31
+ e.Mt = E;
32
+ I.Mt = e;
33
+ }
34
+ if (n > E.EE) E.EE = n;
35
+ }
36
+
37
+ function insertIntoHeap(e, E) {
38
+ let t = e.ft;
39
+ // RECOMPUTING refusals are not always losses: a genuinely missed wake (a
40
+ // write to a link this pass already validated) is latched link-side in
41
+ // insertSubs as REACTIVE_MISSED_WAKE for recompute's tail (#3037).
42
+ if (t & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS | REACTIVE_MANUAL_WRITE)) return;
43
+ if (t & REACTIVE_CHECK) {
44
+ e.ft = t & -4 | REACTIVE_DIRTY | REACTIVE_IN_HEAP;
45
+ } else {
46
+ e.ft = t | REACTIVE_IN_HEAP;
47
+ // An unmarked node entering an already-marked heap is marked on the
48
+ // spot, keeping the markHeap memo valid. `_marked` is only reset by
49
+ // runHeap, so a write between two mid-tick pulls (read-time markHeap +
50
+ // updateIfNecessary) would otherwise leave this node unmarked and every
51
+ // downstream pull stale until the next flush (#2922: the second
52
+ // `latest()` returned the first write's value). Invalidating the memo
53
+ // instead re-walked the WHOLE heap on the next pull — with N effects
54
+ // parked in the heap for a synchronous mount (each row writing a ref
55
+ // signal its effect subscribes to), mounting N rows was O(N²) (#3350).
56
+ // markNode's own guard skips an already-DIRTY node.
57
+ if (E.tE) markNode(e);
58
+ }
59
+ if (!(t & REACTIVE_IN_HEAP_HEIGHT)) actualInsertIntoHeap(e, E);
60
+ }
61
+
62
+ function insertIntoHeapHeight(e, E) {
63
+ let t = e.ft;
64
+ if (t & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS | REACTIVE_IN_HEAP_HEIGHT | REACTIVE_MANUAL_WRITE)) return;
65
+ e.ft = t | REACTIVE_IN_HEAP_HEIGHT;
66
+ actualInsertIntoHeap(e, E);
67
+ }
68
+
69
+ function deleteFromHeap(e, E) {
70
+ const t = e.ft;
71
+ if (!(t & (REACTIVE_IN_HEAP | REACTIVE_IN_HEAP_HEIGHT))) return;
72
+ e.ft = t & -25;
73
+ const n = e.Rt;
74
+ if (e.Mt === e) E.eE[n] = undefined; else {
75
+ const t = e.Wt;
76
+ const I = E.eE[n];
77
+ const o = t ?? I;
78
+ if (e === I) E.eE[n] = t; else e.Mt.Wt = t;
79
+ o.Mt = e.Mt;
80
+ }
81
+ e.Mt = e;
82
+ e.Wt = undefined;
83
+ }
84
+
85
+ function markHeap(e) {
86
+ if (e.tE) return;
87
+ e.tE = true;
88
+ for (let E = 0; E <= e.EE; E++) {
89
+ for (let t = e.eE[E]; t !== undefined; t = t.Wt) {
90
+ if (t.ft & REACTIVE_IN_HEAP) markNode(t);
91
+ }
92
+ }
93
+ }
94
+
95
+ function markNode(e, E = REACTIVE_DIRTY) {
96
+ const t = e.ft;
97
+ if ((t & (REACTIVE_CHECK | REACTIVE_DIRTY)) >= E) return;
98
+ e.ft = t & -4 | E;
99
+ for (let E = e.u; E !== null; E = E.ie) {
100
+ markNode(E.se, REACTIVE_CHECK);
101
+ }
102
+ // Firewall children (projection machinery only): gate the cold-extension
103
+ // deref on the config bit — markNode runs per sub edge per write, and an
104
+ // unconditional _x chase here taxed every propagation (diamond -22%).
105
+ if (e.C & CONFIG_FW_CHILDREN) {
106
+ for (let E = e.o.i; E !== null; E = E.ue) {
107
+ for (let e = E.u; e !== null; e = e.ie) {
108
+ markNode(e.se, REACTIVE_CHECK);
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ function runHeap(e, E) {
115
+ e.tE = false;
116
+ for (e.Ct = 0; e.Ct <= e.EE; e.Ct++) {
117
+ let t = e.eE[e.Ct];
118
+ while (t !== undefined) {
119
+ if (t.ft & REACTIVE_IN_HEAP) E(t); else adjustHeight(t, e);
120
+ t = e.eE[e.Ct];
121
+ }
122
+ }
123
+ e.EE = 0;
124
+ }
125
+
126
+ function adjustHeight(e, E) {
127
+ deleteFromHeap(e, E);
128
+ let t = e.Rt;
129
+ for (let E = e.ee; E; E = E.te) {
130
+ const e = E.re;
131
+ const n = e.ne || e;
132
+ if (n.ht && n.Rt >= t) t = n.Rt + 1;
133
+ }
134
+ if (e.Rt !== t) {
135
+ e.Rt = t;
136
+ for (let E = e.u; E !== null; E = E.ie) {
137
+ // Route each subscriber by its own zombie flag, mirroring the
138
+ // post-recompute height-adjust path. Inserting into the running `heap`
139
+ // unconditionally can park a zombie in `dirtyQueue` (or a live node in
140
+ // `zombieQueue`), breaking the flag/queue invariant `deleteFromHeap`
141
+ // relies on — the same corruption class as #2759.
142
+ insertIntoHeapHeight(E.se, queueFor(E.se));
143
+ }
144
+ }
145
+ }
146
+
147
+ export { deleteFromHeap, enqueueSub, insertIntoHeap, insertIntoHeapHeight, markHeap, markNode, queueFor, runHeap };
@@ -0,0 +1,45 @@
1
+ import "./core.js";
2
+
3
+ import "./dev.js";
4
+
5
+ /**
6
+ * INV-2: a node with an *active* override must be registered for reversion in
7
+ * the queue's or a transition's `_optimisticNodes`. An unregistered active
8
+ * override would survive transition completion forever. Runs at the end of
9
+ * every flush (not just quiescence — the invariant holds mid-transition).
10
+ * (There is no revert-target requirement: authoritative values commit
11
+ * silently into `_value` under the override mask — A17 — so reverting is
12
+ * just dropping the override.)
13
+ */ function devCheckActiveOverrides(e) {
14
+ return;
15
+ }
16
+
17
+ /** INV-1: an isPending() probe must never leak past its own call. */ function devCheckFlushStart() {
18
+ return;
19
+ }
20
+
21
+ /**
22
+ * Companion-vs-oracle census (#2838 pre-work). A NON-ASSERTING diff logger:
23
+ * at the end of every flush it compares each live companion's cached state
24
+ * against a fresh oracle and logs every distinct divergence fingerprint once
25
+ * (console, `[census]` prefix). Legit lane-scoped windows will show up too —
26
+ * the census exists to enumerate the full taxonomy of mid-flight divergence
27
+ * so the write-driven redesign knows every case it must produce, not to
28
+ * judge them. Enabled only when the COMPANION_CENSUS env var is set (in
29
+ * addition to `false`), so normal test runs pay one boolean check.
30
+ */ typeof globalThis !== "undefined" && !!globalThis.process?.env?.COMPANION_CENSUS;
31
+
32
+ function devCensusCompanions(e) {
33
+ return;
34
+ }
35
+
36
+ /**
37
+ * Quiescence checks. Run only when the system is fully drained: nothing
38
+ * scheduled, no active/stashed transitions, no live lanes. At that point no
39
+ * transition-scoped state may survive, and the lazily-created companions must
40
+ * agree with a fresh computation of their owner's state.
41
+ */ function devCheckQuiescent(e) {
42
+ return;
43
+ }
44
+
45
+ export { devCensusCompanions, devCheckActiveOverrides, devCheckFlushStart, devCheckQuiescent };
@@ -0,0 +1,205 @@
1
+ import { CONFIG_DERIVED_OVERRIDE, CONFIG_HAS_LANE, REACTIVE_DISPOSED } from "./constants.js";
2
+
3
+ import { hasActiveOverride, ext, ownsHold, currentOptimisticLane } from "./core.js";
4
+
5
+ import { enqueueSub } from "./heap.js";
6
+
7
+ import { currentTransition, activeTransition, waitingTransition } from "./scheduler.js";
8
+
9
+ // Map from optimistic signal to its lane (reused for multiple writes to same signal)
10
+ const signalLanes = new WeakMap;
11
+
12
+ // All active lanes (for cleanup on transition completion)
13
+ const activeLanes = new Set;
14
+
15
+ /**
16
+ * Get an existing lane for a signal or create a new one.
17
+ * Reuses lane for multiple writes to the same signal.
18
+ */ function getOrCreateLane(n) {
19
+ let e = signalLanes.get(n);
20
+ if (e) {
21
+ return findLane(e);
22
+ }
23
+ // Detect parent lane: _parentSource chains from pendingSignal → pendingValueComputed → original.
24
+ // The child lane should not merge with the parent lane.
25
+ const i = n.o?.nn;
26
+ const t = i?.o?.de;
27
+ const r = t ? findLane(t) : null;
28
+ e = {
29
+ Ln: n,
30
+ Ie: new Set,
31
+ vn: [ [], [] ],
32
+ wn: null,
33
+ Te: activeTransition,
34
+ Hn: r
35
+ };
36
+ signalLanes.set(n, e);
37
+ activeLanes.add(e);
38
+ // A companion may have written before the owner's first optimistic write
39
+ // (affects() as an action's first statement pokes the verdict companion of a
40
+ // still lane-less node, #2887), leaving its lane parentless. Adopt it now:
41
+ // parent-child is a property of the nodes, not of write order — otherwise
42
+ // the owner's write merges the companion's subscribers into this lane and
43
+ // their effects wait on its async instead of flushing immediately.
44
+ adoptCompanionLane(n.o?.he, e);
45
+ adoptCompanionLane(n.o?.Ge, e);
46
+ return e;
47
+ }
48
+
49
+ function adoptCompanionLane(n, e) {
50
+ if (!n) return;
51
+ const i = signalLanes.get(n);
52
+ if (!i) return;
53
+ const t = findLane(i);
54
+ // Only the companion's own unmerged root is safely re-parentable: a root
55
+ // that absorbed other lanes carries work that is not a child of this owner.
56
+ if (t !== e && t.Ln === n && !t.Hn) t.Hn = e;
57
+ }
58
+
59
+ /**
60
+ * Union-find: find the root lane.
61
+ */ function findLane(n) {
62
+ while (n.wn) n = n.wn;
63
+ return n;
64
+ }
65
+
66
+ /**
67
+ * Is the lane held? `_pendingAsync` records the async the lane OWNS (derived
68
+ * under it); a transaction's reporter map records the async a render effect
69
+ * OBSERVED pending with no boundary taking it (INV-3, the one registration
70
+ * site). A hold needs both — the same rule the transaction itself uses, so a
71
+ * memo nobody renders, or one a fallback-showing boundary caught, cannot tear
72
+ * a frame and holds nothing (#3289). An orphan lane has no observation record
73
+ * and never holds.
74
+ *
75
+ * The observation is looked up per NODE, in whichever live transaction
76
+ * recorded it — not in this lane's transaction. Lanes merge across
77
+ * transactions (#2912: ownership never travels through lanes), so after a
78
+ * merge the root's transaction holds the observations of only one member;
79
+ * the async the other member's transaction observed must hold the merged
80
+ * reveal just the same (A15 for lanes, #3335).
81
+ */ function laneHeld(n) {
82
+ if (!n.Te) return false;
83
+ for (const e of n.Ie) if (waitingTransition(e) !== null) return true;
84
+ return false;
85
+ }
86
+
87
+ /**
88
+ * Lanes mirror transitions (#3460): a render effect OFF a HELD lane that reads
89
+ * a value the lane is revealing — an override, a `latest()` shadow — sees the
90
+ * committed value, exactly as a stale reader of a held transaction does
91
+ * (A15 reveal corollary): it publishes now, with the frame that is on screen
92
+ * (the lane defers its own readers' runs, so the committed value is what is
93
+ * visible), entangles nothing — a sync write is never held by a lane — and
94
+ * re-derives at the release. The release re-run rides the lane's own render
95
+ * queue, which runs when the lane reveals (runLaneEffects) or its transaction
96
+ * commits (cleanupCompletedLanes). A reader ON the lane computes the lane's
97
+ * reveal and takes the value as before.
98
+ *
99
+ * OFF the lane is provenance, not membership — the transaction mirror
100
+ * exactly: a stale reader of a transaction is a pass that runs outside it. A
101
+ * pass under the lane's own transaction is the lane's work — its write (lane
102
+ * posture), or the landing of its async, which re-enters the transaction
103
+ * (#3334) and runs a member with no ambient lane. Read as an outsider, that
104
+ * pass published the committed view and queued a replay that revealed the
105
+ * override beside its unready derivation at the release (`1:0` for an
106
+ * optimistic frame that never became ready; #3479 review). Membership is the
107
+ * wrong test the other way: a member re-run by a sibling's sync write is a
108
+ * mainline pass and shows the committed view.
109
+ */ function readsHeldCommitted(n, e) {
110
+ const i = resolveLane(n);
111
+ if (!i || !laneHeld(i)) return false;
112
+ if (ownsLane(i, n)) return false;
113
+ i.vn[0].push(() => e.ft & REACTIVE_DISPOSED || enqueueSub(e));
114
+ return true;
115
+ }
116
+
117
+ /** The ownership relation for a lane hold (core `ownsHold`, §6 ruling 2): the
118
+ * running pass owns `lane`'s hold if it runs under the transition that owns
119
+ * the lane (the node's, resolved through override ownership and merges) or
120
+ * inside the lane itself. */ function ownsLane(n, e) {
121
+ if (activeTransition !== null) {
122
+ const n = resolveTransition(e);
123
+ if (n && ownsHold(n)) return true;
124
+ }
125
+ return currentOptimisticLane !== null && findLane(currentOptimisticLane) === n;
126
+ }
127
+
128
+ /**
129
+ * Merge two lanes when their dependency graphs overlap.
130
+ */ function mergeLanes(n, e) {
131
+ n = findLane(n);
132
+ e = findLane(e);
133
+ if (n === e) return n;
134
+ e.wn = n;
135
+ // Move (not copy) the merged lane's work: after the merge all routing goes
136
+ // through findLane() to the root, so anything left behind here is dead —
137
+ // and anything *added* here later is a routing bug (INV-5).
138
+ for (const i of e.Ie) n.Ie.add(i);
139
+ e.Ie.clear();
140
+ n.vn[0].push(...e.vn[0]);
141
+ n.vn[1].push(...e.vn[1]);
142
+ e.vn[0].length = 0;
143
+ e.vn[1].length = 0;
144
+ return n;
145
+ }
146
+
147
+ /**
148
+ * Resolve a node's lane: follow union-find chain, verify active, clear if stale.
149
+ */ function resolveLane(n) {
150
+ const e = n.o?.de;
151
+ if (!e) return undefined;
152
+ const i = findLane(e);
153
+ if (activeLanes.has(i)) return i;
154
+ if (n.o !== null) n.o.de = undefined;
155
+ return undefined;
156
+ }
157
+
158
+ function resolveTransition(n) {
159
+ // An active override answers with its owner, not its lane: lanes are
160
+ // scheduling affinity and a shared subscriber merges them across
161
+ // transactions (#2912) — the merged root's _transition would hand this
162
+ // node's override to whichever action wrote last through the shared
163
+ // reader. Chase merge chains; a dead owner settled through another path.
164
+ if (hasActiveOverride(n) && n.o?.jt) {
165
+ const e = ext(n).jt = currentTransition(n.o?.jt);
166
+ if (e.yt !== true) return e;
167
+ if (n.o !== null) n.o.jt = null;
168
+ }
169
+ return resolveLane(n)?.Te ?? n.Te;
170
+ }
171
+
172
+ /**
173
+ * Assign or merge a lane onto a node. At convergence points (node already has
174
+ * a different active lane), merge unless the node has an active override.
175
+ */ function assignOrMergeLane(n, e) {
176
+ const i = findLane(e);
177
+ const t = n.o?.de;
178
+ if (t) {
179
+ // A merged lane is followed to its root like any other: the root is where
180
+ // the subscriber's affinity lives now. Replacing it with the source lane
181
+ // outright (as this once did) skipped the parent/child check below — an
182
+ // isPending reader of two async siblings had their two companion lanes
183
+ // merge, and the next parent-lane notification then moved it onto the
184
+ // held parent, where its verdict waited on the async it reports (#3409).
185
+ const r = findLane(t);
186
+ if (activeLanes.has(r)) {
187
+ // A WRITTEN override is its own lane's source and merges nothing
188
+ // through it; a derived one (lanes stage, #3479) is a plain member —
189
+ // the shared reader that merges two writers' lanes carries one.
190
+ if (r !== i && (!hasActiveOverride(n) || n.C & CONFIG_DERIVED_OVERRIDE)) {
191
+ // Parent-child lanes stay independent so isPending resolves without
192
+ // waiting for the parent's async. The child keeps ownership.
193
+ if (i.Hn && findLane(i.Hn) === r) {
194
+ ext(n).de = e;
195
+ n.C |= CONFIG_HAS_LANE;
196
+ } else if (r.Hn && findLane(r.Hn) === i) ; else mergeLanes(i, r);
197
+ }
198
+ return;
199
+ }
200
+ }
201
+ ext(n).de = e;
202
+ n.C |= CONFIG_HAS_LANE;
203
+ }
204
+
205
+ export { activeLanes, assignOrMergeLane, findLane, getOrCreateLane, hasActiveOverride, laneHeld, mergeLanes, ownsLane, readsHeldCommitted, resolveLane, resolveTransition, signalLanes };