@graphrefly/graphrefly 0.21.0 → 0.23.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 (101) hide show
  1. package/README.md +7 -5
  2. package/dist/chunk-263BEJJO.js +115 -0
  3. package/dist/chunk-263BEJJO.js.map +1 -0
  4. package/dist/chunk-2GQLMQVJ.js +47 -0
  5. package/dist/chunk-2GQLMQVJ.js.map +1 -0
  6. package/dist/chunk-32N5A454.js +36 -0
  7. package/dist/chunk-32N5A454.js.map +1 -0
  8. package/dist/chunk-7TAQJHQV.js +103 -0
  9. package/dist/chunk-7TAQJHQV.js.map +1 -0
  10. package/dist/{chunk-VOQFK7YN.js → chunk-CWYPA63G.js} +109 -259
  11. package/dist/chunk-CWYPA63G.js.map +1 -0
  12. package/dist/{chunk-7IGHIFTT.js → chunk-HVBX5KIW.js} +15 -26
  13. package/dist/chunk-HVBX5KIW.js.map +1 -0
  14. package/dist/chunk-JFONSPNF.js +391 -0
  15. package/dist/chunk-JFONSPNF.js.map +1 -0
  16. package/dist/chunk-NZMBRXQV.js +2330 -0
  17. package/dist/chunk-NZMBRXQV.js.map +1 -0
  18. package/dist/{chunk-XWBVAO2R.js → chunk-PNUZM7PC.js} +20 -30
  19. package/dist/chunk-PNUZM7PC.js.map +1 -0
  20. package/dist/{chunk-ZTCDY5NQ.js → chunk-PX6PDUJ5.js} +34 -50
  21. package/dist/chunk-PX6PDUJ5.js.map +1 -0
  22. package/dist/chunk-XRFJJ2IU.js +2417 -0
  23. package/dist/chunk-XRFJJ2IU.js.map +1 -0
  24. package/dist/chunk-XTLYW4FR.js +6829 -0
  25. package/dist/chunk-XTLYW4FR.js.map +1 -0
  26. package/dist/compat/nestjs/index.cjs +3489 -2286
  27. package/dist/compat/nestjs/index.cjs.map +1 -1
  28. package/dist/compat/nestjs/index.d.cts +6 -4
  29. package/dist/compat/nestjs/index.d.ts +6 -4
  30. package/dist/compat/nestjs/index.js +10 -8
  31. package/dist/core/index.cjs +1706 -1217
  32. package/dist/core/index.cjs.map +1 -1
  33. package/dist/core/index.d.cts +3 -2
  34. package/dist/core/index.d.ts +3 -2
  35. package/dist/core/index.js +37 -34
  36. package/dist/extra/index.cjs +7519 -6125
  37. package/dist/extra/index.cjs.map +1 -1
  38. package/dist/extra/index.d.cts +4 -4
  39. package/dist/extra/index.d.ts +4 -4
  40. package/dist/extra/index.js +63 -34
  41. package/dist/graph/index.cjs +3199 -2212
  42. package/dist/graph/index.cjs.map +1 -1
  43. package/dist/graph/index.d.cts +5 -3
  44. package/dist/graph/index.d.ts +5 -3
  45. package/dist/graph/index.js +24 -11
  46. package/dist/graph-BtdSRHUc.d.cts +1128 -0
  47. package/dist/graph-CEO2FkLY.d.ts +1128 -0
  48. package/dist/{index-DuN3bhtm.d.ts → index-B0tfuXwV.d.cts} +1697 -586
  49. package/dist/index-BFGjXbiP.d.cts +315 -0
  50. package/dist/{index-CgSiUouz.d.ts → index-BPlWVAKY.d.cts} +4 -4
  51. package/dist/index-BUj3ASVe.d.cts +406 -0
  52. package/dist/{index-VHA43cGP.d.cts → index-C59uSJAH.d.cts} +2 -2
  53. package/dist/index-CkElcUY6.d.ts +315 -0
  54. package/dist/index-DSPc5rkv.d.ts +406 -0
  55. package/dist/{index-BjtlNirP.d.cts → index-DgscL7v0.d.ts} +4 -4
  56. package/dist/{index-SFzE_KTa.d.cts → index-RXN94sHK.d.ts} +1697 -586
  57. package/dist/{index-8a605sg9.d.ts → index-jEtF4N7L.d.ts} +2 -2
  58. package/dist/index.cjs +9947 -7949
  59. package/dist/index.cjs.map +1 -1
  60. package/dist/index.d.cts +214 -37
  61. package/dist/index.d.ts +214 -37
  62. package/dist/index.js +919 -648
  63. package/dist/index.js.map +1 -1
  64. package/dist/meta-3QjzotRv.d.ts +41 -0
  65. package/dist/meta-B-Lbs4-O.d.cts +41 -0
  66. package/dist/node-C7PD3sn9.d.cts +1188 -0
  67. package/dist/node-C7PD3sn9.d.ts +1188 -0
  68. package/dist/{observable-DcBwQY7t.d.ts → observable-EyO-moQY.d.ts} +1 -1
  69. package/dist/{observable-C8Kx_O6k.d.cts → observable-axpzv1K2.d.cts} +1 -1
  70. package/dist/patterns/reactive-layout/index.cjs +3205 -2138
  71. package/dist/patterns/reactive-layout/index.cjs.map +1 -1
  72. package/dist/patterns/reactive-layout/index.d.cts +5 -3
  73. package/dist/patterns/reactive-layout/index.d.ts +5 -3
  74. package/dist/patterns/reactive-layout/index.js +7 -4
  75. package/dist/storage-CHT5WE9m.d.ts +182 -0
  76. package/dist/storage-DIgAr7M_.d.cts +182 -0
  77. package/package.json +2 -1
  78. package/dist/chunk-2UDLYZHT.js +0 -2117
  79. package/dist/chunk-2UDLYZHT.js.map +0 -1
  80. package/dist/chunk-4MQ2J6IG.js +0 -1631
  81. package/dist/chunk-4MQ2J6IG.js.map +0 -1
  82. package/dist/chunk-7IGHIFTT.js.map +0 -1
  83. package/dist/chunk-DOSLSFKL.js +0 -162
  84. package/dist/chunk-DOSLSFKL.js.map +0 -1
  85. package/dist/chunk-ECN37NVS.js +0 -6227
  86. package/dist/chunk-ECN37NVS.js.map +0 -1
  87. package/dist/chunk-G66H6ZRK.js +0 -111
  88. package/dist/chunk-G66H6ZRK.js.map +0 -1
  89. package/dist/chunk-VOQFK7YN.js.map +0 -1
  90. package/dist/chunk-WZ2Z2CRV.js +0 -32
  91. package/dist/chunk-WZ2Z2CRV.js.map +0 -1
  92. package/dist/chunk-XWBVAO2R.js.map +0 -1
  93. package/dist/chunk-ZTCDY5NQ.js.map +0 -1
  94. package/dist/graph-KsTe57nI.d.cts +0 -750
  95. package/dist/graph-mILUUqW8.d.ts +0 -750
  96. package/dist/index-B2SvPEbc.d.ts +0 -257
  97. package/dist/index-BHfg_Ez3.d.ts +0 -629
  98. package/dist/index-Bc_diYYJ.d.cts +0 -629
  99. package/dist/index-UudxGnzc.d.cts +0 -257
  100. package/dist/meta-BnG7XAaE.d.cts +0 -778
  101. package/dist/meta-BnG7XAaE.d.ts +0 -778
@@ -20,50 +20,53 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/core/index.ts
21
21
  var core_exports = {};
22
22
  __export(core_exports, {
23
- CLEANUP_RESULT: () => CLEANUP_RESULT,
24
23
  COMPLETE: () => COMPLETE,
24
+ COMPLETE_MSG: () => COMPLETE_MSG,
25
+ COMPLETE_ONLY_BATCH: () => COMPLETE_ONLY_BATCH,
25
26
  DATA: () => DATA,
26
27
  DEFAULT_ACTOR: () => DEFAULT_ACTOR,
27
- DEFAULT_DOWN: () => DEFAULT_DOWN,
28
28
  DIRTY: () => DIRTY,
29
- DynamicNodeImpl: () => DynamicNodeImpl,
29
+ DIRTY_MSG: () => DIRTY_MSG,
30
+ DIRTY_ONLY_BATCH: () => DIRTY_ONLY_BATCH,
30
31
  ERROR: () => ERROR,
32
+ GraphReFlyConfig: () => GraphReFlyConfig,
31
33
  GuardDenied: () => GuardDenied,
32
34
  INVALIDATE: () => INVALIDATE,
35
+ INVALIDATE_MSG: () => INVALIDATE_MSG,
36
+ INVALIDATE_ONLY_BATCH: () => INVALIDATE_ONLY_BATCH,
37
+ NodeImpl: () => NodeImpl,
33
38
  PAUSE: () => PAUSE,
34
39
  RESOLVED: () => RESOLVED,
40
+ RESOLVED_MSG: () => RESOLVED_MSG,
41
+ RESOLVED_ONLY_BATCH: () => RESOLVED_ONLY_BATCH,
35
42
  RESUME: () => RESUME,
36
- ResettableTimer: () => ResettableTimer,
37
43
  START: () => START,
44
+ START_MSG: () => START_MSG,
38
45
  TEARDOWN: () => TEARDOWN,
46
+ TEARDOWN_MSG: () => TEARDOWN_MSG,
47
+ TEARDOWN_ONLY_BATCH: () => TEARDOWN_ONLY_BATCH,
39
48
  accessHintForGuard: () => accessHintForGuard,
40
49
  advanceVersion: () => advanceVersion,
50
+ autoTrackNode: () => autoTrackNode,
41
51
  batch: () => batch,
42
- bridge: () => bridge,
43
- cleanupResult: () => cleanupResult,
52
+ configure: () => configure,
44
53
  createVersioning: () => createVersioning,
54
+ defaultConfig: () => defaultConfig,
45
55
  defaultHash: () => defaultHash,
46
56
  derived: () => derived,
47
57
  downWithBatch: () => downWithBatch,
48
58
  dynamicNode: () => dynamicNode,
49
59
  effect: () => effect,
50
60
  isBatching: () => isBatching,
51
- isKnownMessageType: () => isKnownMessageType,
52
- isLocalOnly: () => isLocalOnly,
53
- isPhase2Message: () => isPhase2Message,
54
- isTerminalMessage: () => isTerminalMessage,
55
61
  isV1: () => isV1,
56
- knownMessageTypes: () => knownMessageTypes,
57
- messageTier: () => messageTier,
58
62
  monotonicNs: () => monotonicNs,
59
63
  node: () => node,
60
64
  normalizeActor: () => normalizeActor,
61
- partitionForBatch: () => partitionForBatch,
62
65
  pipe: () => pipe,
63
66
  policy: () => policy,
64
67
  policyFromRules: () => policyFromRules,
65
68
  producer: () => producer,
66
- propagatesToMeta: () => propagatesToMeta,
69
+ registerBuiltins: () => registerBuiltins,
67
70
  resolveDescribeFields: () => resolveDescribeFields,
68
71
  state: () => state,
69
72
  wallClockNs: () => wallClockNs
@@ -82,66 +85,27 @@ function normalizeActor(actor) {
82
85
  };
83
86
  }
84
87
 
85
- // src/core/messages.ts
86
- var START = /* @__PURE__ */ Symbol.for("graphrefly/START");
87
- var DATA = /* @__PURE__ */ Symbol.for("graphrefly/DATA");
88
- var DIRTY = /* @__PURE__ */ Symbol.for("graphrefly/DIRTY");
89
- var RESOLVED = /* @__PURE__ */ Symbol.for("graphrefly/RESOLVED");
90
- var INVALIDATE = /* @__PURE__ */ Symbol.for("graphrefly/INVALIDATE");
91
- var PAUSE = /* @__PURE__ */ Symbol.for("graphrefly/PAUSE");
92
- var RESUME = /* @__PURE__ */ Symbol.for("graphrefly/RESUME");
93
- var TEARDOWN = /* @__PURE__ */ Symbol.for("graphrefly/TEARDOWN");
94
- var COMPLETE = /* @__PURE__ */ Symbol.for("graphrefly/COMPLETE");
95
- var ERROR = /* @__PURE__ */ Symbol.for("graphrefly/ERROR");
96
- var knownMessageTypes = [
97
- START,
98
- DATA,
99
- DIRTY,
100
- RESOLVED,
101
- INVALIDATE,
102
- PAUSE,
103
- RESUME,
104
- TEARDOWN,
105
- COMPLETE,
106
- ERROR
107
- ];
108
- var knownMessageSet = new Set(knownMessageTypes);
109
- function isKnownMessageType(t) {
110
- return knownMessageSet.has(t);
111
- }
112
- function messageTier(t) {
113
- if (t === START) return 0;
114
- if (t === DIRTY || t === INVALIDATE) return 1;
115
- if (t === PAUSE || t === RESUME) return 2;
116
- if (t === DATA || t === RESOLVED) return 3;
117
- if (t === COMPLETE || t === ERROR) return 4;
118
- if (t === TEARDOWN) return 5;
119
- return 1;
120
- }
121
- function isPhase2Message(msg) {
122
- const t = msg[0];
123
- return t === DATA || t === RESOLVED;
124
- }
125
- function isTerminalMessage(t) {
126
- return t === COMPLETE || t === ERROR;
127
- }
128
- function isLocalOnly(t) {
129
- if (!knownMessageSet.has(t)) return false;
130
- return messageTier(t) < 3;
131
- }
132
- function propagatesToMeta(t) {
133
- return t === TEARDOWN;
134
- }
135
-
136
88
  // src/core/batch.ts
137
89
  var MAX_DRAIN_ITERATIONS = 1e3;
138
90
  var batchDepth = 0;
139
91
  var flushInProgress = false;
140
- var pendingPhase2 = [];
141
- var pendingPhase3 = [];
92
+ var drainPhase2 = [];
93
+ var drainPhase3 = [];
94
+ var drainPhase4 = [];
95
+ var flushHooks = [];
142
96
  function isBatching() {
143
97
  return batchDepth > 0 || flushInProgress;
144
98
  }
99
+ function isExplicitlyBatching() {
100
+ return batchDepth > 0;
101
+ }
102
+ function registerBatchFlushHook(hook) {
103
+ if (batchDepth > 0) {
104
+ flushHooks.push(hook);
105
+ } else {
106
+ hook();
107
+ }
108
+ }
145
109
  function batch(fn) {
146
110
  batchDepth += 1;
147
111
  let threw = false;
@@ -155,8 +119,16 @@ function batch(fn) {
155
119
  if (batchDepth === 0) {
156
120
  if (threw) {
157
121
  if (!flushInProgress) {
158
- pendingPhase2.length = 0;
159
- pendingPhase3.length = 0;
122
+ const hooks = flushHooks.splice(0);
123
+ for (const h of hooks) {
124
+ try {
125
+ h();
126
+ } catch {
127
+ }
128
+ }
129
+ drainPhase2.length = 0;
130
+ drainPhase3.length = 0;
131
+ drainPhase4.length = 0;
160
132
  }
161
133
  } else {
162
134
  drainPending();
@@ -166,147 +138,348 @@ function batch(fn) {
166
138
  }
167
139
  function drainPending() {
168
140
  const ownsFlush = !flushInProgress;
169
- if (ownsFlush) {
170
- flushInProgress = true;
171
- }
141
+ if (ownsFlush) flushInProgress = true;
172
142
  const errors = [];
143
+ let iterations = 0;
173
144
  try {
174
- let iterations = 0;
175
- while (pendingPhase2.length > 0 || pendingPhase3.length > 0) {
176
- while (pendingPhase2.length > 0) {
177
- iterations += 1;
178
- if (iterations > MAX_DRAIN_ITERATIONS) {
179
- pendingPhase2.length = 0;
180
- pendingPhase3.length = 0;
181
- throw new Error(
182
- `batch drain exceeded ${MAX_DRAIN_ITERATIONS} iterations \u2014 likely a reactive cycle`
183
- );
184
- }
185
- const ops = pendingPhase2.splice(0);
186
- for (const run of ops) {
145
+ while (drainPhase2.length > 0 || drainPhase3.length > 0 || drainPhase4.length > 0 || ownsFlush && flushHooks.length > 0) {
146
+ if (ownsFlush && flushHooks.length > 0) {
147
+ const hooks = flushHooks.splice(0);
148
+ for (const h of hooks) {
187
149
  try {
188
- run();
150
+ h();
189
151
  } catch (e) {
190
152
  errors.push(e);
191
153
  }
192
154
  }
155
+ continue;
193
156
  }
194
- if (pendingPhase3.length > 0) {
195
- iterations += 1;
196
- if (iterations > MAX_DRAIN_ITERATIONS) {
197
- pendingPhase2.length = 0;
198
- pendingPhase3.length = 0;
199
- throw new Error(
200
- `batch drain exceeded ${MAX_DRAIN_ITERATIONS} iterations \u2014 likely a reactive cycle`
201
- );
202
- }
203
- const ops = pendingPhase3.splice(0);
204
- for (const run of ops) {
205
- try {
206
- run();
207
- } catch (e) {
208
- errors.push(e);
209
- }
157
+ iterations += 1;
158
+ if (iterations > MAX_DRAIN_ITERATIONS) {
159
+ drainPhase2.length = 0;
160
+ drainPhase3.length = 0;
161
+ drainPhase4.length = 0;
162
+ throw new Error(
163
+ `batch drain exceeded ${MAX_DRAIN_ITERATIONS} iterations \u2014 likely a reactive cycle`
164
+ );
165
+ }
166
+ const queue = drainPhase2.length > 0 ? drainPhase2 : drainPhase3.length > 0 ? drainPhase3 : drainPhase4;
167
+ const ops = queue.splice(0);
168
+ for (const run of ops) {
169
+ try {
170
+ run();
171
+ } catch (e) {
172
+ errors.push(e);
210
173
  }
211
174
  }
212
175
  }
213
176
  } finally {
214
- if (ownsFlush) {
215
- flushInProgress = false;
216
- }
217
- }
218
- if (errors.length === 1) {
219
- throw errors[0];
177
+ if (ownsFlush) flushInProgress = false;
220
178
  }
179
+ if (errors.length === 1) throw errors[0];
221
180
  if (errors.length > 1) {
222
181
  throw new AggregateError(errors, "batch drain: multiple callbacks threw");
223
182
  }
224
183
  }
225
- function partitionForBatch(messages) {
226
- const immediate = [];
227
- const deferred = [];
228
- const terminal = [];
229
- for (const m of messages) {
230
- if (isPhase2Message(m)) {
231
- deferred.push(m);
232
- } else if (isTerminalMessage(m[0])) {
233
- terminal.push(m);
234
- } else {
235
- immediate.push(m);
236
- }
237
- }
238
- return { immediate, deferred, terminal };
239
- }
240
- function downWithBatch(sink, messages, phase = 2, options) {
241
- if (messages.length === 0) {
242
- return;
243
- }
244
- if (options?.strategy === "sequential") {
245
- _downSequential(sink, messages, phase);
246
- return;
247
- }
248
- const queue = phase === 3 ? pendingPhase3 : pendingPhase2;
184
+ function downWithBatch(sink, messages, tierOf) {
185
+ if (messages.length === 0) return;
249
186
  if (messages.length === 1) {
250
- const t = messages[0][0];
251
- if (t === DATA || t === RESOLVED) {
252
- if (isBatching()) {
253
- queue.push(() => sink(messages));
254
- } else {
255
- sink(messages);
256
- }
257
- } else if (isTerminalMessage(t)) {
258
- if (isBatching()) {
259
- queue.push(() => sink(messages));
260
- } else {
261
- sink(messages);
262
- }
263
- } else {
187
+ const tier = tierOf(messages[0][0]);
188
+ if (tier < 3 || !isBatching()) {
264
189
  sink(messages);
190
+ return;
265
191
  }
192
+ const queue = tier >= 5 ? drainPhase4 : tier === 4 ? drainPhase3 : drainPhase2;
193
+ queue.push(() => sink(messages));
266
194
  return;
267
195
  }
268
- const { immediate, deferred, terminal } = partitionForBatch(messages);
269
- if (immediate.length > 0) {
196
+ const n = messages.length;
197
+ let phase2Start = n;
198
+ let phase3Start = n;
199
+ let phase4Start = n;
200
+ let i = 0;
201
+ while (i < n && tierOf(messages[i][0]) < 3) i++;
202
+ phase2Start = i;
203
+ while (i < n && tierOf(messages[i][0]) === 3) i++;
204
+ phase3Start = i;
205
+ while (i < n && tierOf(messages[i][0]) === 4) i++;
206
+ phase4Start = i;
207
+ const batching = isBatching();
208
+ if (phase2Start > 0) {
209
+ const immediate = messages.slice(0, phase2Start);
270
210
  sink(immediate);
271
211
  }
272
- if (isBatching()) {
273
- if (deferred.length > 0) {
274
- queue.push(() => sink(deferred));
275
- }
276
- if (terminal.length > 0) {
277
- queue.push(() => sink(terminal));
278
- }
279
- } else {
280
- if (deferred.length > 0) {
281
- sink(deferred);
282
- }
283
- if (terminal.length > 0) {
284
- sink(terminal);
285
- }
212
+ if (phase3Start > phase2Start) {
213
+ const phase2 = messages.slice(phase2Start, phase3Start);
214
+ if (batching) drainPhase2.push(() => sink(phase2));
215
+ else sink(phase2);
216
+ }
217
+ if (phase4Start > phase3Start) {
218
+ const phase3 = messages.slice(phase3Start, phase4Start);
219
+ if (batching) drainPhase3.push(() => sink(phase3));
220
+ else sink(phase3);
221
+ }
222
+ if (n > phase4Start) {
223
+ const phase4 = messages.slice(phase4Start, n);
224
+ if (batching) drainPhase4.push(() => sink(phase4));
225
+ else sink(phase4);
286
226
  }
287
227
  }
288
- function _downSequential(sink, messages, phase = 2) {
289
- const dataQueue = phase === 3 ? pendingPhase3 : pendingPhase2;
290
- for (const msg of messages) {
291
- const tier = messageTier(msg[0]);
292
- if (tier === 3) {
293
- if (isBatching()) {
294
- const m = msg;
295
- dataQueue.push(() => sink([m]));
296
- } else {
297
- sink([msg]);
298
- }
299
- } else if (tier >= 4) {
300
- if (isBatching()) {
301
- const m = msg;
302
- pendingPhase3.push(() => sink([m]));
303
- } else {
304
- sink([msg]);
305
- }
306
- } else {
307
- sink([msg]);
228
+
229
+ // src/core/clock.ts
230
+ function monotonicNs() {
231
+ return Math.trunc(performance.now() * 1e6);
232
+ }
233
+ function wallClockNs() {
234
+ return Date.now() * 1e6;
235
+ }
236
+
237
+ // src/core/messages.ts
238
+ var START = /* @__PURE__ */ Symbol.for("graphrefly/START");
239
+ var DATA = /* @__PURE__ */ Symbol.for("graphrefly/DATA");
240
+ var DIRTY = /* @__PURE__ */ Symbol.for("graphrefly/DIRTY");
241
+ var RESOLVED = /* @__PURE__ */ Symbol.for("graphrefly/RESOLVED");
242
+ var INVALIDATE = /* @__PURE__ */ Symbol.for("graphrefly/INVALIDATE");
243
+ var PAUSE = /* @__PURE__ */ Symbol.for("graphrefly/PAUSE");
244
+ var RESUME = /* @__PURE__ */ Symbol.for("graphrefly/RESUME");
245
+ var TEARDOWN = /* @__PURE__ */ Symbol.for("graphrefly/TEARDOWN");
246
+ var COMPLETE = /* @__PURE__ */ Symbol.for("graphrefly/COMPLETE");
247
+ var ERROR = /* @__PURE__ */ Symbol.for("graphrefly/ERROR");
248
+ var DIRTY_MSG = Object.freeze([DIRTY]);
249
+ var RESOLVED_MSG = Object.freeze([RESOLVED]);
250
+ var INVALIDATE_MSG = Object.freeze([INVALIDATE]);
251
+ var START_MSG = Object.freeze([START]);
252
+ var COMPLETE_MSG = Object.freeze([COMPLETE]);
253
+ var TEARDOWN_MSG = Object.freeze([TEARDOWN]);
254
+ var DIRTY_ONLY_BATCH = Object.freeze([DIRTY_MSG]);
255
+ var RESOLVED_ONLY_BATCH = Object.freeze([RESOLVED_MSG]);
256
+ var INVALIDATE_ONLY_BATCH = Object.freeze([INVALIDATE_MSG]);
257
+ var COMPLETE_ONLY_BATCH = Object.freeze([COMPLETE_MSG]);
258
+ var TEARDOWN_ONLY_BATCH = Object.freeze([TEARDOWN_MSG]);
259
+
260
+ // src/core/config.ts
261
+ var GraphReFlyConfig = class {
262
+ _messageTypes = /* @__PURE__ */ new Map();
263
+ _codecs = /* @__PURE__ */ new Map();
264
+ _onMessage;
265
+ _onSubscribe;
266
+ _defaultVersioning;
267
+ _defaultHashFn;
268
+ _inspectorEnabled = !(typeof process !== "undefined" && process.env?.NODE_ENV === "production");
269
+ _globalInspector;
270
+ _frozen = false;
271
+ /**
272
+ * Pre-bound tier lookup — shared by every node bound to this config. Since
273
+ * the registry is frozen on first hook access, this closure can be built
274
+ * once in the constructor and handed directly to `downWithBatch` /
275
+ * `_frameBatch` paths without per-node or per-emission `.bind(config)`
276
+ * allocation.
277
+ */
278
+ tierOf;
279
+ constructor(init) {
280
+ this._onMessage = init.onMessage;
281
+ this._onSubscribe = init.onSubscribe;
282
+ this._defaultVersioning = init.defaultVersioning;
283
+ this._defaultHashFn = init.defaultHashFn;
284
+ this.tierOf = (t) => {
285
+ const reg = this._messageTypes.get(t);
286
+ return reg != null ? reg.tier : 1;
287
+ };
288
+ }
289
+ // --- Hook getters (freeze on read) ---
290
+ get onMessage() {
291
+ this._frozen = true;
292
+ return this._onMessage;
293
+ }
294
+ get onSubscribe() {
295
+ this._frozen = true;
296
+ return this._onSubscribe;
297
+ }
298
+ // --- Hook setters (throw when frozen) ---
299
+ set onMessage(v) {
300
+ this._assertUnfrozen();
301
+ this._onMessage = v;
302
+ }
303
+ set onSubscribe(v) {
304
+ this._assertUnfrozen();
305
+ this._onSubscribe = v;
306
+ }
307
+ /**
308
+ * Default versioning level applied to every node bound to this config,
309
+ * unless the node's own `opts.versioning` provides an explicit override.
310
+ * Setting this is only allowed before the config freezes (i.e., before
311
+ * the first node is created) so every node in the graph sees a
312
+ * consistent starting level. Individual nodes can still opt into a
313
+ * higher level via `opts.versioning`, or post-hoc via
314
+ * `NodeImpl._applyVersioning(level)` when the node is quiescent.
315
+ *
316
+ * v0 is the minimum opt-in — unversioned nodes (`undefined`) skip
317
+ * the version counter entirely. v1 adds content-addressed cid.
318
+ * Future levels (v2, v3) are reserved for linked-history and
319
+ * cryptographic attestation extensions.
320
+ */
321
+ get defaultVersioning() {
322
+ return this._defaultVersioning;
323
+ }
324
+ set defaultVersioning(v) {
325
+ this._assertUnfrozen();
326
+ this._defaultVersioning = v;
327
+ }
328
+ /**
329
+ * Default content-hash function applied to every versioned node bound
330
+ * to this config, unless the node's own `opts.versioningHash` provides
331
+ * an explicit override. Use this when a graph needs a non-default hash
332
+ * — e.g., swap the vendored sync SHA-256 for a faster non-crypto hash
333
+ * (xxHash, FNV-1a) in hot-path workloads, or a stronger hash when
334
+ * versioning v1 cids are used as audit anchors.
335
+ *
336
+ * Only settable before the config freezes. Individual nodes can still
337
+ * override via `opts.versioningHash`.
338
+ */
339
+ get defaultHashFn() {
340
+ return this._defaultHashFn;
341
+ }
342
+ set defaultHashFn(v) {
343
+ this._assertUnfrozen();
344
+ this._defaultHashFn = v;
345
+ }
346
+ /**
347
+ * When `false`, structured observation options (`causal`, `timeline`)
348
+ * and `Graph.trace()` writes are no-ops. Raw `Graph.observe()` always
349
+ * works. Default: `true` outside production (`NODE_ENV !== "production"`).
350
+ *
351
+ * Settable at any time — inspector gating is an operational concern, not
352
+ * a protocol invariant, so it does NOT require freeze before node creation.
353
+ */
354
+ get inspectorEnabled() {
355
+ return this._inspectorEnabled;
356
+ }
357
+ set inspectorEnabled(v) {
358
+ this._inspectorEnabled = v;
359
+ }
360
+ /**
361
+ * Process-global observability hook (Redux-DevTools-style full-graph
362
+ * tracer). Fires once per outgoing batch from every node bound to this
363
+ * config, gated by {@link inspectorEnabled}. See {@link GlobalInspectorHook}.
364
+ *
365
+ * Settable at any time — like {@link inspectorEnabled} this is operational,
366
+ * not protocol-shaping, so it does NOT trigger config freeze.
367
+ */
368
+ get globalInspector() {
369
+ return this._globalInspector;
370
+ }
371
+ set globalInspector(v) {
372
+ this._globalInspector = v;
373
+ }
374
+ // --- Registry (writes require unfrozen; reads are free lookups) ---
375
+ /**
376
+ * Register a custom message type. Must be called before any node that
377
+ * uses this config has been created — otherwise throws. Default
378
+ * `wireCrossing` is `tier >= 3`.
379
+ */
380
+ registerMessageType(t, input) {
381
+ this._assertUnfrozen();
382
+ this._messageTypes.set(t, {
383
+ tier: input.tier,
384
+ wireCrossing: input.wireCrossing ?? input.tier >= 3,
385
+ metaPassthrough: input.metaPassthrough ?? true
386
+ });
387
+ return this;
388
+ }
389
+ /** Tier for `t`. Unknown types default to tier 1 (immediate, after START). */
390
+ messageTier(t) {
391
+ const reg = this._messageTypes.get(t);
392
+ return reg != null ? reg.tier : 1;
393
+ }
394
+ /**
395
+ * Whether `t` is registered as wire-crossing. Unknown types default to
396
+ * `true` (spec §1.3.6 forward-compat — unknowns cross the wire).
397
+ */
398
+ isWireCrossing(t) {
399
+ const reg = this._messageTypes.get(t);
400
+ return reg != null ? reg.wireCrossing : true;
401
+ }
402
+ /** Convenience inverse of {@link isWireCrossing}. */
403
+ isLocalOnly(t) {
404
+ return !this.isWireCrossing(t);
405
+ }
406
+ /**
407
+ * Whether `t` is forwarded to meta companions by `Graph.signal`. Defaults
408
+ * to `true` for unknowns (forward-compat — new types pass through meta by
409
+ * default; opt-in filter via `registerMessageType({metaPassthrough: false})`).
410
+ */
411
+ isMetaPassthrough(t) {
412
+ const reg = this._messageTypes.get(t);
413
+ return reg != null ? reg.metaPassthrough : true;
414
+ }
415
+ /** Whether `t` is a registered (built-in or custom) type. */
416
+ isKnownMessageType(t) {
417
+ return this._messageTypes.has(t);
418
+ }
419
+ // --- Codec registry (writes require unfrozen; reads are free lookups) ---
420
+ /**
421
+ * Register a graph codec by `codec.name`. Used by the envelope-based
422
+ * `graph.snapshot({format: "bytes", codec: name})` path and
423
+ * `Graph.decode(bytes)` auto-dispatch. Must be called before any node
424
+ * bound to this config is created — otherwise throws.
425
+ *
426
+ * Re-registering the same name overwrites, so user codecs can shadow
427
+ * built-in ones before freeze (e.g., to swap a zstd-wrapped dag-cbor in
428
+ * for `"dag-cbor"`).
429
+ */
430
+ registerCodec(codec) {
431
+ this._assertUnfrozen();
432
+ this._codecs.set(codec.name, codec);
433
+ return this;
434
+ }
435
+ /**
436
+ * Resolve a registered codec by name. Returns `undefined` for unknown
437
+ * names. Typed callers cast to their concrete codec interface (e.g.,
438
+ * `config.lookupCodec<GraphCodec>("json")`) — this method stays
439
+ * layer-pure (no import of graph-layer types into `core/`).
440
+ */
441
+ lookupCodec(name) {
442
+ return this._codecs.get(name);
443
+ }
444
+ /** @internal Used by tests and dev tooling — check freeze state without triggering it. */
445
+ _isFrozen() {
446
+ return this._frozen;
447
+ }
448
+ _assertUnfrozen() {
449
+ if (this._frozen) {
450
+ throw new Error(
451
+ "GraphReFlyConfig is frozen: a node has already captured this config. Register custom types and set hooks before creating any node."
452
+ );
308
453
  }
309
454
  }
455
+ };
456
+ function registerBuiltins(cfg) {
457
+ cfg.registerMessageType(START, { tier: 0, wireCrossing: false });
458
+ cfg.registerMessageType(DIRTY, { tier: 1, wireCrossing: false });
459
+ cfg.registerMessageType(INVALIDATE, {
460
+ tier: 1,
461
+ wireCrossing: false,
462
+ metaPassthrough: false
463
+ });
464
+ cfg.registerMessageType(PAUSE, { tier: 2, wireCrossing: false });
465
+ cfg.registerMessageType(RESUME, { tier: 2, wireCrossing: false });
466
+ cfg.registerMessageType(DATA, { tier: 3, wireCrossing: true });
467
+ cfg.registerMessageType(RESOLVED, { tier: 3, wireCrossing: true });
468
+ cfg.registerMessageType(COMPLETE, {
469
+ tier: 4,
470
+ wireCrossing: true,
471
+ metaPassthrough: false
472
+ });
473
+ cfg.registerMessageType(ERROR, {
474
+ tier: 4,
475
+ wireCrossing: true,
476
+ metaPassthrough: false
477
+ });
478
+ cfg.registerMessageType(TEARDOWN, {
479
+ tier: 5,
480
+ wireCrossing: true,
481
+ metaPassthrough: false
482
+ });
310
483
  }
311
484
 
312
485
  // src/core/guard.ts
@@ -407,16 +580,25 @@ function accessHintForGuard(guard) {
407
580
  return allowed.join("+");
408
581
  }
409
582
 
410
- // src/core/clock.ts
411
- function monotonicNs() {
412
- return Math.trunc(performance.now() * 1e6);
413
- }
414
- function wallClockNs() {
415
- return Date.now() * 1e6;
583
+ // src/graph/codec.ts
584
+ var JsonCodec = {
585
+ name: "json",
586
+ version: 1,
587
+ contentType: "application/json",
588
+ encode(snapshot) {
589
+ const json = JSON.stringify(snapshot);
590
+ return new TextEncoder().encode(json);
591
+ },
592
+ decode(buffer, _codecVersion) {
593
+ const json = new TextDecoder().decode(buffer);
594
+ return JSON.parse(json);
595
+ }
596
+ };
597
+ function registerBuiltinCodecs(config) {
598
+ config.registerCodec(JsonCodec);
416
599
  }
417
600
 
418
601
  // src/core/versioning.ts
419
- var import_node_crypto = require("crypto");
420
602
  function canonicalizeForHash(value) {
421
603
  if (value === void 0) return null;
422
604
  if (typeof value === "number") {
@@ -445,13 +627,153 @@ function canonicalizeForHash(value) {
445
627
  }
446
628
  return null;
447
629
  }
630
+ var SHA256_K = /* @__PURE__ */ new Uint32Array([
631
+ 1116352408,
632
+ 1899447441,
633
+ 3049323471,
634
+ 3921009573,
635
+ 961987163,
636
+ 1508970993,
637
+ 2453635748,
638
+ 2870763221,
639
+ 3624381080,
640
+ 310598401,
641
+ 607225278,
642
+ 1426881987,
643
+ 1925078388,
644
+ 2162078206,
645
+ 2614888103,
646
+ 3248222580,
647
+ 3835390401,
648
+ 4022224774,
649
+ 264347078,
650
+ 604807628,
651
+ 770255983,
652
+ 1249150122,
653
+ 1555081692,
654
+ 1996064986,
655
+ 2554220882,
656
+ 2821834349,
657
+ 2952996808,
658
+ 3210313671,
659
+ 3336571891,
660
+ 3584528711,
661
+ 113926993,
662
+ 338241895,
663
+ 666307205,
664
+ 773529912,
665
+ 1294757372,
666
+ 1396182291,
667
+ 1695183700,
668
+ 1986661051,
669
+ 2177026350,
670
+ 2456956037,
671
+ 2730485921,
672
+ 2820302411,
673
+ 3259730800,
674
+ 3345764771,
675
+ 3516065817,
676
+ 3600352804,
677
+ 4094571909,
678
+ 275423344,
679
+ 430227734,
680
+ 506948616,
681
+ 659060556,
682
+ 883997877,
683
+ 958139571,
684
+ 1322822218,
685
+ 1537002063,
686
+ 1747873779,
687
+ 1955562222,
688
+ 2024104815,
689
+ 2227730452,
690
+ 2361852424,
691
+ 2428436474,
692
+ 2756734187,
693
+ 3204031479,
694
+ 3329325298
695
+ ]);
696
+ var UTF8_ENCODER = /* @__PURE__ */ new TextEncoder();
697
+ function sha256Hex(msg) {
698
+ const bytes = UTF8_ENCODER.encode(msg);
699
+ const msgLen = bytes.length;
700
+ const bitLen = msgLen * 8;
701
+ const totalLen = msgLen + 9 + 63 & ~63;
702
+ const padded = new Uint8Array(totalLen);
703
+ padded.set(bytes);
704
+ padded[msgLen] = 128;
705
+ const dv = new DataView(padded.buffer);
706
+ dv.setUint32(totalLen - 4, bitLen >>> 0, false);
707
+ dv.setUint32(totalLen - 8, Math.floor(bitLen / 4294967296) >>> 0, false);
708
+ let h0 = 1779033703;
709
+ let h1 = 3144134277;
710
+ let h2 = 1013904242;
711
+ let h3 = 2773480762;
712
+ let h4 = 1359893119;
713
+ let h5 = 2600822924;
714
+ let h6 = 528734635;
715
+ let h7 = 1541459225;
716
+ const W = new Uint32Array(64);
717
+ const rotr = (x, n) => x >>> n | x << 32 - n;
718
+ for (let off = 0; off < totalLen; off += 64) {
719
+ for (let i = 0; i < 16; i++) W[i] = dv.getUint32(off + i * 4, false);
720
+ for (let i = 16; i < 64; i++) {
721
+ const w15 = W[i - 15];
722
+ const w2 = W[i - 2];
723
+ const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ w15 >>> 3;
724
+ const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ w2 >>> 10;
725
+ W[i] = W[i - 16] + s0 + W[i - 7] + s1 >>> 0;
726
+ }
727
+ let a = h0;
728
+ let b = h1;
729
+ let c = h2;
730
+ let d = h3;
731
+ let e = h4;
732
+ let f = h5;
733
+ let g = h6;
734
+ let h = h7;
735
+ for (let i = 0; i < 64; i++) {
736
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
737
+ const ch = e & f ^ ~e & g;
738
+ const t1 = h + S1 + ch + SHA256_K[i] + W[i] >>> 0;
739
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
740
+ const mj = a & b ^ a & c ^ b & c;
741
+ const t2 = S0 + mj >>> 0;
742
+ h = g;
743
+ g = f;
744
+ f = e;
745
+ e = d + t1 >>> 0;
746
+ d = c;
747
+ c = b;
748
+ b = a;
749
+ a = t1 + t2 >>> 0;
750
+ }
751
+ h0 = h0 + a >>> 0;
752
+ h1 = h1 + b >>> 0;
753
+ h2 = h2 + c >>> 0;
754
+ h3 = h3 + d >>> 0;
755
+ h4 = h4 + e >>> 0;
756
+ h5 = h5 + f >>> 0;
757
+ h6 = h6 + g >>> 0;
758
+ h7 = h7 + h >>> 0;
759
+ }
760
+ const toHex = (x) => x.toString(16).padStart(8, "0");
761
+ return toHex(h0) + toHex(h1) + toHex(h2) + toHex(h3) + toHex(h4) + toHex(h5) + toHex(h6) + toHex(h7);
762
+ }
448
763
  function defaultHash(value) {
449
764
  const canonical = canonicalizeForHash(value ?? null);
450
765
  const json = JSON.stringify(canonical);
451
- return (0, import_node_crypto.createHash)("sha256").update(json).digest("hex").slice(0, 16);
766
+ return sha256Hex(json).slice(0, 16);
767
+ }
768
+ function randomUuid() {
769
+ const c = globalThis.crypto;
770
+ if (c?.randomUUID) return c.randomUUID();
771
+ const r = () => Math.floor(Math.random() * 4294967296).toString(16).padStart(8, "0");
772
+ const hex = r() + r() + r() + r();
773
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-${(parseInt(hex.slice(16, 17), 16) & 3 | 8).toString(16)}${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
452
774
  }
453
775
  function createVersioning(level, initialValue, opts) {
454
- const id = opts?.id ?? (0, import_node_crypto.randomUUID)();
776
+ const id = opts?.id ?? randomUuid();
455
777
  if (level === 0) {
456
778
  return { id, version: 0 };
457
779
  }
@@ -470,288 +792,418 @@ function isV1(info) {
470
792
  return "cid" in info;
471
793
  }
472
794
 
473
- // src/core/node-base.ts
474
- var NO_VALUE = /* @__PURE__ */ Symbol.for("graphrefly/NO_VALUE");
475
- var CLEANUP_RESULT = /* @__PURE__ */ Symbol.for("graphrefly/CLEANUP_RESULT");
476
- function cleanupResult(cleanup, ...args) {
477
- const r = { [CLEANUP_RESULT]: true, cleanup };
478
- if (args.length > 0) r.value = args[0];
479
- return r;
480
- }
481
- var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
482
- var isCleanupFn = (value) => typeof value === "function";
483
- function statusAfterMessage(status, msg) {
484
- const t = msg[0];
485
- if (t === DIRTY) return "dirty";
486
- if (t === DATA) return "settled";
487
- if (t === RESOLVED) return "resolved";
488
- if (t === COMPLETE) return "completed";
489
- if (t === ERROR) return "errored";
490
- if (t === INVALIDATE) return "dirty";
491
- if (t === TEARDOWN) return "disconnected";
492
- return status;
493
- }
494
- function createIntBitSet(size) {
495
- const fullMask = size >= 32 ? -1 : ~(-1 << size);
496
- let bits = 0;
795
+ // src/core/node.ts
796
+ var noopUnsub = () => {
797
+ };
798
+ var MAX_RERUN_DEPTH = 100;
799
+ function createDepRecord(n) {
497
800
  return {
498
- set(i) {
499
- bits |= 1 << i;
500
- },
501
- clear(i) {
502
- bits &= ~(1 << i);
503
- },
504
- has(i) {
505
- return (bits & 1 << i) !== 0;
506
- },
507
- covers(other) {
508
- const otherBits = other._bits();
509
- return (bits & otherBits) === otherBits;
510
- },
511
- any() {
512
- return bits !== 0;
513
- },
514
- reset() {
515
- bits = 0;
516
- },
517
- setAll() {
518
- bits = fullMask;
519
- },
520
- _bits() {
521
- return bits;
522
- }
801
+ node: n,
802
+ unsub: null,
803
+ prevData: void 0,
804
+ dirty: false,
805
+ involvedThisWave: false,
806
+ dataBatch: [],
807
+ terminal: void 0
523
808
  };
524
809
  }
525
- function createArrayBitSet(size) {
526
- const words = new Uint32Array(Math.ceil(size / 32));
527
- const lastBits = size % 32;
528
- const lastWordMask = lastBits === 0 ? 4294967295 : (1 << lastBits) - 1 >>> 0;
529
- return {
530
- set(i) {
531
- words[i >>> 5] |= 1 << (i & 31);
532
- },
533
- clear(i) {
534
- words[i >>> 5] &= ~(1 << (i & 31));
535
- },
536
- has(i) {
537
- return (words[i >>> 5] & 1 << (i & 31)) !== 0;
538
- },
539
- covers(other) {
540
- const ow = other._words;
541
- for (let w = 0; w < words.length; w++) {
542
- if ((words[w] & ow[w]) >>> 0 !== ow[w]) return false;
543
- }
544
- return true;
545
- },
546
- any() {
547
- for (let w = 0; w < words.length; w++) {
548
- if (words[w] !== 0) return true;
549
- }
550
- return false;
551
- },
552
- reset() {
553
- words.fill(0);
554
- },
555
- setAll() {
556
- for (let w = 0; w < words.length - 1; w++) words[w] = 4294967295;
557
- if (words.length > 0) words[words.length - 1] = lastWordMask;
558
- },
559
- _words: words
560
- };
810
+ function resetDepRecord(d) {
811
+ d.prevData = void 0;
812
+ d.dirty = false;
813
+ d.involvedThisWave = false;
814
+ d.dataBatch.length = 0;
815
+ d.terminal = void 0;
816
+ }
817
+ function normalizeMessages(input) {
818
+ if (input.length === 0) return input;
819
+ return typeof input[0] === "symbol" ? [input] : input;
561
820
  }
562
- function createBitSet(size) {
563
- return size <= 31 ? createIntBitSet(size) : createArrayBitSet(size);
821
+ var defaultOnMessage = (node2, msg, ctx, _actions) => {
822
+ if (ctx.direction === "down-in") {
823
+ node2._onDepMessage(ctx.depIndex, msg);
824
+ }
825
+ return void 0;
826
+ };
827
+ var defaultOnSubscribe = (node2, sink, _ctx, _actions) => {
828
+ const impl = node2;
829
+ if (impl._status === "completed" || impl._status === "errored") return;
830
+ const cached = impl._cached;
831
+ const initial = cached === void 0 ? [START_MSG] : [START_MSG, [DATA, cached]];
832
+ if (impl._status === "dirty") initial.push(DIRTY_MSG);
833
+ downWithBatch(sink, initial, impl._config.tierOf);
834
+ };
835
+ var defaultConfig = new GraphReFlyConfig({
836
+ onMessage: defaultOnMessage,
837
+ onSubscribe: defaultOnSubscribe
838
+ });
839
+ registerBuiltins(defaultConfig);
840
+ registerBuiltinCodecs(defaultConfig);
841
+ function configure(fn) {
842
+ if (defaultConfig._isFrozen()) {
843
+ throw new Error(
844
+ "configure() called after a node was created \u2014 the default GraphReFlyConfig is frozen. Call configure(...) at application startup, before any node factories run."
845
+ );
846
+ }
847
+ fn(defaultConfig);
564
848
  }
565
- var NodeBase = class {
566
- // --- Identity (set once) ---
849
+ var NodeImpl = class _NodeImpl {
850
+ // --- Identity ---
567
851
  _optsName;
568
- _registryName;
569
- /** @internal Read by `describeNode` before inference. */
570
852
  _describeKind;
571
853
  meta;
572
- // --- Options ---
854
+ /**
855
+ * Cached `Object.keys(meta).length > 0` check. `meta` is frozen at
856
+ * construction so this boolean never flips. Used by `_emit` to skip
857
+ * the meta TEARDOWN fan-out block allocation on the common "no meta"
858
+ * hot path.
859
+ */
860
+ _hasMeta;
861
+ // --- Config ---
862
+ _config;
863
+ // --- Topology ---
864
+ /** Mutable for autoTrackNode / Graph.connect() post-construction dep addition. */
865
+ _deps;
866
+ _sinks = null;
867
+ _sinkCount = 0;
868
+ // --- State ---
869
+ _cached;
870
+ _status;
871
+ _cleanup;
872
+ _store = {};
873
+ _waveHasNewData = false;
874
+ _hasNewTerminal = false;
875
+ _hasCalledFnOnce = false;
876
+ _paused = false;
877
+ _pendingWave = false;
878
+ _isExecutingFn = false;
879
+ _pendingRerun = false;
880
+ _rerunDepth = 0;
881
+ // --- Settlement counter (A3) ---
882
+ /**
883
+ * Count of deps currently in `dirty === true`. `_maybeRunFnOnSettlement`
884
+ * treats `0` as "wave settled" — O(1) check for full dep settlement.
885
+ */
886
+ _dirtyDepCount = 0;
887
+ // --- Per-batch emit accumulator (Bug 2: K+1 fan-in fix) ---
888
+ /**
889
+ * Inside an explicit `batch(() => ...)` scope, every `_emit` accumulates
890
+ * its already-framed messages here instead of dispatching synchronously.
891
+ * At batch end, `_flushBatchPending` runs (registered via
892
+ * `registerBatchFlushHook`) and delivers the whole accumulated batch as
893
+ * one `downWithBatch` call — collapsing what would otherwise be K
894
+ * separate sink invocations into one. This is the fix for the diamond
895
+ * fan-in K+1 over-fire.
896
+ *
897
+ * `null` outside batch (or after flush). Only ever appended to within
898
+ * a single explicit batch lifetime; reset to `null` on flush. State
899
+ * updates (cache, version, status) still happen per-emit via
900
+ * `_updateState` — only the downstream delivery is coalesced.
901
+ */
902
+ _batchPendingMessages = null;
903
+ // --- PAUSE/RESUME lock tracking (C0) ---
904
+ /**
905
+ * Set of active pause locks held against this node. Every `[PAUSE, lockId]`
906
+ * adds its `lockId` to the set; every `[RESUME, lockId]` removes it.
907
+ * `_paused` is a derived quantity: `_pauseLocks.size > 0`. Multi-pauser
908
+ * correctness — one controller releasing its lock does NOT resume the
909
+ * node while another controller still holds its lock.
910
+ */
911
+ _pauseLocks = null;
912
+ /**
913
+ * Buffered DATA messages held while paused. Only populated when
914
+ * `_pausable === "resumeAll"` (bufferAll mode). On final lock release
915
+ * the buffer is replayed through the node's outgoing pipeline in the
916
+ * order received. Non-bufferAll pause mode drops DATA on the floor
917
+ * (upstream is expected to honor PAUSE by suppressing production).
918
+ */
919
+ _pauseBuffer = null;
920
+ // --- Options (frozen at construction) ---
921
+ _fn;
573
922
  _equals;
574
923
  _resubscribable;
575
924
  _resetOnTeardown;
576
- _onResubscribe;
577
- _onMessage;
578
- /** @internal Read by `describeNode` for `accessHintForGuard`. */
925
+ _autoComplete;
926
+ _autoError;
927
+ _pausable;
579
928
  _guard;
580
- /** @internal Subclasses update this through {@link _recordMutation}. */
581
- _lastMutation;
582
- // --- Versioning ---
583
929
  _hashFn;
584
930
  _versioning;
585
- // --- Lifecycle state ---
586
- /** @internal Read by `describeNode` and `graph.ts`. */
587
- _cached;
588
- /** @internal Read externally via `get status()`. */
589
- _status;
590
- _terminal = false;
591
- _active = false;
592
- // --- Sink storage ---
593
- /** @internal Read by `graph/profile.ts` for subscriber counts. */
594
- _sinkCount = 0;
595
- _singleDepSinkCount = 0;
596
- _singleDepSinks = /* @__PURE__ */ new WeakSet();
597
- _sinks = null;
598
- // --- Actions + bound helpers ---
931
+ /**
932
+ * Explicit versioning level, tracked separately from `_versioning` so
933
+ * monotonicity checks and future v2/v3 extensions don't rely on the
934
+ * fragile `"cid" in _versioning` shape discriminator. `undefined` means
935
+ * the node has no versioning attached; `0` / `1` / future levels name
936
+ * the tier. Mutated in lockstep with `_versioning` by the constructor
937
+ * and by `_applyVersioning`.
938
+ */
939
+ _versioningLevel;
940
+ // --- ABAC ---
941
+ _lastMutation;
942
+ /**
943
+ * @internal Per-node inspector hooks for `Graph.observe(path,
944
+ * { causal, derived })`. Fires in `_onDepMessage` and `_execFn`.
945
+ * Attached via `_setInspectorHook` (returns a disposer). Multiple
946
+ * observers can attach simultaneously — all registered hooks fire for
947
+ * every event.
948
+ */
949
+ _inspectorHooks;
950
+ // --- Actions (built once in the constructor) ---
599
951
  _actions;
600
- _boundDownToSinks;
601
- // --- Inspector hook (Graph observability) ---
602
- _inspectorHook;
603
- constructor(opts) {
952
+ constructor(deps, fn, opts) {
953
+ this._config = opts.config ?? defaultConfig;
954
+ void this._config.onMessage;
604
955
  this._optsName = opts.name;
605
956
  this._describeKind = opts.describeKind;
606
957
  this._equals = opts.equals ?? Object.is;
607
958
  this._resubscribable = opts.resubscribable ?? false;
608
959
  this._resetOnTeardown = opts.resetOnTeardown ?? false;
609
- this._onResubscribe = opts.onResubscribe;
610
- this._onMessage = opts.onMessage;
960
+ this._autoComplete = opts.completeWhenDepsComplete ?? true;
961
+ this._autoError = opts.errorWhenDepsError ?? true;
962
+ this._pausable = opts.pausable ?? true;
611
963
  this._guard = opts.guard;
612
- this._cached = "initial" in opts ? opts.initial : NO_VALUE;
613
- this._status = "disconnected";
614
- this._hashFn = opts.versioningHash ?? defaultHash;
615
- this._versioning = opts.versioning != null ? createVersioning(opts.versioning, this._cached === NO_VALUE ? void 0 : this._cached, {
964
+ this._fn = fn;
965
+ this._cached = opts.initial !== void 0 ? opts.initial : void 0;
966
+ this._status = deps.length === 0 && fn == null && this._cached !== void 0 ? "settled" : "sentinel";
967
+ this._hashFn = opts.versioningHash ?? this._config.defaultHashFn ?? defaultHash;
968
+ const versioningLevel = opts.versioning ?? this._config.defaultVersioning;
969
+ this._versioningLevel = versioningLevel;
970
+ this._versioning = versioningLevel != null ? createVersioning(versioningLevel, this._cached === void 0 ? void 0 : this._cached, {
616
971
  id: opts.versioningId,
617
972
  hash: this._hashFn
618
973
  }) : void 0;
974
+ this._deps = deps.map(createDepRecord);
619
975
  const meta = {};
620
976
  for (const [k, v] of Object.entries(opts.meta ?? {})) {
621
- meta[k] = this._createMetaNode(k, v, opts);
977
+ const metaOpts = {
978
+ initial: v,
979
+ name: `${opts.name ?? "node"}:meta:${k}`,
980
+ describeKind: "state",
981
+ config: this._config
982
+ };
983
+ if (opts.guard != null) metaOpts.guard = opts.guard;
984
+ meta[k] = new _NodeImpl([], void 0, metaOpts);
622
985
  }
623
986
  Object.freeze(meta);
624
987
  this.meta = meta;
988
+ this._hasMeta = Object.keys(meta).length > 0;
625
989
  const self = this;
626
990
  this._actions = {
627
- down(messages) {
628
- self._onManualEmit();
629
- self._downInternal(messages);
630
- },
631
991
  emit(value) {
632
- self._onManualEmit();
633
- self._downAutoValue(value);
992
+ self._emit([[DATA, value]]);
634
993
  },
635
- up(messages) {
636
- self._upInternal(messages);
994
+ down(messageOrMessages) {
995
+ self._emit(normalizeMessages(messageOrMessages));
996
+ },
997
+ up(messageOrMessages) {
998
+ self._emitUp(normalizeMessages(messageOrMessages));
637
999
  }
638
1000
  };
639
- this._boundDownToSinks = this._downToSinks.bind(this);
1001
+ this.down = this.down.bind(this);
1002
+ this.up = this.up.bind(this);
640
1003
  }
641
- /**
642
- * Subclass hook invoked by `actions.down` / `actions.emit`. Default no-op;
643
- * {@link NodeImpl} overrides to set `_manualEmitUsed`.
644
- */
645
- _onManualEmit() {
1004
+ // --- Derived state ---
1005
+ get _isTerminal() {
1006
+ return this._status === "completed" || this._status === "errored";
646
1007
  }
647
- // --- Identity getters ---
1008
+ // --- Public getters ---
648
1009
  get name() {
649
- return this._registryName ?? this._optsName;
650
- }
651
- /** @internal Assigned by `Graph.add` when registered without an options `name`. */
652
- _assignRegistryName(localName) {
653
- if (this._optsName !== void 0 || this._registryName !== void 0) return;
654
- this._registryName = localName;
655
- }
656
- /**
657
- * @internal Attach/remove inspector hook for graph-level observability.
658
- * Returns a disposer that restores the previous hook.
659
- */
660
- _setInspectorHook(hook) {
661
- const prev = this._inspectorHook;
662
- this._inspectorHook = hook;
663
- return () => {
664
- if (this._inspectorHook === hook) {
665
- this._inspectorHook = prev;
666
- }
667
- };
668
- }
669
- /** @internal Used by subclasses to surface inspector events. */
670
- _emitInspectorHook(event) {
671
- this._inspectorHook?.(event);
1010
+ return this._optsName;
672
1011
  }
673
1012
  get status() {
674
1013
  return this._status;
675
1014
  }
1015
+ get cache() {
1016
+ return this._cached === void 0 ? void 0 : this._cached;
1017
+ }
676
1018
  get lastMutation() {
677
1019
  return this._lastMutation;
678
1020
  }
679
1021
  get v() {
680
1022
  return this._versioning;
681
1023
  }
682
- /** @internal Used by `Graph.setVersioning` to retroactively apply versioning. */
683
- _applyVersioning(level, opts) {
684
- if (this._versioning != null) return;
685
- this._hashFn = opts?.hash ?? this._hashFn;
686
- this._versioning = createVersioning(
687
- level,
688
- this._cached === NO_VALUE ? void 0 : this._cached,
689
- {
690
- id: opts?.id,
691
- hash: this._hashFn
692
- }
693
- );
694
- }
695
1024
  hasGuard() {
696
1025
  return this._guard != null;
697
1026
  }
698
- allowsObserve(actor) {
699
- if (this._guard == null) return true;
700
- return this._guard(normalizeActor(actor), "observe");
701
- }
702
- // --- Public transport ---
703
- get() {
704
- return this._cached === NO_VALUE ? void 0 : this._cached;
705
- }
706
- down(messages, options) {
707
- if (messages.length === 0) return;
708
- if (!options?.internal && this._guard != null) {
709
- const actor = normalizeActor(options?.actor);
710
- const delivery = options?.delivery ?? "write";
711
- const action = delivery === "signal" ? "signal" : "write";
712
- if (!this._guard(actor, action)) {
713
- throw new GuardDenied({ actor, action, nodeName: this.name });
714
- }
715
- this._recordMutation(actor);
716
- }
717
- this._downInternal(messages);
718
- }
719
- /** @internal Record a successful guarded mutation (called by `down` and subclass `up`). */
720
- _recordMutation(actor) {
721
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
722
- }
723
1027
  /**
724
- * At-most-once deactivation guard. Both TEARDOWN (eager) and
725
- * unsubscribe-body (lazy) call this. The `_active` flag ensures
726
- * `_doDeactivate` runs exactly once per activation cycle.
1028
+ * @internal Retroactively attach (or upgrade) versioning state on this
1029
+ * node. Intended for `Graph.setVersioning(level)` bulk application and
1030
+ * for rare cases where a specific node needs to be bumped to a higher
1031
+ * level (e.g., `v0 → v1`) after construction.
1032
+ *
1033
+ * **Safety:** the mutation is rejected mid-wave. Specifically,
1034
+ * throws if the node is currently executing its fn (`_isExecutingFn`).
1035
+ * Callers at quiescent points — before the first sink subscribes, or
1036
+ * after all sinks unsubscribe, or between external `down()` / `emit()`
1037
+ * invocations — are safe. The re-entrance window that motivated §10.6.4
1038
+ * removal was the "transition `_versioning` from `undefined` to a fresh
1039
+ * object mid-`_updateState`" case; that path is now guarded.
1040
+ *
1041
+ * **Monotonicity:** levels can only go up. Downgrade (e.g., `v1 → v0`)
1042
+ * is a no-op — once a node carries higher-level metadata, dropping it
1043
+ * mid-graph would tear the linked-history invariant for v1 and above.
1044
+ *
1045
+ * **Linked-history boundary (D1, 2026-04-13):** upgrading v0 → v1
1046
+ * produces a **fresh history root**. The new v1 state has `cid =
1047
+ * hash(currentCachedValue)` and `prev = null`, not a synthetic `prev`
1048
+ * anchored to any previous v0 value. The v0 monotonic `version` counter
1049
+ * is preserved across the upgrade, but the linked-cid chain (spec §7)
1050
+ * starts fresh at the upgrade point. Downstream audit tools that walk
1051
+ * `v.cid.prev` backwards through time will see a `null` boundary at
1052
+ * the upgrade — **this is intentional**: v0 had no cid to link to, and
1053
+ * fabricating one would lie about the hash. Callers that require an
1054
+ * unbroken cid chain from birth must attach versioning at construction
1055
+ * via `opts.versioning` or `config.defaultVersioning`, not retroactively.
1056
+ *
1057
+ * @param level - New minimum versioning level.
1058
+ * @param opts - Optional id / hash overrides; applied only if the
1059
+ * node currently has no versioning state.
727
1060
  */
728
- _onDeactivate() {
729
- if (!this._active) return;
730
- this._active = false;
731
- this._doDeactivate();
732
- }
733
- // --- Subscribe (uniform across node shapes) ---
734
- subscribe(sink, hints) {
735
- if (hints?.actor != null && this._guard != null) {
736
- const actor = normalizeActor(hints.actor);
737
- if (!this._guard(actor, "observe")) {
738
- throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
739
- }
1061
+ _applyVersioning(level, opts) {
1062
+ if (this._isExecutingFn) {
1063
+ throw new Error(
1064
+ `Node "${this.name}": _applyVersioning cannot run mid-fn \u2014 call it outside of \`_execFn\` (typically at graph setup time before the first subscribe).`
1065
+ );
740
1066
  }
741
- if (this._terminal && this._resubscribable) {
742
- this._terminal = false;
743
- this._cached = NO_VALUE;
744
- this._status = "disconnected";
745
- this._onResubscribe?.();
1067
+ const currentLevel = this._versioningLevel;
1068
+ if (currentLevel != null && level <= currentLevel) {
1069
+ return;
746
1070
  }
747
- this._sinkCount += 1;
748
- if (hints?.singleDep) {
749
- this._singleDepSinkCount += 1;
750
- this._singleDepSinks.add(sink);
1071
+ const hash = opts?.hash ?? this._hashFn;
1072
+ if (hash !== this._hashFn) this._hashFn = hash;
1073
+ const initialValue = this._cached === void 0 ? void 0 : this._cached;
1074
+ const current = this._versioning;
1075
+ const preservedId = current?.id ?? opts?.id;
1076
+ const preservedVersion = current?.version ?? 0;
1077
+ const fresh = createVersioning(level, initialValue, {
1078
+ id: preservedId,
1079
+ hash
1080
+ });
1081
+ fresh.version = preservedVersion;
1082
+ this._versioning = fresh;
1083
+ this._versioningLevel = level;
1084
+ }
1085
+ /**
1086
+ * @internal Attach an inspector hook. Returns a disposer that removes
1087
+ * the hook. Used by `Graph.observe(path, { causal, derived })` to build
1088
+ * causal traces. Multiple hooks may be attached concurrently — all fire
1089
+ * for every event in registration order. Passing `undefined` is a no-op
1090
+ * and returns a no-op disposer.
1091
+ */
1092
+ _setInspectorHook(hook) {
1093
+ if (hook == null) return () => {
1094
+ };
1095
+ if (this._inspectorHooks == null) this._inspectorHooks = /* @__PURE__ */ new Set();
1096
+ this._inspectorHooks.add(hook);
1097
+ return () => {
1098
+ this._inspectorHooks?.delete(hook);
1099
+ if (this._inspectorHooks?.size === 0) this._inspectorHooks = void 0;
1100
+ };
1101
+ }
1102
+ allowsObserve(actor) {
1103
+ if (this._guard == null) return true;
1104
+ return this._guard(normalizeActor(actor), "observe");
1105
+ }
1106
+ // --- Guard helper ---
1107
+ _checkGuard(options) {
1108
+ if (options?.internal || this._guard == null) return;
1109
+ const actor = normalizeActor(options?.actor);
1110
+ const action = options?.delivery === "signal" ? "signal" : "write";
1111
+ if (!this._guard(actor, action)) {
1112
+ throw new GuardDenied({ actor, action, nodeName: this.name });
1113
+ }
1114
+ this._lastMutation = { actor, timestamp_ns: wallClockNs() };
1115
+ }
1116
+ // --- Public transport ---
1117
+ down(messageOrMessages, options) {
1118
+ const messages = normalizeMessages(messageOrMessages);
1119
+ if (messages.length === 0) return;
1120
+ this._checkGuard(options);
1121
+ this._emit(messages);
1122
+ }
1123
+ emit(value, options) {
1124
+ this._checkGuard(options);
1125
+ this._emit([[DATA, value]]);
1126
+ }
1127
+ up(messageOrMessages, options) {
1128
+ if (this._deps.length === 0) return;
1129
+ const messages = normalizeMessages(messageOrMessages);
1130
+ if (messages.length === 0) return;
1131
+ this._checkGuard(options);
1132
+ const forwardOpts = options ?? { internal: true };
1133
+ this._validateUpTiers(messages);
1134
+ for (const d of this._deps) {
1135
+ d.node.up?.(messages, forwardOpts);
1136
+ }
1137
+ }
1138
+ /**
1139
+ * @internal Internal up-path used by `actions.up(...)` from inside fn.
1140
+ * Same tier validation as public `up`, but bypasses the guard check
1141
+ * since the fn context is already inside an authorized operation.
1142
+ */
1143
+ _emitUp(messages) {
1144
+ if (this._deps.length === 0) return;
1145
+ if (messages.length === 0) return;
1146
+ this._validateUpTiers(messages);
1147
+ for (const d of this._deps) {
1148
+ d.node.up?.(messages, { internal: true });
751
1149
  }
752
- if (!this._terminal) {
753
- const startMessages = this._cached === NO_VALUE ? [[START]] : [[START], [DATA, this._cached]];
754
- downWithBatch(sink, startMessages);
1150
+ }
1151
+ /**
1152
+ * @internal Enforce spec §1.2 — up-direction messages are restricted to
1153
+ * tier 0–2 and tier 5 (START, DIRTY, INVALIDATE, PAUSE, RESUME,
1154
+ * TEARDOWN). Tier 3 (DATA/RESOLVED) and tier 4 (COMPLETE/ERROR) are
1155
+ * downstream-only. Emitting tier-3/4 via `up` would bypass equals
1156
+ * substitution and cache advance entirely and is a protocol bug.
1157
+ */
1158
+ _validateUpTiers(messages) {
1159
+ const tierOf = this._config.tierOf;
1160
+ for (const m of messages) {
1161
+ const tier = tierOf(m[0]);
1162
+ if (tier === 3 || tier === 4) {
1163
+ throw new Error(
1164
+ `Node "${this.name}": tier-${tier} messages cannot flow up \u2014 DATA/RESOLVED/COMPLETE/ERROR are downstream-only. Use \`down(...)\` for value delivery; \`up(...)\` is for control signals (DIRTY, INVALIDATE, PAUSE, RESUME, TEARDOWN).`
1165
+ );
1166
+ }
1167
+ }
1168
+ }
1169
+ subscribe(sink, actor) {
1170
+ if (actor != null && this._guard != null) {
1171
+ const a = normalizeActor(actor);
1172
+ if (!this._guard(a, "observe")) {
1173
+ throw new GuardDenied({ actor: a, action: "observe", nodeName: this.name });
1174
+ }
1175
+ }
1176
+ const wasTerminal = this._isTerminal;
1177
+ const afterTerminalReset = wasTerminal && this._resubscribable;
1178
+ if (afterTerminalReset) {
1179
+ this._cached = void 0;
1180
+ this._status = "sentinel";
1181
+ this._store = {};
1182
+ this._hasCalledFnOnce = false;
1183
+ this._waveHasNewData = false;
1184
+ this._hasNewTerminal = false;
1185
+ this._paused = false;
1186
+ this._pendingWave = false;
1187
+ this._pendingRerun = false;
1188
+ this._isExecutingFn = false;
1189
+ this._rerunDepth = 0;
1190
+ this._dirtyDepCount = 0;
1191
+ this._pauseLocks = null;
1192
+ this._pauseBuffer = null;
1193
+ for (const d of this._deps) resetDepRecord(d);
1194
+ }
1195
+ this._sinkCount += 1;
1196
+ let subCleanup;
1197
+ try {
1198
+ subCleanup = this._config.onSubscribe(
1199
+ this,
1200
+ sink,
1201
+ { sinkCount: this._sinkCount, afterTerminalReset },
1202
+ this._actions
1203
+ );
1204
+ } catch (err) {
1205
+ this._sinkCount -= 1;
1206
+ throw err;
755
1207
  }
756
1208
  if (this._sinks == null) {
757
1209
  this._sinks = sink;
@@ -760,11 +1212,24 @@ var NodeBase = class {
760
1212
  } else {
761
1213
  this._sinks.add(sink);
762
1214
  }
763
- if (this._sinkCount === 1 && !this._terminal) {
764
- this._active = true;
765
- this._onActivate();
1215
+ const isTerminalNow = this._isTerminal;
1216
+ if (this._sinkCount === 1 && !isTerminalNow) {
1217
+ try {
1218
+ this._activate();
1219
+ } catch (err) {
1220
+ this._sinkCount -= 1;
1221
+ this._removeSink(sink);
1222
+ if (this._sinkCount === 0) this._status = "sentinel";
1223
+ if (typeof subCleanup === "function") {
1224
+ try {
1225
+ subCleanup();
1226
+ } catch {
1227
+ }
1228
+ }
1229
+ throw err;
1230
+ }
766
1231
  }
767
- if (!this._terminal && this._status === "disconnected" && this._cached === NO_VALUE) {
1232
+ if (this._status === "sentinel" && this._cached === void 0) {
768
1233
  this._status = "pending";
769
1234
  }
770
1235
  let removed = false;
@@ -772,853 +1237,802 @@ var NodeBase = class {
772
1237
  if (removed) return;
773
1238
  removed = true;
774
1239
  this._sinkCount -= 1;
775
- if (this._singleDepSinks.has(sink)) {
776
- this._singleDepSinkCount -= 1;
777
- this._singleDepSinks.delete(sink);
778
- }
779
- if (this._sinks == null) return;
780
- if (typeof this._sinks === "function") {
781
- if (this._sinks === sink) this._sinks = null;
782
- } else {
783
- this._sinks.delete(sink);
784
- if (this._sinks.size === 1) {
785
- const [only] = this._sinks;
786
- this._sinks = only;
787
- } else if (this._sinks.size === 0) {
788
- this._sinks = null;
789
- }
790
- }
791
- if (this._sinks == null) {
792
- this._onDeactivate();
793
- }
1240
+ this._removeSink(sink);
1241
+ if (typeof subCleanup === "function") subCleanup();
1242
+ if (this._sinks == null) this._deactivate();
794
1243
  };
795
1244
  }
796
- // --- Down pipeline ---
1245
+ _removeSink(sink) {
1246
+ if (this._sinks === sink) {
1247
+ this._sinks = null;
1248
+ } else if (this._sinks != null && typeof this._sinks !== "function") {
1249
+ this._sinks.delete(sink);
1250
+ if (this._sinks.size === 1) {
1251
+ const [only] = this._sinks;
1252
+ this._sinks = only;
1253
+ } else if (this._sinks.size === 0) {
1254
+ this._sinks = null;
1255
+ }
1256
+ }
1257
+ }
1258
+ // --- Lifecycle ---
797
1259
  /**
798
- * Core outgoing dispatch. Applies terminal filter + local lifecycle
799
- * update, then hands messages to `downWithBatch` for tier-aware delivery.
1260
+ * @internal First-sink activation. For a producer (no deps + fn),
1261
+ * invokes fn once. For a compute node (has deps), subscribes to every
1262
+ * dep with the pre-set-dirty trick so the first-run gate waits for
1263
+ * every dep to settle at least once.
800
1264
  */
801
- _downInternal(messages) {
802
- if (messages.length === 0) return;
803
- let sinkMessages = messages;
804
- if (this._terminal && !this._resubscribable) {
805
- const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
806
- if (pass.length === 0) return;
807
- sinkMessages = pass;
808
- }
809
- this._handleLocalLifecycle(sinkMessages);
810
- if (this._canSkipDirty()) {
811
- let hasPhase2 = false;
812
- for (let i = 0; i < sinkMessages.length; i++) {
813
- const t = sinkMessages[i][0];
814
- if (t === DATA || t === RESOLVED) {
815
- hasPhase2 = true;
816
- break;
817
- }
1265
+ _activate() {
1266
+ if (this._deps.length === 0) {
1267
+ if (this._fn) this._execFn();
1268
+ return;
1269
+ }
1270
+ this._dirtyDepCount = 0;
1271
+ const initialLen = this._deps.length;
1272
+ let subscribedCount = 0;
1273
+ try {
1274
+ for (let i = 0; i < initialLen; i++) {
1275
+ const depIdx = i;
1276
+ const dep = this._deps[i];
1277
+ dep.unsub = noopUnsub;
1278
+ dep.unsub = dep.node.subscribe((msgs) => {
1279
+ if (dep.unsub === null) return;
1280
+ const tierOf = this._config.tierOf;
1281
+ let sawSettlement = false;
1282
+ for (const m of msgs) {
1283
+ if (tierOf(m[0]) >= 3) sawSettlement = true;
1284
+ this._config.onMessage(
1285
+ this,
1286
+ m,
1287
+ { direction: "down-in", depIndex: depIdx },
1288
+ this._actions
1289
+ );
1290
+ }
1291
+ if (sawSettlement) this._maybeRunFnOnSettlement();
1292
+ });
1293
+ subscribedCount++;
818
1294
  }
819
- if (hasPhase2) {
820
- const filtered = [];
821
- for (let i = 0; i < sinkMessages.length; i++) {
822
- if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
823
- }
824
- if (filtered.length > 0) {
825
- downWithBatch(this._boundDownToSinks, filtered);
1295
+ } catch (err) {
1296
+ this._deps[subscribedCount].unsub = null;
1297
+ for (let j = 0; j < subscribedCount; j++) {
1298
+ const d = this._deps[j];
1299
+ if (d.unsub != null) {
1300
+ const u = d.unsub;
1301
+ d.unsub = null;
1302
+ try {
1303
+ u();
1304
+ } catch {
1305
+ }
1306
+ resetDepRecord(d);
826
1307
  }
827
- return;
828
1308
  }
1309
+ this._dirtyDepCount = 0;
1310
+ throw err;
829
1311
  }
830
- downWithBatch(this._boundDownToSinks, sinkMessages);
831
- }
832
- _canSkipDirty() {
833
- return this._sinkCount === 1 && this._singleDepSinkCount === 1;
834
1312
  }
835
- _downToSinks(messages) {
836
- if (this._sinks == null) return;
837
- if (typeof this._sinks === "function") {
838
- this._sinks(messages);
839
- return;
840
- }
841
- const snapshot = [...this._sinks];
842
- for (const sink of snapshot) {
843
- sink(messages);
1313
+ /**
1314
+ * @internal Append a dep post-construction. Used by `autoTrackNode`
1315
+ * (runtime dep discovery) and `Graph.connect()` (post-construction
1316
+ * wiring). Subscribes immediately — if DATA arrives synchronously
1317
+ * during subscribe and fn is currently executing, the re-run is
1318
+ * deferred via `_pendingRerun` flag (see `_execFn` guard).
1319
+ *
1320
+ * **Dedup:** idempotent on duplicate `depNode` — if `depNode` is
1321
+ * already in `_deps`, returns the existing index without mutating
1322
+ * state. Callers can safely invoke `_addDep` without their own
1323
+ * "already added" check. `autoTrackNode` still keeps a `depIndexMap`
1324
+ * as a fast-path lookup for known deps (returning cached `data[idx]`
1325
+ * without calling `_addDep` at all); this internal dedup is the
1326
+ * backstop for any caller that doesn't track its own dep set.
1327
+ *
1328
+ * @returns The index of the new dep in `_deps`, or the existing index
1329
+ * if the dep was already present.
1330
+ */
1331
+ _addDep(depNode) {
1332
+ for (let i = 0; i < this._deps.length; i++) {
1333
+ if (this._deps[i].node === depNode) return i;
1334
+ }
1335
+ const depIdx = this._deps.length;
1336
+ const record = createDepRecord(depNode);
1337
+ this._deps.push(record);
1338
+ if (this._sinks == null) return depIdx;
1339
+ record.dirty = true;
1340
+ this._dirtyDepCount++;
1341
+ if (this._status !== "dirty") this._emit(DIRTY_ONLY_BATCH);
1342
+ record.unsub = noopUnsub;
1343
+ try {
1344
+ record.unsub = depNode.subscribe((msgs) => {
1345
+ if (record.unsub === null) return;
1346
+ const tierOf = this._config.tierOf;
1347
+ let sawSettlement = false;
1348
+ for (const m of msgs) {
1349
+ if (tierOf(m[0]) >= 3) sawSettlement = true;
1350
+ this._config.onMessage(
1351
+ this,
1352
+ m,
1353
+ { direction: "down-in", depIndex: depIdx },
1354
+ this._actions
1355
+ );
1356
+ }
1357
+ if (sawSettlement) this._maybeRunFnOnSettlement();
1358
+ });
1359
+ } catch (err) {
1360
+ record.unsub = null;
1361
+ this._deps.pop();
1362
+ this._dirtyDepCount--;
1363
+ throw err;
844
1364
  }
1365
+ return depIdx;
845
1366
  }
846
1367
  /**
847
- * Update `_cached`, `_status`, `_terminal` from message batch before
848
- * delivery. Subclass hooks `_onInvalidate` / `_onTeardown` let
849
- * {@link NodeImpl} clear `_lastDepValues` and invoke cleanup fns.
1368
+ * @internal Unsubscribes from deps, fires fn cleanup (both shapes),
1369
+ * clears wave/store state, and (for compute nodes) drops `_cached` per
1370
+ * the ROM/RAM rule. Idempotent: second call is a no-op.
1371
+ *
1372
+ * @param skipStatusUpdate — When `true`, the caller takes responsibility
1373
+ * for setting `_status` after deactivation (e.g. TEARDOWN always sets
1374
+ * `"sentinel"` unconditionally). When `false` (default), deactivation
1375
+ * applies the ROM rule: compute nodes → `"sentinel"`, state nodes
1376
+ * preserve their current status.
850
1377
  */
851
- _handleLocalLifecycle(messages) {
852
- for (const m of messages) {
853
- const t = m[0];
854
- if (t === DATA) {
855
- if (m.length < 2) {
856
- continue;
857
- }
858
- this._cached = m[1];
859
- if (this._versioning != null) {
860
- advanceVersion(this._versioning, m[1], this._hashFn);
861
- }
862
- }
863
- if (t === INVALIDATE) {
864
- this._onInvalidate();
865
- this._cached = NO_VALUE;
1378
+ _deactivate(skipStatusUpdate = false) {
1379
+ const cleanup = this._cleanup;
1380
+ this._cleanup = void 0;
1381
+ if (typeof cleanup === "function") {
1382
+ try {
1383
+ cleanup();
1384
+ } catch (err) {
1385
+ this._emit([[ERROR, this._wrapFnError("cleanup threw", err)]]);
866
1386
  }
867
- this._status = statusAfterMessage(this._status, m);
868
- if (t === COMPLETE || t === ERROR) {
869
- this._terminal = true;
1387
+ } else if (cleanup != null && typeof cleanup.deactivation === "function") {
1388
+ try {
1389
+ cleanup.deactivation();
1390
+ } catch (err) {
1391
+ this._emit([[ERROR, this._wrapFnError("cleanup.deactivation threw", err)]]);
870
1392
  }
871
- if (t === TEARDOWN) {
872
- if (this._resetOnTeardown) {
873
- this._cached = NO_VALUE;
874
- }
875
- this._onTeardown();
1393
+ }
1394
+ for (const d of this._deps) {
1395
+ if (d.unsub != null) {
1396
+ const u = d.unsub;
1397
+ d.unsub = null;
876
1398
  try {
877
- this._propagateToMeta(t);
878
- } finally {
879
- this._onDeactivate();
1399
+ u();
1400
+ } catch {
880
1401
  }
881
1402
  }
882
- if (t !== TEARDOWN && propagatesToMeta(t)) {
883
- this._propagateToMeta(t);
884
- }
1403
+ resetDepRecord(d);
1404
+ }
1405
+ this._waveHasNewData = false;
1406
+ this._hasNewTerminal = false;
1407
+ this._hasCalledFnOnce = false;
1408
+ this._paused = false;
1409
+ this._pendingWave = false;
1410
+ this._pendingRerun = false;
1411
+ this._rerunDepth = 0;
1412
+ this._store = {};
1413
+ this._dirtyDepCount = 0;
1414
+ this._pauseLocks = null;
1415
+ this._pauseBuffer = null;
1416
+ if (this._fn != null) {
1417
+ this._cached = void 0;
885
1418
  }
886
- }
887
- /**
888
- * Subclass hook: invoked when INVALIDATE arrives (before `_cached` is
889
- * cleared). {@link NodeImpl} uses this to run the fn cleanup fn and
890
- * drop `_lastDepValues` so the next wave re-runs fn.
891
- */
892
- _onInvalidate() {
893
- }
894
- /**
895
- * Subclass hook: invoked when TEARDOWN arrives, before `_onDeactivate`.
896
- * {@link NodeImpl} uses this to run any pending cleanup fn.
897
- */
898
- _onTeardown() {
899
- }
900
- /** Forward a signal to all companion meta nodes (best-effort). */
901
- _propagateToMeta(t) {
902
- for (const metaNode of Object.values(this.meta)) {
903
- try {
904
- metaNode._downInternal([[t]]);
905
- } catch {
1419
+ if (!skipStatusUpdate) {
1420
+ if (this._fn != null || this._deps.length > 0) {
1421
+ if (!this._isTerminal || this._resubscribable) {
1422
+ this._status = "sentinel";
1423
+ }
906
1424
  }
907
1425
  }
908
1426
  }
1427
+ // --- Dep message dispatch (§3.5 singleton default) ---
909
1428
  /**
910
- * Frame a computed value into the right protocol messages and dispatch
911
- * via `_downInternal`. Used by `_runFn` and `actions.emit`.
1429
+ * @internal Default per-tier dispatch for incoming dep messages. Called
1430
+ * by `defaultOnMessage`. Updates the DepRecord, triggers wave
1431
+ * completion, and forwards passthrough traffic.
912
1432
  */
913
- _downAutoValue(value) {
914
- const wasDirty = this._status === "dirty";
915
- let unchanged;
916
- try {
917
- unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
918
- } catch (eqErr) {
919
- const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
920
- const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, {
921
- cause: eqErr
922
- });
923
- this._downInternal([[ERROR, wrapped]]);
924
- return;
1433
+ _onDepMessage(depIndex, msg) {
1434
+ const dep = this._deps[depIndex];
1435
+ const t = msg[0];
1436
+ if (this._inspectorHooks != null) {
1437
+ const ev = { kind: "dep_message", depIndex, message: msg };
1438
+ for (const hook of this._inspectorHooks) hook(ev);
925
1439
  }
926
- if (unchanged) {
927
- this._downInternal(wasDirty ? [[RESOLVED]] : [[DIRTY], [RESOLVED]]);
1440
+ if (t === START) return;
1441
+ if (t === DIRTY) {
1442
+ this._depDirtied(dep);
928
1443
  return;
929
1444
  }
930
- this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
931
- }
932
- };
933
-
934
- // src/core/node.ts
935
- var NodeImpl = class extends NodeBase {
936
- // --- Dep configuration (set once) ---
937
- _deps;
938
- _fn;
939
- _opts;
940
- _hasDeps;
941
- _isSingleDep;
942
- _autoComplete;
943
- // --- Wave tracking masks ---
944
- _depDirtyMask;
945
- _depSettledMask;
946
- _depCompleteMask;
947
- _allDepsCompleteMask;
948
- // --- Identity-skip optimization ---
949
- _lastDepValues;
950
- _cleanup;
951
- // --- Upstream bookkeeping ---
952
- _upstreamUnsubs = [];
953
- // --- Fn behavior flag ---
954
- /** @internal Read by `describeNode` to infer `"operator"` label. */
955
- _manualEmitUsed = false;
956
- constructor(deps, fn, opts) {
957
- super(opts);
958
- this._deps = deps;
959
- this._fn = fn;
960
- this._opts = opts;
961
- this._hasDeps = deps.length > 0;
962
- this._isSingleDep = deps.length === 1 && fn != null;
963
- this._autoComplete = opts.completeWhenDepsComplete ?? true;
964
- if (!this._hasDeps && fn == null && this._cached !== NO_VALUE) {
965
- this._status = "settled";
966
- }
967
- this._depDirtyMask = createBitSet(deps.length);
968
- this._depSettledMask = createBitSet(deps.length);
969
- this._depCompleteMask = createBitSet(deps.length);
970
- this._allDepsCompleteMask = createBitSet(deps.length);
971
- for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
972
- this.down = this.down.bind(this);
973
- this.up = this.up.bind(this);
974
- }
975
- // --- Meta node factory (called from base constructor) ---
976
- _createMetaNode(key, initialValue, opts) {
977
- return node({
978
- initial: initialValue,
979
- name: `${opts.name ?? "node"}:meta:${key}`,
980
- describeKind: "state",
981
- ...opts.guard != null ? { guard: opts.guard } : {}
982
- });
983
- }
984
- // --- Manual emit tracker (set by actions.down / actions.emit) ---
985
- _onManualEmit() {
986
- this._manualEmitUsed = true;
987
- }
988
- // --- Up / unsubscribe ---
989
- up(messages, options) {
990
- if (!this._hasDeps) return;
991
- if (!options?.internal && this._guard != null) {
992
- const actor = normalizeActor(options?.actor);
993
- if (!this._guard(actor, "write")) {
994
- throw new GuardDenied({ actor, action: "write", nodeName: this.name });
995
- }
996
- this._recordMutation(actor);
1445
+ if (t === INVALIDATE) {
1446
+ this._depInvalidated(dep);
1447
+ this._emit(INVALIDATE_ONLY_BATCH);
1448
+ return;
997
1449
  }
998
- for (const dep of this._deps) {
999
- if (options === void 0) {
1000
- dep.up?.(messages);
1001
- } else {
1002
- dep.up?.(messages, options);
1003
- }
1450
+ if (t === PAUSE || t === RESUME) {
1451
+ this._emit([msg]);
1452
+ return;
1004
1453
  }
1005
- }
1006
- _upInternal(messages) {
1007
- if (!this._hasDeps) return;
1008
- for (const dep of this._deps) {
1009
- dep.up?.(messages, { internal: true });
1454
+ if (t === TEARDOWN) {
1455
+ this._emit(TEARDOWN_ONLY_BATCH);
1456
+ return;
1010
1457
  }
1011
- }
1012
- unsubscribe() {
1013
- if (!this._hasDeps) return;
1014
- this._disconnectUpstream();
1015
- }
1016
- // --- Activation (first-subscriber / last-subscriber hooks) ---
1017
- _onActivate() {
1018
- if (this._hasDeps) {
1019
- this._connectUpstream();
1458
+ if (t === DATA) {
1459
+ this._depSettledAsData(dep, msg[1]);
1460
+ } else if (t === RESOLVED) {
1461
+ this._depSettledAsResolved(dep);
1462
+ } else if (t === COMPLETE) {
1463
+ this._depSettledAsTerminal(dep, true);
1464
+ } else if (t === ERROR) {
1465
+ this._depSettledAsTerminal(dep, msg[1]);
1466
+ } else {
1467
+ this._emit([msg]);
1020
1468
  return;
1021
1469
  }
1022
- if (this._fn) {
1023
- this._runFn();
1470
+ if (!this._fn) {
1471
+ if (t === DATA || t === RESOLVED) {
1472
+ this._emit([msg]);
1473
+ }
1474
+ if (t === COMPLETE || t === ERROR) {
1475
+ this._maybeAutoTerminalAfterWave();
1476
+ }
1024
1477
  return;
1025
1478
  }
1026
1479
  }
1027
- _doDeactivate() {
1028
- this._disconnectUpstream();
1029
- const cleanup = this._cleanup;
1030
- this._cleanup = void 0;
1031
- cleanup?.();
1032
- if (this._fn != null) {
1033
- this._cached = NO_VALUE;
1034
- this._lastDepValues = void 0;
1035
- }
1036
- if (this._hasDeps || this._fn != null) {
1037
- this._status = "disconnected";
1480
+ // --- Centralized dep-state transitions (A3 settlement counters) ---
1481
+ //
1482
+ // Every mutation to `DepRecord.dirty` / `DepRecord.prevData` /
1483
+ // `DepRecord.terminal` must go through one of these helpers so the
1484
+ // `_dirtyDepCount` and `_sentinelDepCount` counters stay in sync with
1485
+ // the per-record flags. `_maybeRunFnOnSettlement` reads the counters
1486
+ // and never re-scans the `_deps` array.
1487
+ /**
1488
+ * Called when a dep transitions `dirty: false → true` (either from an
1489
+ * incoming DIRTY, or pre-set during `_activate` / `_addDep` /
1490
+ * `_depInvalidated`). No-op if the dep is already dirty. Fires the
1491
+ * downstream DIRTY emit if we're the first to dirty this wave.
1492
+ */
1493
+ _depDirtied(dep) {
1494
+ if (dep.dirty) return;
1495
+ dep.dirty = true;
1496
+ dep.involvedThisWave = true;
1497
+ this._dirtyDepCount++;
1498
+ if (this._status !== "dirty") {
1499
+ this._emit(DIRTY_ONLY_BATCH);
1038
1500
  }
1039
1501
  }
1040
- // --- INVALIDATE / TEARDOWN hooks (clear fn state) ---
1041
- _onInvalidate() {
1042
- const cleanup = this._cleanup;
1043
- this._cleanup = void 0;
1044
- cleanup?.();
1045
- this._lastDepValues = void 0;
1046
- }
1047
- _onTeardown() {
1048
- const cleanup = this._cleanup;
1049
- this._cleanup = void 0;
1050
- cleanup?.();
1051
- }
1052
- // --- Upstream connect / disconnect ---
1053
- _connectUpstream() {
1054
- if (!this._hasDeps) return;
1055
- if (this._upstreamUnsubs.length > 0) return;
1056
- this._depDirtyMask.setAll();
1057
- this._depSettledMask.reset();
1058
- this._depCompleteMask.reset();
1059
- const depValuesBefore = this._lastDepValues;
1060
- const subHints = this._isSingleDep ? { singleDep: true } : void 0;
1061
- for (let i = 0; i < this._deps.length; i += 1) {
1062
- const dep = this._deps[i];
1063
- this._upstreamUnsubs.push(
1064
- dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
1065
- );
1066
- }
1067
- if (this._fn && this._onMessage && !this._terminal && this._lastDepValues === depValuesBefore) {
1068
- this._runFn();
1502
+ /**
1503
+ * Called when a dep delivers new DATA: clears dirty, stores the payload,
1504
+ * marks wave-has-data, and — if this is the dep's first DATA — clears
1505
+ * its sentinel slot so the first-run gate can open.
1506
+ */
1507
+ _depSettledAsData(dep, value) {
1508
+ if (dep.dirty) {
1509
+ dep.dirty = false;
1510
+ this._dirtyDepCount--;
1069
1511
  }
1512
+ dep.involvedThisWave = true;
1513
+ dep.dataBatch.push(value);
1514
+ this._waveHasNewData = true;
1070
1515
  }
1071
- _disconnectUpstream() {
1072
- if (this._upstreamUnsubs.length === 0) return;
1073
- for (const unsub of this._upstreamUnsubs.splice(0)) {
1074
- unsub();
1516
+ /**
1517
+ * Called when a dep emits RESOLVED (wave settled, value unchanged).
1518
+ * Clears dirty; does NOT touch `prevData` / `terminal` / sentinel
1519
+ * count — sentinel only exits on first DATA or terminal, not RESOLVED.
1520
+ */
1521
+ _depSettledAsResolved(dep) {
1522
+ if (dep.dirty) {
1523
+ dep.dirty = false;
1524
+ this._dirtyDepCount--;
1075
1525
  }
1076
- this._depDirtyMask.reset();
1077
- this._depSettledMask.reset();
1078
- this._depCompleteMask.reset();
1079
1526
  }
1080
- // --- Wave handling ---
1081
- _handleDepMessages(index, messages) {
1082
- for (const msg of messages) {
1083
- this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
1084
- const t = msg[0];
1085
- if (this._onMessage) {
1086
- try {
1087
- const consumed = this._onMessage(msg, index, this._actions);
1088
- if (consumed) {
1089
- if (t === START || t === DATA || t === RESOLVED) {
1090
- this._depDirtyMask.clear(index);
1091
- if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1092
- this._depDirtyMask.reset();
1093
- this._depSettledMask.reset();
1094
- this._runFn();
1095
- }
1096
- }
1097
- continue;
1098
- }
1099
- } catch (err) {
1100
- const errMsg = err instanceof Error ? err.message : String(err);
1101
- const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
1102
- cause: err
1103
- });
1104
- this._downInternal([[ERROR, wrapped]]);
1105
- return;
1106
- }
1107
- }
1108
- if (messageTier(t) < 1) continue;
1109
- if (!this._fn) {
1110
- if (t === COMPLETE && this._deps.length > 1) {
1111
- this._depCompleteMask.set(index);
1112
- this._maybeCompleteFromDeps();
1113
- continue;
1114
- }
1115
- this._downInternal([msg]);
1116
- continue;
1117
- }
1118
- if (t === DIRTY) {
1119
- this._onDepDirty(index);
1120
- continue;
1121
- }
1122
- if (t === DATA || t === RESOLVED) {
1123
- this._onDepSettled(index);
1124
- continue;
1125
- }
1126
- if (t === COMPLETE) {
1127
- this._depCompleteMask.set(index);
1128
- this._depDirtyMask.clear(index);
1129
- this._depSettledMask.clear(index);
1130
- if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1131
- this._depDirtyMask.reset();
1132
- this._depSettledMask.reset();
1133
- this._runFn();
1134
- } else if (!this._depDirtyMask.any() && this._status === "dirty") {
1135
- this._depSettledMask.reset();
1136
- this._runFn();
1137
- }
1138
- this._maybeCompleteFromDeps();
1139
- continue;
1140
- }
1141
- if (t === ERROR) {
1142
- this._downInternal([msg]);
1143
- continue;
1144
- }
1145
- if (t === INVALIDATE || t === TEARDOWN || t === PAUSE || t === RESUME) {
1146
- this._downInternal([msg]);
1147
- continue;
1148
- }
1149
- this._downInternal([msg]);
1527
+ /**
1528
+ * Called when a dep delivers COMPLETE (`terminal = true`) or ERROR
1529
+ * (`terminal = errorPayload`). Clears dirty, stores the terminal, and
1530
+ * if the dep had never contributed a DATA yet — leaves sentinel
1531
+ * since the gate treats "terminated without data" as gate-open too.
1532
+ */
1533
+ _depSettledAsTerminal(dep, terminal) {
1534
+ if (dep.dirty) {
1535
+ dep.dirty = false;
1536
+ this._dirtyDepCount--;
1150
1537
  }
1538
+ dep.terminal = terminal;
1539
+ dep.involvedThisWave = true;
1540
+ this._hasNewTerminal = true;
1151
1541
  }
1152
- _onDepDirty(index) {
1153
- const wasDirty = this._depDirtyMask.has(index);
1154
- this._depDirtyMask.set(index);
1155
- this._depSettledMask.clear(index);
1156
- if (!wasDirty) {
1157
- this._downInternal([[DIRTY]]);
1542
+ /**
1543
+ * Called when a dep emits INVALIDATE: clears prevData, terminal, and
1544
+ * dataBatch. The dep is now back in the "never delivered a real value"
1545
+ * state — `prevData === undefined` so the sentinel check in fn will fire.
1546
+ */
1547
+ _depInvalidated(dep) {
1548
+ dep.prevData = void 0;
1549
+ dep.terminal = void 0;
1550
+ dep.dataBatch.length = 0;
1551
+ if (!dep.dirty) {
1552
+ dep.dirty = true;
1553
+ dep.involvedThisWave = true;
1554
+ this._dirtyDepCount++;
1555
+ } else {
1556
+ dep.involvedThisWave = false;
1158
1557
  }
1159
1558
  }
1160
- _onDepSettled(index) {
1161
- if (!this._depDirtyMask.has(index)) {
1162
- this._onDepDirty(index);
1559
+ _maybeRunFnOnSettlement() {
1560
+ if (this._isTerminal && !this._resubscribable) return;
1561
+ if (this._dirtyDepCount > 0) return;
1562
+ if (this._paused) {
1563
+ this._pendingWave = true;
1564
+ return;
1163
1565
  }
1164
- this._depSettledMask.set(index);
1165
- if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1166
- this._depDirtyMask.reset();
1167
- this._depSettledMask.reset();
1168
- this._runFn();
1566
+ if (!this._waveHasNewData && !this._hasNewTerminal && this._hasCalledFnOnce) {
1567
+ this._clearWaveFlags();
1568
+ this._emit(RESOLVED_ONLY_BATCH);
1569
+ this._maybeAutoTerminalAfterWave();
1570
+ return;
1169
1571
  }
1572
+ if (this._fn) this._execFn();
1573
+ this._maybeAutoTerminalAfterWave();
1170
1574
  }
1171
- _maybeCompleteFromDeps() {
1172
- if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
1173
- this._downInternal([[COMPLETE]]);
1575
+ _maybeAutoTerminalAfterWave() {
1576
+ if (this._deps.length === 0) return;
1577
+ if (this._isTerminal) return;
1578
+ const erroredDep = this._deps.find((d) => d.terminal !== void 0 && d.terminal !== true);
1579
+ if (erroredDep != null) {
1580
+ if (this._autoError) {
1581
+ this._emit([[ERROR, erroredDep.terminal]]);
1582
+ }
1583
+ return;
1584
+ }
1585
+ if (this._autoComplete && this._deps.every((d) => d.terminal !== void 0)) {
1586
+ this._emit(COMPLETE_ONLY_BATCH);
1174
1587
  }
1175
1588
  }
1176
1589
  // --- Fn execution ---
1177
- _runFn() {
1590
+ /**
1591
+ * @internal Runs the node fn once. Default cleanup (function form) fires
1592
+ * before the new run; `{ deactivation }` cleanup survives.
1593
+ */
1594
+ _execFn() {
1178
1595
  if (!this._fn) return;
1179
- if (this._terminal && !this._resubscribable) return;
1180
- try {
1181
- const n = this._deps.length;
1182
- const depValues = new Array(n);
1183
- for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
1184
- const prev = this._lastDepValues;
1185
- if (n > 0 && prev != null && prev.length === n) {
1186
- let allSame = true;
1187
- for (let i = 0; i < n; i++) {
1188
- if (!Object.is(depValues[i], prev[i])) {
1189
- allSame = false;
1190
- break;
1191
- }
1192
- }
1193
- if (allSame) {
1194
- if (this._status === "dirty") {
1195
- this._downInternal([[RESOLVED]]);
1196
- }
1197
- return;
1198
- }
1199
- }
1200
- const prevCleanup = this._cleanup;
1596
+ if (this._isTerminal && !this._resubscribable) return;
1597
+ if (this._isExecutingFn) {
1598
+ this._pendingRerun = true;
1599
+ return;
1600
+ }
1601
+ const prevCleanup = this._cleanup;
1602
+ if (typeof prevCleanup === "function") {
1201
1603
  this._cleanup = void 0;
1202
- prevCleanup?.();
1203
- this._manualEmitUsed = false;
1204
- this._lastDepValues = depValues;
1205
- this._emitInspectorHook({ kind: "run", depValues });
1206
- const out = this._fn(depValues, this._actions);
1207
- if (isCleanupResult(out)) {
1208
- this._cleanup = out.cleanup;
1209
- if (this._manualEmitUsed) return;
1210
- if ("value" in out) {
1211
- this._downAutoValue(out.value);
1212
- }
1213
- return;
1214
- }
1215
- if (isCleanupFn(out)) {
1216
- this._cleanup = out;
1604
+ try {
1605
+ prevCleanup();
1606
+ } catch (err) {
1607
+ this._emit([[ERROR, this._wrapFnError("cleanup threw", err)]]);
1217
1608
  return;
1218
1609
  }
1219
- if (this._manualEmitUsed) return;
1220
- if (out === void 0) return;
1221
- this._downAutoValue(out);
1222
- } catch (err) {
1223
- const errMsg = err instanceof Error ? err.message : String(err);
1224
- const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
1225
- this._downInternal([[ERROR, wrapped]]);
1226
1610
  }
1227
- }
1228
- };
1229
- var isNodeArray = (value) => Array.isArray(value);
1230
- var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
1231
- function node(depsOrFn, fnOrOpts, optsArg) {
1232
- const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
1233
- const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
1234
- let opts = {};
1235
- if (isNodeArray(depsOrFn)) {
1236
- opts = (isNodeOptions(fnOrOpts) ? fnOrOpts : optsArg) ?? {};
1237
- } else if (isNodeOptions(depsOrFn)) {
1238
- opts = depsOrFn;
1239
- } else {
1240
- opts = (isNodeOptions(fnOrOpts) ? fnOrOpts : optsArg) ?? {};
1241
- }
1242
- return new NodeImpl(deps, fn, opts);
1243
- }
1244
-
1245
- // src/core/bridge.ts
1246
- var DEFAULT_DOWN = [
1247
- DATA,
1248
- DIRTY,
1249
- RESOLVED,
1250
- COMPLETE,
1251
- ERROR,
1252
- TEARDOWN,
1253
- PAUSE,
1254
- RESUME,
1255
- INVALIDATE
1256
- ];
1257
- var STANDARD_TYPES = /* @__PURE__ */ new Set([
1258
- DATA,
1259
- DIRTY,
1260
- RESOLVED,
1261
- COMPLETE,
1262
- ERROR,
1263
- TEARDOWN,
1264
- PAUSE,
1265
- RESUME,
1266
- INVALIDATE
1267
- ]);
1268
- function bridge(from, to, opts) {
1269
- const allowedDown = new Set(opts?.down ?? DEFAULT_DOWN);
1270
- const onMessage = (msg, _depIndex, _actions) => {
1271
- const type = msg[0];
1272
- if (!STANDARD_TYPES.has(type)) {
1273
- to.down([msg]);
1274
- return true;
1275
- }
1276
- if (type === COMPLETE || type === ERROR) {
1277
- if (allowedDown.has(type)) {
1278
- to.down([msg]);
1611
+ const batchData = this._deps.map(
1612
+ (d) => !d.involvedThisWave ? void 0 : d.dataBatch.length > 0 ? [...d.dataBatch] : []
1613
+ );
1614
+ const prevData = this._deps.map((d) => d.prevData);
1615
+ for (let i = 0; i < this._deps.length; i++) {
1616
+ const batch2 = batchData[i];
1617
+ if (batch2 != null && batch2.length > 0) {
1618
+ this._deps[i].prevData = batch2[batch2.length - 1];
1279
1619
  }
1280
- return false;
1281
1620
  }
1282
- if (!allowedDown.has(type)) {
1283
- return true;
1621
+ const terminalDeps = this._deps.map((d) => d.terminal);
1622
+ const ctx = { prevData, terminalDeps, store: this._store };
1623
+ this._hasCalledFnOnce = true;
1624
+ this._clearWaveFlags();
1625
+ if (this._inspectorHooks != null) {
1626
+ const ev = { kind: "run", batchData, prevData };
1627
+ for (const hook of this._inspectorHooks) hook(ev);
1628
+ }
1629
+ this._isExecutingFn = true;
1630
+ try {
1631
+ const result = this._fn(batchData, this._actions, ctx);
1632
+ if (typeof result === "function") {
1633
+ this._cleanup = result;
1634
+ } else if (result != null && typeof result === "object" && typeof result.deactivation === "function") {
1635
+ this._cleanup = result;
1636
+ }
1637
+ } catch (err) {
1638
+ this._emit([[ERROR, this._wrapFnError("fn threw", err)]]);
1639
+ } finally {
1640
+ this._isExecutingFn = false;
1641
+ if (this._pendingRerun) {
1642
+ this._pendingRerun = false;
1643
+ this._rerunDepth += 1;
1644
+ if (this._rerunDepth > MAX_RERUN_DEPTH) {
1645
+ this._rerunDepth = 0;
1646
+ this._emit([
1647
+ [
1648
+ ERROR,
1649
+ new Error(
1650
+ `Node "${this.name}": _pendingRerun depth exceeded ${MAX_RERUN_DEPTH} \u2014 likely a reactive cycle`
1651
+ )
1652
+ ]
1653
+ ]);
1654
+ } else {
1655
+ this._maybeRunFnOnSettlement();
1656
+ }
1657
+ } else {
1658
+ this._rerunDepth = 0;
1659
+ }
1660
+ this._clearWaveFlags();
1284
1661
  }
1285
- to.down([msg]);
1286
- return true;
1287
- };
1288
- return node([from], void 0, {
1289
- name: opts?.name,
1290
- describeKind: "effect",
1291
- onMessage,
1292
- meta: { _internal: true }
1293
- });
1294
- }
1295
-
1296
- // src/core/dynamic-node.ts
1297
- var MAX_RERUN = 16;
1298
- function dynamicNode(fn, opts) {
1299
- return new DynamicNodeImpl(fn, opts ?? {});
1300
- }
1301
- var DynamicNodeImpl = class extends NodeBase {
1302
- _fn;
1303
- _autoComplete;
1304
- // Dynamic deps tracking
1305
- /** @internal Read by `describeNode`. */
1306
- _deps = [];
1307
- _depUnsubs = [];
1308
- _depIndexMap = /* @__PURE__ */ new Map();
1309
- _depDirtyBits = /* @__PURE__ */ new Set();
1310
- _depSettledBits = /* @__PURE__ */ new Set();
1311
- _depCompleteBits = /* @__PURE__ */ new Set();
1312
- // Execution state
1313
- _running = false;
1314
- _rewiring = false;
1315
- _bufferedDepMessages = [];
1316
- _trackedValues = /* @__PURE__ */ new Map();
1317
- _rerunCount = 0;
1318
- constructor(fn, opts) {
1319
- super(opts);
1320
- this._fn = fn;
1321
- this._autoComplete = opts.completeWhenDepsComplete ?? true;
1322
- this.down = this.down.bind(this);
1323
- this.up = this.up.bind(this);
1324
1662
  }
1325
- _createMetaNode(key, initialValue, opts) {
1326
- return node({
1327
- initial: initialValue,
1328
- name: `${opts.name ?? "dynamicNode"}:meta:${key}`,
1329
- describeKind: "state",
1330
- ...opts.guard != null ? { guard: opts.guard } : {}
1331
- });
1663
+ _clearWaveFlags() {
1664
+ this._waveHasNewData = false;
1665
+ this._hasNewTerminal = false;
1666
+ for (const d of this._deps) {
1667
+ d.involvedThisWave = false;
1668
+ d.dataBatch.length = 0;
1669
+ }
1332
1670
  }
1333
- /** Versioning not supported on DynamicNodeImpl (override base). */
1334
- get v() {
1335
- return void 0;
1671
+ _wrapFnError(label, err) {
1672
+ const msg = err instanceof Error ? err.message : String(err);
1673
+ return new Error(`Node "${this.name}": ${label}: ${msg}`, { cause: err });
1336
1674
  }
1337
- // --- Up / unsubscribe ---
1338
- up(messages, options) {
1339
- if (this._deps.length === 0) return;
1340
- if (!options?.internal && this._guard != null) {
1341
- const actor = normalizeActor(options?.actor);
1342
- if (!this._guard(actor, "write")) {
1343
- throw new GuardDenied({ actor, action: "write", nodeName: this.name });
1675
+ // --- Framing (tier sort + synthetic DIRTY prefix) ---
1676
+ /**
1677
+ * @internal Stable tier sort + synthetic DIRTY prefix for an outgoing
1678
+ * batch. Fast path: already-monotone single-tier batches (the common
1679
+ * case from interned singletons like `DIRTY_ONLY_BATCH`) return the
1680
+ * input unchanged. General path: decorate-sort-undecorate into a new
1681
+ * array, then prepend `[DIRTY]` after any tier-0 START entries when
1682
+ * a tier-3 payload is present and the node isn't already dirty.
1683
+ *
1684
+ * Single source of truth for the spec §1.3.1 framing invariant. Every
1685
+ * outgoing path hits `_frameBatch` exactly once via `_emit`.
1686
+ */
1687
+ _frameBatch(messages) {
1688
+ const tierOf = this._config.tierOf;
1689
+ if (messages.length === 1) {
1690
+ const t = tierOf(messages[0][0]);
1691
+ if (t === 3 && this._status !== "dirty") {
1692
+ return [DIRTY_MSG, messages[0]];
1344
1693
  }
1345
- this._recordMutation(actor);
1694
+ return messages;
1346
1695
  }
1347
- for (const dep of this._deps) {
1348
- dep.up?.(messages, options);
1696
+ let monotone = true;
1697
+ let hasTier3 = false;
1698
+ let hasDirty = false;
1699
+ let prevTier = -1;
1700
+ for (const m of messages) {
1701
+ const tier = tierOf(m[0]);
1702
+ if (tier < prevTier) monotone = false;
1703
+ if (tier === 3) hasTier3 = true;
1704
+ if (m[0] === DIRTY) hasDirty = true;
1705
+ prevTier = tier;
1706
+ }
1707
+ let sorted = messages;
1708
+ if (!monotone) {
1709
+ const indexed = messages.map((m, i) => ({ m, i, tier: tierOf(m[0]) }));
1710
+ indexed.sort((a, b) => a.tier - b.tier || a.i - b.i);
1711
+ sorted = indexed.map((x) => x.m);
1712
+ }
1713
+ if (hasTier3 && !hasDirty && this._status !== "dirty") {
1714
+ let insertAt = 0;
1715
+ while (insertAt < sorted.length && tierOf(sorted[insertAt][0]) === 0) insertAt++;
1716
+ if (insertAt === 0) return [DIRTY_MSG, ...sorted];
1717
+ return [...sorted.slice(0, insertAt), DIRTY_MSG, ...sorted.slice(insertAt)];
1349
1718
  }
1719
+ return sorted;
1350
1720
  }
1351
- _upInternal(messages) {
1352
- for (const dep of this._deps) {
1353
- dep.up?.(messages, { internal: true });
1721
+ // --- Emit pipeline ---
1722
+ /**
1723
+ * @internal The unified dispatch waist — one call = one wave.
1724
+ *
1725
+ * Pipeline stages, in order:
1726
+ *
1727
+ * 1. Early-return on empty batch.
1728
+ * 2. Terminal filter — post-COMPLETE/ERROR only TEARDOWN/INVALIDATE
1729
+ * still propagate so graph teardown and cache-clear still work.
1730
+ * 3. Tier sort (stable) — the batch can be in any order when it
1731
+ * arrives; the walker downstream (`downWithBatch`) assumes
1732
+ * ascending tier monotone, and so does `_updateState`'s tier-3
1733
+ * slice walk. This is the single source of truth for ordering.
1734
+ * 4. Synthetic DIRTY prefix — if a tier-3 payload is present, no
1735
+ * DIRTY is already in the batch, and the node isn't already in
1736
+ * `"dirty"` status, prepend `[DIRTY]` after any tier-0 START
1737
+ * entries. Guarantees spec §1.3.1 (DIRTY precedes DATA within
1738
+ * the same batch) uniformly across every entry point.
1739
+ * 5. PAUSE/RESUME lock bookkeeping (C0) — update `_pauseLocks`,
1740
+ * derive `_paused`, filter unknown-lockId RESUME, replay
1741
+ * bufferAll buffer on final lock release.
1742
+ * 6. Meta TEARDOWN fan-out — notify meta children before
1743
+ * `_updateState`'s TEARDOWN branch calls `_deactivate`. Hoisted
1744
+ * out of the walk to keep `_updateState` re-entrance-free.
1745
+ * 7. `_updateState` — walk the batch in tier order, advancing
1746
+ * `_cached` / `_status` / `_versioning` and running equals
1747
+ * substitution on tier-3 DATA (§3.5.1). Returns
1748
+ * `{finalMessages, equalsError?}`.
1749
+ * 8. `downWithBatch` dispatch (or bufferAll capture if paused with
1750
+ * `pausable: "resumeAll"`).
1751
+ * 9. Recursive ERROR emission if equals threw mid-walk.
1752
+ *
1753
+ * `node.down` / `node.emit` / `actions.down` / `actions.emit` all
1754
+ * converge here — the unified `_emit` waist (spec §1.3.1).
1755
+ */
1756
+ _emit(messages) {
1757
+ if (messages.length === 0) return;
1758
+ let deliverable = messages;
1759
+ const terminal = this._isTerminal;
1760
+ if (terminal && !this._resubscribable) {
1761
+ const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
1762
+ if (pass.length === 0) return;
1763
+ deliverable = pass;
1354
1764
  }
1355
- }
1356
- unsubscribe() {
1357
- this._disconnect();
1358
- }
1359
- // --- Activation hooks ---
1360
- _onActivate() {
1361
- this._runFn();
1362
- }
1363
- _doDeactivate() {
1364
- this._disconnect();
1365
- }
1366
- _disconnect() {
1367
- for (const unsub of this._depUnsubs) unsub();
1368
- this._depUnsubs = [];
1369
- this._deps = [];
1370
- this._depIndexMap.clear();
1371
- this._depDirtyBits.clear();
1372
- this._depSettledBits.clear();
1373
- this._depCompleteBits.clear();
1374
- this._cached = NO_VALUE;
1375
- this._status = "disconnected";
1376
- }
1377
- // --- Fn execution with rewire buffer ---
1378
- _runFn() {
1379
- if (this._terminal && !this._resubscribable) return;
1380
- if (this._running) return;
1381
- this._running = true;
1382
- this._rerunCount = 0;
1383
- let result;
1384
- try {
1385
- for (; ; ) {
1386
- const trackedDeps = [];
1387
- const trackedValuesMap = /* @__PURE__ */ new Map();
1388
- const trackedSet = /* @__PURE__ */ new Set();
1389
- const get = (dep) => {
1390
- if (!trackedSet.has(dep)) {
1391
- trackedSet.add(dep);
1392
- trackedDeps.push(dep);
1393
- trackedValuesMap.set(dep, dep.get());
1765
+ deliverable = this._frameBatch(deliverable);
1766
+ let filtered = null;
1767
+ for (let i = 0; i < deliverable.length; i++) {
1768
+ const m = deliverable[i];
1769
+ const t = m[0];
1770
+ if (t !== PAUSE && t !== RESUME) {
1771
+ if (filtered != null) filtered.push(m);
1772
+ continue;
1773
+ }
1774
+ if (m.length < 2) {
1775
+ throw new Error(
1776
+ `Node "${this.name}": [[${t === PAUSE ? "PAUSE" : "RESUME"}]] must carry a lockId payload \u2014 bare PAUSE/RESUME is a protocol violation (C0 rule). Use \`[[PAUSE, lockId]]\` / \`[[RESUME, lockId]]\`.`
1777
+ );
1778
+ }
1779
+ let forward = true;
1780
+ if (this._pausable !== false) {
1781
+ const lockId = m[1];
1782
+ if (t === PAUSE) {
1783
+ if (this._pauseLocks == null) this._pauseLocks = /* @__PURE__ */ new Set();
1784
+ this._pauseLocks.add(lockId);
1785
+ this._paused = true;
1786
+ if (this._pausable === "resumeAll" && this._pauseBuffer == null) {
1787
+ this._pauseBuffer = [];
1394
1788
  }
1395
- return dep.get();
1396
- };
1397
- this._trackedValues = trackedValuesMap;
1398
- const depValues = [];
1399
- for (const dep of this._deps) depValues.push(dep.get());
1400
- this._emitInspectorHook({ kind: "run", depValues });
1401
- try {
1402
- result = this._fn(get);
1403
- } catch (err) {
1404
- const errMsg = err instanceof Error ? err.message : String(err);
1405
- const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, {
1406
- cause: err
1407
- });
1408
- this._downInternal([[ERROR, wrapped]]);
1409
- return;
1410
- }
1411
- this._rewiring = true;
1412
- this._bufferedDepMessages = [];
1413
- try {
1414
- this._rewire(trackedDeps);
1415
- } finally {
1416
- this._rewiring = false;
1417
- }
1418
- let needsRerun = false;
1419
- for (const entry of this._bufferedDepMessages) {
1420
- for (const msg of entry.msgs) {
1421
- if (msg[0] === DATA) {
1422
- const dep = this._deps[entry.index];
1423
- const trackedValue = dep != null ? this._trackedValues.get(dep) : void 0;
1424
- const actualValue = msg[1];
1425
- if (!this._equals(trackedValue, actualValue)) {
1426
- needsRerun = true;
1427
- break;
1789
+ } else {
1790
+ if (this._pauseLocks == null || !this._pauseLocks.has(lockId)) {
1791
+ forward = false;
1792
+ } else {
1793
+ this._pauseLocks.delete(lockId);
1794
+ if (this._pauseLocks.size === 0) {
1795
+ this._paused = false;
1796
+ if (this._pauseBuffer != null && this._pauseBuffer.length > 0) {
1797
+ const drain = this._pauseBuffer;
1798
+ this._pauseBuffer = [];
1799
+ this._emit(drain);
1800
+ }
1801
+ if (this._pendingWave) {
1802
+ this._pendingWave = false;
1803
+ this._maybeRunFnOnSettlement();
1428
1804
  }
1429
1805
  }
1430
1806
  }
1431
- if (needsRerun) break;
1432
- }
1433
- if (needsRerun) {
1434
- this._rerunCount += 1;
1435
- if (this._rerunCount > MAX_RERUN) {
1436
- this._bufferedDepMessages = [];
1437
- this._downInternal([
1438
- [
1439
- ERROR,
1440
- new Error(
1441
- `dynamicNode "${this.name ?? "anonymous"}": rewire did not stabilize within ${MAX_RERUN} iterations`
1442
- )
1443
- ]
1444
- ]);
1445
- return;
1446
- }
1447
- this._bufferedDepMessages = [];
1448
- continue;
1449
1807
  }
1450
- const drain = this._bufferedDepMessages;
1451
- this._bufferedDepMessages = [];
1452
- for (const entry of drain) {
1453
- for (const msg of entry.msgs) {
1454
- this._updateMasksForMessage(entry.index, msg);
1455
- }
1456
- }
1457
- this._depDirtyBits.clear();
1458
- this._depSettledBits.clear();
1459
- break;
1460
1808
  }
1461
- } finally {
1462
- this._running = false;
1463
- }
1464
- this._downAutoValue(result);
1465
- }
1466
- _rewire(newDeps) {
1467
- const oldMap = this._depIndexMap;
1468
- const newMap = /* @__PURE__ */ new Map();
1469
- const newUnsubs = [];
1470
- for (let i = 0; i < newDeps.length; i++) {
1471
- const dep = newDeps[i];
1472
- newMap.set(dep, i);
1473
- const oldIdx = oldMap.get(dep);
1474
- if (oldIdx !== void 0) {
1475
- newUnsubs.push(this._depUnsubs[oldIdx]);
1476
- this._depUnsubs[oldIdx] = () => {
1477
- };
1478
- } else {
1479
- const idx = i;
1480
- const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
1481
- newUnsubs.push(unsub);
1809
+ if (!forward) {
1810
+ if (filtered == null) filtered = deliverable.slice(0, i);
1811
+ } else if (filtered != null) {
1812
+ filtered.push(m);
1482
1813
  }
1483
1814
  }
1484
- for (const [dep, oldIdx] of oldMap) {
1485
- if (!newMap.has(dep)) {
1486
- this._depUnsubs[oldIdx]();
1487
- }
1815
+ if (filtered != null) {
1816
+ if (filtered.length === 0) return;
1817
+ deliverable = filtered;
1488
1818
  }
1489
- this._deps = newDeps;
1490
- this._depUnsubs = newUnsubs;
1491
- this._depIndexMap = newMap;
1492
- this._depDirtyBits.clear();
1493
- this._depSettledBits.clear();
1494
- const newCompleteBits = /* @__PURE__ */ new Set();
1495
- for (const oldIdx of this._depCompleteBits) {
1496
- for (const [dep, idx] of oldMap) {
1497
- if (idx === oldIdx && newMap.has(dep)) {
1498
- newCompleteBits.add(newMap.get(dep));
1499
- break;
1819
+ if (this._hasMeta && deliverable.some((m) => m[0] === TEARDOWN)) {
1820
+ for (const k of Object.keys(this.meta)) {
1821
+ try {
1822
+ this.meta[k]._emit(TEARDOWN_ONLY_BATCH);
1823
+ } catch {
1500
1824
  }
1501
1825
  }
1502
1826
  }
1503
- this._depCompleteBits = newCompleteBits;
1504
- }
1505
- // --- Dep message handling ---
1506
- _handleDepMessages(index, messages) {
1507
- if (this._rewiring) {
1508
- this._bufferedDepMessages.push({ index, msgs: messages });
1509
- return;
1510
- }
1511
- for (const msg of messages) {
1512
- this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
1513
- const t = msg[0];
1514
- if (this._onMessage) {
1827
+ const { finalMessages, equalsError } = this._updateState(deliverable);
1828
+ if (finalMessages.length > 0 && this._config.inspectorEnabled) {
1829
+ const inspector = this._config.globalInspector;
1830
+ if (inspector != null) {
1515
1831
  try {
1516
- if (this._onMessage(msg, index, this._actions)) continue;
1517
- } catch (err) {
1518
- const errMsg = err instanceof Error ? err.message : String(err);
1519
- const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
1520
- cause: err
1521
- });
1522
- this._downInternal([[ERROR, wrapped]]);
1523
- return;
1832
+ inspector({ kind: "emit", node: this, messages: finalMessages });
1833
+ } catch {
1524
1834
  }
1525
1835
  }
1526
- if (messageTier(t) < 1) continue;
1527
- if (t === DIRTY) {
1528
- const wasEmpty = this._depDirtyBits.size === 0;
1529
- this._depDirtyBits.add(index);
1530
- this._depSettledBits.delete(index);
1531
- if (wasEmpty) {
1532
- this._downInternal([[DIRTY]]);
1836
+ }
1837
+ if (finalMessages.length > 0) {
1838
+ if (this._paused && this._pausable === "resumeAll" && this._pauseBuffer != null) {
1839
+ const tierOf = this._config.tierOf;
1840
+ const immediate = [];
1841
+ for (const m of finalMessages) {
1842
+ const tier = tierOf(m[0]);
1843
+ if (tier < 3 || tier === 5) {
1844
+ immediate.push(m);
1845
+ } else {
1846
+ this._pauseBuffer.push(m);
1847
+ }
1533
1848
  }
1534
- continue;
1849
+ if (immediate.length > 0) {
1850
+ this._dispatchOrAccumulate(immediate);
1851
+ }
1852
+ } else {
1853
+ this._dispatchOrAccumulate(finalMessages);
1535
1854
  }
1536
- if (t === DATA || t === RESOLVED) {
1537
- if (!this._depDirtyBits.has(index)) {
1538
- const wasEmpty = this._depDirtyBits.size === 0;
1539
- this._depDirtyBits.add(index);
1540
- if (wasEmpty) {
1541
- this._downInternal([[DIRTY]]);
1542
- }
1855
+ }
1856
+ if (equalsError != null) {
1857
+ this._emit([[ERROR, equalsError]]);
1858
+ }
1859
+ }
1860
+ /**
1861
+ * @internal Walk an outgoing (already-framed) batch, updating own
1862
+ * cache / status / versioning and running equals substitution on
1863
+ * every tier-3 DATA (§3.5.1). Framing — tier sort and synthetic
1864
+ * DIRTY prefix — has already happened upstream in `_frameBatch`.
1865
+ * This walk trusts the input is in monotone tier order and that the
1866
+ * spec §1.3.1 DIRTY/RESOLVED precedence invariant is already
1867
+ * satisfied by the frame.
1868
+ *
1869
+ * Equals substitution: every DATA payload is compared against the
1870
+ * live `_cached`; when equal, the tuple is rewritten to `[RESOLVED]`
1871
+ * in a per-call copy and cache is not re-advanced. `.cache` remains
1872
+ * coherent with "the last DATA payload this node actually sent
1873
+ * downstream".
1874
+ *
1875
+ * Returns `{ finalMessages, equalsError? }`:
1876
+ * - `finalMessages` — the array to deliver to sinks (may be
1877
+ * `messages` unchanged, a rewritten copy with DATA→RESOLVED
1878
+ * substitutions, or a truncated prefix when equals throws mid-walk).
1879
+ * - `equalsError` — present only when the configured `equals` function
1880
+ * threw on some DATA message. `_emit` delivers the prefix first,
1881
+ * then emits a fresh ERROR batch via a recursive `_emit` call so
1882
+ * subscribers observe `[...walked_prefix, ERROR]` in order.
1883
+ */
1884
+ _updateState(messages) {
1885
+ const tierOf = this._config.tierOf;
1886
+ let rewritten;
1887
+ let equalsError;
1888
+ let abortedAt = -1;
1889
+ let dataCount = 0;
1890
+ for (const m of messages) {
1891
+ if (tierOf(m[0]) === 3) dataCount++;
1892
+ }
1893
+ const checkEquals = dataCount <= 1;
1894
+ let lastDataIdx = -1;
1895
+ if (this._versioning != null && dataCount > 1) {
1896
+ for (let i = messages.length - 1; i >= 0; i--) {
1897
+ if (messages[i][0] === DATA) {
1898
+ lastDataIdx = i;
1899
+ break;
1543
1900
  }
1544
- this._depSettledBits.add(index);
1545
- if (this._allDirtySettled()) {
1546
- this._depDirtyBits.clear();
1547
- this._depSettledBits.clear();
1548
- if (!this._running) {
1549
- if (this._depValuesDifferFromTracked()) {
1550
- this._runFn();
1901
+ }
1902
+ }
1903
+ for (let i = 0; i < messages.length; i++) {
1904
+ const m = messages[i];
1905
+ const t = m[0];
1906
+ if (t === DATA) {
1907
+ if (m.length >= 2) {
1908
+ let unchanged = false;
1909
+ if (checkEquals && this._cached !== void 0) {
1910
+ try {
1911
+ unchanged = this._equals(this._cached, m[1]);
1912
+ } catch (err) {
1913
+ equalsError = this._wrapFnError("equals threw", err);
1914
+ abortedAt = i;
1915
+ break;
1916
+ }
1917
+ }
1918
+ if (unchanged) {
1919
+ if (rewritten == null) rewritten = messages.slice(0, i);
1920
+ rewritten.push(RESOLVED_MSG);
1921
+ this._status = "resolved";
1922
+ continue;
1923
+ }
1924
+ this._cached = m[1];
1925
+ if (this._versioning != null) {
1926
+ if (lastDataIdx < 0 || i === lastDataIdx) {
1927
+ advanceVersion(this._versioning, m[1], this._hashFn);
1551
1928
  }
1552
1929
  }
1553
1930
  }
1554
- continue;
1555
- }
1556
- if (t === COMPLETE) {
1557
- this._depCompleteBits.add(index);
1558
- this._depDirtyBits.delete(index);
1559
- this._depSettledBits.delete(index);
1560
- if (this._allDirtySettled()) {
1561
- this._depDirtyBits.clear();
1562
- this._depSettledBits.clear();
1563
- if (!this._running) this._runFn();
1564
- }
1565
- if (this._autoComplete && this._depCompleteBits.size >= this._deps.length && this._deps.length > 0) {
1566
- this._downInternal([[COMPLETE]]);
1931
+ this._status = "settled";
1932
+ if (rewritten != null) rewritten.push(m);
1933
+ } else {
1934
+ if (rewritten != null) rewritten.push(m);
1935
+ if (t === DIRTY) {
1936
+ this._status = "dirty";
1937
+ } else if (t === RESOLVED) {
1938
+ this._status = "resolved";
1939
+ } else if (t === COMPLETE) {
1940
+ this._status = "completed";
1941
+ } else if (t === ERROR) {
1942
+ this._status = "errored";
1943
+ } else if (t === INVALIDATE) {
1944
+ this._cached = void 0;
1945
+ this._status = "dirty";
1946
+ const c = this._cleanup;
1947
+ if (typeof c === "function") {
1948
+ this._cleanup = void 0;
1949
+ try {
1950
+ c();
1951
+ } catch {
1952
+ }
1953
+ }
1954
+ } else if (t === TEARDOWN) {
1955
+ if (this._resetOnTeardown) this._cached = void 0;
1956
+ this._deactivate(
1957
+ /* skipStatusUpdate */
1958
+ true
1959
+ );
1960
+ this._status = "sentinel";
1567
1961
  }
1568
- continue;
1569
1962
  }
1570
- if (t === ERROR) {
1571
- this._downInternal([msg]);
1572
- continue;
1573
- }
1574
- if (t === INVALIDATE || t === TEARDOWN || t === PAUSE || t === RESUME) {
1575
- this._downInternal([msg]);
1576
- continue;
1577
- }
1578
- this._downInternal([msg]);
1579
1963
  }
1964
+ const base = abortedAt >= 0 ? rewritten ?? messages.slice(0, abortedAt) : rewritten ?? messages;
1965
+ return equalsError != null ? { finalMessages: base, equalsError } : { finalMessages: base };
1580
1966
  }
1967
+ _deliverToSinks = (messages) => {
1968
+ if (this._sinks == null) return;
1969
+ if (typeof this._sinks === "function") {
1970
+ this._sinks(messages);
1971
+ return;
1972
+ }
1973
+ const snapshot = [...this._sinks];
1974
+ for (const sink of snapshot) sink(messages);
1975
+ };
1581
1976
  /**
1582
- * Update dep masks for a message without triggering `_runFn` — used
1583
- * during post-rewire drain so the wave state is consistent with the
1584
- * buffered activation cascade without recursing.
1977
+ * @internal Dispatch entry point that respects the per-batch emit
1978
+ * accumulator (Bug 2). Inside an explicit `batch()` scope, append to
1979
+ * `_batchPendingMessages` and register a flush hook on first append.
1980
+ * Outside batch — or during a drain (where `flushInProgress` is true
1981
+ * but `batchDepth` is 0) — dispatch synchronously through `downWithBatch`.
1982
+ *
1983
+ * Per-emit state updates (`_frameBatch`, `_updateState`) have already
1984
+ * happened by the time we reach here; only the **downstream delivery**
1985
+ * is coalesced. Cache, version, and status are visible mid-batch on
1986
+ * the emitting node itself.
1585
1987
  */
1586
- _updateMasksForMessage(index, msg) {
1587
- const t = msg[0];
1588
- if (t === DIRTY) {
1589
- this._depDirtyBits.add(index);
1590
- this._depSettledBits.delete(index);
1591
- } else if (t === DATA || t === RESOLVED) {
1592
- this._depDirtyBits.add(index);
1593
- this._depSettledBits.add(index);
1594
- } else if (t === COMPLETE) {
1595
- this._depCompleteBits.add(index);
1596
- this._depDirtyBits.delete(index);
1597
- this._depSettledBits.delete(index);
1598
- }
1599
- }
1600
- _allDirtySettled() {
1601
- if (this._depDirtyBits.size === 0) return false;
1602
- for (const idx of this._depDirtyBits) {
1603
- if (!this._depSettledBits.has(idx)) return false;
1988
+ _dispatchOrAccumulate(messages) {
1989
+ if (isExplicitlyBatching()) {
1990
+ if (this._batchPendingMessages === null) {
1991
+ this._batchPendingMessages = [];
1992
+ registerBatchFlushHook(() => this._flushBatchPending());
1993
+ }
1994
+ for (const m of messages) this._batchPendingMessages.push(m);
1995
+ return;
1604
1996
  }
1605
- return true;
1997
+ downWithBatch(this._deliverToSinks, messages, this._config.tierOf);
1606
1998
  }
1607
1999
  /**
1608
- * True if any current dep value differs from what the last `_runFn`
1609
- * saw via `get()`. Used to suppress redundant re-runs when deferred
1610
- * handshake messages arrive after `_rewire` for a dep whose value
1611
- * already matches `_trackedValues`.
2000
+ * @internal Flushes the accumulated batch through `downWithBatch` and
2001
+ * clears the pending state. Idempotent safe to call when pending is
2002
+ * already null or empty (e.g. on a `batch()` throw, where the hook
2003
+ * fires for cleanup but the drainPhase queues are wiped after).
2004
+ *
2005
+ * Critical: the accumulated batch is interleaved per-emit framings like
2006
+ * `[DIRTY, DATA(1), DIRTY, DATA(2)]` — non-monotone tier order. We must
2007
+ * re-frame to sort by tier before handing to `downWithBatch`, which
2008
+ * assumes pre-sorted input. `_frameBatch` also handles the synthetic
2009
+ * DIRTY prepend rule (no-op here — `hasDirty` is true since each
2010
+ * accumulated emit already carries its own DIRTY prefix).
1612
2011
  */
1613
- _depValuesDifferFromTracked() {
1614
- for (const dep of this._deps) {
1615
- const current = dep.get();
1616
- const tracked = this._trackedValues.get(dep);
1617
- if (!this._equals(current, tracked)) return true;
1618
- }
1619
- return false;
2012
+ _flushBatchPending() {
2013
+ const pending = this._batchPendingMessages;
2014
+ if (pending === null) return;
2015
+ this._batchPendingMessages = null;
2016
+ if (pending.length === 0) return;
2017
+ const framed = this._frameBatch(pending);
2018
+ downWithBatch(this._deliverToSinks, framed, this._config.tierOf);
1620
2019
  }
1621
2020
  };
2021
+ var isNodeArray = (value) => Array.isArray(value);
2022
+ var isNodeOptionsObject = (value) => typeof value === "object" && value != null && !Array.isArray(value);
2023
+ function node(depsOrFn, fnOrOpts, optsArg) {
2024
+ const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
2025
+ const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
2026
+ let opts = {};
2027
+ if (isNodeArray(depsOrFn)) {
2028
+ opts = (isNodeOptionsObject(fnOrOpts) ? fnOrOpts : optsArg) ?? {};
2029
+ } else if (isNodeOptionsObject(depsOrFn)) {
2030
+ opts = depsOrFn;
2031
+ } else {
2032
+ opts = (isNodeOptionsObject(fnOrOpts) ? fnOrOpts : optsArg) ?? {};
2033
+ }
2034
+ return new NodeImpl(deps, fn, opts);
2035
+ }
1622
2036
 
1623
2037
  // src/core/meta.ts
1624
2038
  function resolveDescribeFields(detail, fields) {
@@ -1628,106 +2042,181 @@ function resolveDescribeFields(detail, fields) {
1628
2042
  return /* @__PURE__ */ new Set(["type", "status", "value", "deps", "meta", "v"]);
1629
2043
  case "full":
1630
2044
  return null;
1631
- // null = include everything
1632
2045
  default:
1633
2046
  return /* @__PURE__ */ new Set(["type", "deps"]);
1634
2047
  }
1635
2048
  }
1636
2049
 
1637
2050
  // src/core/sugar.ts
2051
+ function sentinelGuard(batchData, ctx, allowPartial) {
2052
+ if (allowPartial) return false;
2053
+ return batchData.some(
2054
+ (batch2, i) => !(batch2 != null && batch2.length > 0) && ctx.prevData[i] === void 0
2055
+ );
2056
+ }
1638
2057
  function state(initial, opts) {
1639
2058
  return node([], { ...opts, initial });
1640
2059
  }
1641
2060
  function producer(fn, opts) {
1642
- return node(fn, { describeKind: "producer", ...opts });
2061
+ const wrapped = (_data, actions, ctx) => fn(actions, ctx) ?? void 0;
2062
+ return node(wrapped, { describeKind: "producer", ...opts });
1643
2063
  }
1644
2064
  function derived(deps, fn, opts) {
1645
- return node(deps, fn, { describeKind: "derived", ...opts });
2065
+ const allowPartial = opts?.partial ?? false;
2066
+ const wrapped = (batchData, actions, ctx) => {
2067
+ if (sentinelGuard(batchData, ctx, allowPartial)) {
2068
+ actions.down([[RESOLVED]]);
2069
+ return void 0;
2070
+ }
2071
+ const data = batchData.map(
2072
+ (batch2, i) => batch2 != null && batch2.length > 0 ? batch2.at(-1) : ctx.prevData[i]
2073
+ );
2074
+ actions.emit(fn(data, ctx));
2075
+ return void 0;
2076
+ };
2077
+ return node(deps, wrapped, { describeKind: "derived", ...opts });
1646
2078
  }
1647
2079
  function effect(deps, fn, opts) {
1648
- return node(deps, fn, { describeKind: "effect", ...opts });
2080
+ const allowPartial = opts?.partial ?? false;
2081
+ const wrapped = (batchData, actions, ctx) => {
2082
+ if (sentinelGuard(batchData, ctx, allowPartial)) {
2083
+ actions.down([[RESOLVED]]);
2084
+ return void 0;
2085
+ }
2086
+ const data = batchData.map(
2087
+ (batch2, i) => batch2 != null && batch2.length > 0 ? batch2.at(-1) : ctx.prevData[i]
2088
+ );
2089
+ return fn(data, actions, ctx) ?? void 0;
2090
+ };
2091
+ return node(deps, wrapped, { describeKind: "effect", ...opts });
2092
+ }
2093
+ function dynamicNode(allDeps, fn, opts) {
2094
+ const depIndex = /* @__PURE__ */ new Map();
2095
+ allDeps.forEach((d, i) => {
2096
+ depIndex.set(d, i);
2097
+ });
2098
+ return derived(
2099
+ allDeps,
2100
+ // data[i] is already sugar-unwrapped to a scalar by derived()'s wrapper.
2101
+ (data, ctx) => {
2102
+ const track = (dep) => {
2103
+ const i = depIndex.get(dep);
2104
+ if (i == null) {
2105
+ throw new Error(`dynamicNode: untracked dep "${dep.name ?? "<unnamed>"}"`);
2106
+ }
2107
+ return data[i];
2108
+ };
2109
+ return fn(track, ctx);
2110
+ },
2111
+ opts
2112
+ );
2113
+ }
2114
+ function autoTrackNode(fn, opts) {
2115
+ let implRef;
2116
+ const depIndexMap = /* @__PURE__ */ new Map();
2117
+ const allowPartial = opts?.partial ?? false;
2118
+ const wrappedFn = (batchData, actions, ctx) => {
2119
+ let foundNew = false;
2120
+ const track = (dep) => {
2121
+ const idx = depIndexMap.get(dep);
2122
+ if (idx !== void 0) {
2123
+ if (idx < batchData.length) {
2124
+ const batch2 = batchData[idx];
2125
+ if (batch2 != null && batch2.length > 0) return batch2.at(-1);
2126
+ return ctx.prevData[idx];
2127
+ }
2128
+ return dep.cache;
2129
+ }
2130
+ foundNew = true;
2131
+ const newIdx = implRef._addDep(dep);
2132
+ depIndexMap.set(dep, newIdx);
2133
+ return dep.cache;
2134
+ };
2135
+ if (!allowPartial && depIndexMap.size > 0) {
2136
+ for (const [, idx] of depIndexMap) {
2137
+ if (idx < batchData.length) {
2138
+ const batch2 = batchData[idx];
2139
+ if (!(batch2 != null && batch2.length > 0) && ctx.prevData[idx] === void 0) {
2140
+ actions.down([[RESOLVED]]);
2141
+ return void 0;
2142
+ }
2143
+ }
2144
+ }
2145
+ }
2146
+ try {
2147
+ const result = fn(track, ctx);
2148
+ if (!foundNew) {
2149
+ actions.emit(result);
2150
+ if (ctx.store.__autoTrackLastDiscoveryError != null) {
2151
+ delete ctx.store.__autoTrackLastDiscoveryError;
2152
+ }
2153
+ }
2154
+ } catch (err) {
2155
+ if (!foundNew) throw err;
2156
+ ctx.store.__autoTrackLastDiscoveryError = err;
2157
+ }
2158
+ return void 0;
2159
+ };
2160
+ implRef = new NodeImpl([], wrappedFn, {
2161
+ describeKind: "derived",
2162
+ ...opts
2163
+ });
2164
+ return implRef;
1649
2165
  }
1650
2166
  function pipe(source, ...ops) {
1651
2167
  let current = source;
1652
- for (const op of ops) {
1653
- current = op(current);
1654
- }
2168
+ for (const op of ops) current = op(current);
1655
2169
  return current;
1656
2170
  }
1657
-
1658
- // src/core/timer.ts
1659
- var ResettableTimer = class {
1660
- _timer;
1661
- _gen = 0;
1662
- /** Schedule callback after delayMs. Cancels any pending timer. */
1663
- start(delayMs, callback) {
1664
- this.cancel();
1665
- this._gen += 1;
1666
- const gen = this._gen;
1667
- this._timer = setTimeout(() => {
1668
- this._timer = void 0;
1669
- if (gen !== this._gen) return;
1670
- callback();
1671
- }, delayMs);
1672
- }
1673
- /** Cancel the pending timer (if any). */
1674
- cancel() {
1675
- if (this._timer !== void 0) {
1676
- clearTimeout(this._timer);
1677
- this._timer = void 0;
1678
- }
1679
- }
1680
- /** Whether a timer is currently pending. */
1681
- get pending() {
1682
- return this._timer !== void 0;
1683
- }
1684
- };
1685
2171
  // Annotate the CommonJS export names for ESM import in node:
1686
2172
  0 && (module.exports = {
1687
- CLEANUP_RESULT,
1688
2173
  COMPLETE,
2174
+ COMPLETE_MSG,
2175
+ COMPLETE_ONLY_BATCH,
1689
2176
  DATA,
1690
2177
  DEFAULT_ACTOR,
1691
- DEFAULT_DOWN,
1692
2178
  DIRTY,
1693
- DynamicNodeImpl,
2179
+ DIRTY_MSG,
2180
+ DIRTY_ONLY_BATCH,
1694
2181
  ERROR,
2182
+ GraphReFlyConfig,
1695
2183
  GuardDenied,
1696
2184
  INVALIDATE,
2185
+ INVALIDATE_MSG,
2186
+ INVALIDATE_ONLY_BATCH,
2187
+ NodeImpl,
1697
2188
  PAUSE,
1698
2189
  RESOLVED,
2190
+ RESOLVED_MSG,
2191
+ RESOLVED_ONLY_BATCH,
1699
2192
  RESUME,
1700
- ResettableTimer,
1701
2193
  START,
2194
+ START_MSG,
1702
2195
  TEARDOWN,
2196
+ TEARDOWN_MSG,
2197
+ TEARDOWN_ONLY_BATCH,
1703
2198
  accessHintForGuard,
1704
2199
  advanceVersion,
2200
+ autoTrackNode,
1705
2201
  batch,
1706
- bridge,
1707
- cleanupResult,
2202
+ configure,
1708
2203
  createVersioning,
2204
+ defaultConfig,
1709
2205
  defaultHash,
1710
2206
  derived,
1711
2207
  downWithBatch,
1712
2208
  dynamicNode,
1713
2209
  effect,
1714
2210
  isBatching,
1715
- isKnownMessageType,
1716
- isLocalOnly,
1717
- isPhase2Message,
1718
- isTerminalMessage,
1719
2211
  isV1,
1720
- knownMessageTypes,
1721
- messageTier,
1722
2212
  monotonicNs,
1723
2213
  node,
1724
2214
  normalizeActor,
1725
- partitionForBatch,
1726
2215
  pipe,
1727
2216
  policy,
1728
2217
  policyFromRules,
1729
2218
  producer,
1730
- propagatesToMeta,
2219
+ registerBuiltins,
1731
2220
  resolveDescribeFields,
1732
2221
  state,
1733
2222
  wallClockNs