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