@graphrefly/graphrefly 0.17.0 → 0.19.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 (77) hide show
  1. package/dist/{chunk-R6OHUUYB.js → chunk-AHRKWMNI.js} +7 -7
  2. package/dist/chunk-AHRKWMNI.js.map +1 -0
  3. package/dist/{chunk-2PORF4RP.js → chunk-BER7UYLM.js} +27 -32
  4. package/dist/chunk-BER7UYLM.js.map +1 -0
  5. package/dist/{chunk-646OG3PO.js → chunk-IRZAGZUB.js} +51 -52
  6. package/dist/chunk-IRZAGZUB.js.map +1 -0
  7. package/dist/{chunk-IHJHBADD.js → chunk-JC2SN46B.js} +385 -197
  8. package/dist/chunk-JC2SN46B.js.map +1 -0
  9. package/dist/{chunk-XJ6EMQ22.js → chunk-OO5QOAXI.js} +4 -10
  10. package/dist/chunk-OO5QOAXI.js.map +1 -0
  11. package/dist/{chunk-YXROQFXZ.js → chunk-UW77D7SP.js} +3 -3
  12. package/dist/{chunk-F2ULI3Q3.js → chunk-XUOY3YKN.js} +7 -3
  13. package/dist/chunk-XUOY3YKN.js.map +1 -0
  14. package/dist/chunk-YLR5JUJZ.js +111 -0
  15. package/dist/chunk-YLR5JUJZ.js.map +1 -0
  16. package/dist/{chunk-BV3TPSBK.js → chunk-YXR3WW3Q.js} +740 -755
  17. package/dist/chunk-YXR3WW3Q.js.map +1 -0
  18. package/dist/compat/nestjs/index.cjs +1127 -983
  19. package/dist/compat/nestjs/index.cjs.map +1 -1
  20. package/dist/compat/nestjs/index.d.cts +4 -4
  21. package/dist/compat/nestjs/index.d.ts +4 -4
  22. package/dist/compat/nestjs/index.js +7 -13
  23. package/dist/core/index.cjs +653 -749
  24. package/dist/core/index.cjs.map +1 -1
  25. package/dist/core/index.d.cts +2 -2
  26. package/dist/core/index.d.ts +2 -2
  27. package/dist/core/index.js +7 -7
  28. package/dist/extra/index.cjs +773 -795
  29. package/dist/extra/index.cjs.map +1 -1
  30. package/dist/extra/index.d.cts +4 -4
  31. package/dist/extra/index.d.ts +4 -4
  32. package/dist/extra/index.js +5 -11
  33. package/dist/graph/index.cjs +1036 -975
  34. package/dist/graph/index.cjs.map +1 -1
  35. package/dist/graph/index.d.cts +3 -3
  36. package/dist/graph/index.d.ts +3 -3
  37. package/dist/graph/index.js +8 -8
  38. package/dist/{graph-fCsaaVIa.d.cts → graph-KsTe57nI.d.cts} +127 -51
  39. package/dist/{graph-Dc-P9BVm.d.ts → graph-mILUUqW8.d.ts} +127 -51
  40. package/dist/{index-DhXznWyH.d.ts → index-8a605sg9.d.ts} +2 -2
  41. package/dist/{index-D7y9Q8W4.d.ts → index-B2SvPEbc.d.ts} +8 -69
  42. package/dist/{index-YlOH1Gw6.d.cts → index-BBUYZfJ1.d.cts} +122 -78
  43. package/dist/{index-ClaKZFPl.d.cts → index-Bjh5C1Tp.d.cts} +38 -35
  44. package/dist/{index-DWq0P9T6.d.ts → index-BjtlNirP.d.cts} +5 -7
  45. package/dist/{index-N704txAA.d.ts → index-BnkMgNNa.d.ts} +38 -35
  46. package/dist/{index-BBVBYPxr.d.cts → index-CgSiUouz.d.ts} +5 -7
  47. package/dist/{index-BmoUvOGN.d.ts → index-CvKzv0AW.d.ts} +122 -78
  48. package/dist/{index-4OIX-q0C.d.cts → index-UudxGnzc.d.cts} +8 -69
  49. package/dist/{index-DlGMf_Qe.d.cts → index-VHA43cGP.d.cts} +2 -2
  50. package/dist/index.cjs +6146 -5725
  51. package/dist/index.cjs.map +1 -1
  52. package/dist/index.d.cts +617 -383
  53. package/dist/index.d.ts +617 -383
  54. package/dist/index.js +4401 -4028
  55. package/dist/index.js.map +1 -1
  56. package/dist/{meta-BV4pj9ML.d.cts → meta-BnG7XAaE.d.cts} +395 -289
  57. package/dist/{meta-BV4pj9ML.d.ts → meta-BnG7XAaE.d.ts} +395 -289
  58. package/dist/observable-C8Kx_O6k.d.cts +36 -0
  59. package/dist/observable-DcBwQY7t.d.ts +36 -0
  60. package/dist/patterns/reactive-layout/index.cjs +1037 -857
  61. package/dist/patterns/reactive-layout/index.cjs.map +1 -1
  62. package/dist/patterns/reactive-layout/index.d.cts +3 -3
  63. package/dist/patterns/reactive-layout/index.d.ts +3 -3
  64. package/dist/patterns/reactive-layout/index.js +4 -4
  65. package/package.json +1 -1
  66. package/dist/chunk-2PORF4RP.js.map +0 -1
  67. package/dist/chunk-646OG3PO.js.map +0 -1
  68. package/dist/chunk-BV3TPSBK.js.map +0 -1
  69. package/dist/chunk-EBNKJULL.js +0 -231
  70. package/dist/chunk-EBNKJULL.js.map +0 -1
  71. package/dist/chunk-F2ULI3Q3.js.map +0 -1
  72. package/dist/chunk-IHJHBADD.js.map +0 -1
  73. package/dist/chunk-R6OHUUYB.js.map +0 -1
  74. package/dist/chunk-XJ6EMQ22.js.map +0 -1
  75. package/dist/observable-Cz-AWhwR.d.cts +0 -42
  76. package/dist/observable-DCqlwGyl.d.ts +0 -42
  77. /package/dist/{chunk-YXROQFXZ.js.map → chunk-UW77D7SP.js.map} +0 -0
@@ -68,7 +68,106 @@ function normalizeActor(actor) {
68
68
  };
69
69
  }
70
70
 
71
+ // src/core/guard.ts
72
+ var GuardDenied = class extends Error {
73
+ actor;
74
+ action;
75
+ nodeName;
76
+ /**
77
+ * @param details - Actor, action, and optional node name for the denial.
78
+ * @param message - Optional override for the default error message.
79
+ */
80
+ constructor(details, message) {
81
+ super(
82
+ message ?? `GuardDenied: action "${String(details.action)}" denied for actor type "${String(details.actor.type)}"`
83
+ );
84
+ this.name = "GuardDenied";
85
+ this.actor = details.actor;
86
+ this.action = details.action;
87
+ this.nodeName = details.nodeName;
88
+ }
89
+ /** Qualified registry path when known (roadmap diagnostics: same as {@link nodeName}). */
90
+ get node() {
91
+ return this.nodeName;
92
+ }
93
+ };
94
+ function normalizeActions(action) {
95
+ if (Array.isArray(action)) {
96
+ return [...action];
97
+ }
98
+ return [action];
99
+ }
100
+ function matchesActions(set, action) {
101
+ return set.has(action) || set.has("*");
102
+ }
103
+ function policy(build) {
104
+ const rules = [];
105
+ const allow = (action, opts) => {
106
+ rules.push({
107
+ kind: "allow",
108
+ actions: new Set(normalizeActions(action)),
109
+ where: opts?.where ?? (() => true)
110
+ });
111
+ };
112
+ const deny = (action, opts) => {
113
+ rules.push({
114
+ kind: "deny",
115
+ actions: new Set(normalizeActions(action)),
116
+ where: opts?.where ?? (() => true)
117
+ });
118
+ };
119
+ build(allow, deny);
120
+ return (actor, action) => {
121
+ let denied = false;
122
+ let allowed = false;
123
+ for (const r of rules) {
124
+ if (!matchesActions(r.actions, action)) continue;
125
+ if (!r.where(actor)) continue;
126
+ if (r.kind === "deny") {
127
+ denied = true;
128
+ } else {
129
+ allowed = true;
130
+ }
131
+ }
132
+ if (denied) return false;
133
+ return allowed;
134
+ };
135
+ }
136
+ function policyFromRules(rules) {
137
+ return policy((allow, deny) => {
138
+ for (const rule of rules) {
139
+ const actorTypes = rule.actorType == null ? null : new Set(Array.isArray(rule.actorType) ? rule.actorType : [rule.actorType]);
140
+ const actorIds = rule.actorId == null ? null : new Set(Array.isArray(rule.actorId) ? rule.actorId : [rule.actorId]);
141
+ const claimEntries = Object.entries(rule.claims ?? {});
142
+ const where = (actor) => {
143
+ if (actorTypes !== null && !actorTypes.has(String(actor.type))) return false;
144
+ if (actorIds !== null && !actorIds.has(String(actor.id ?? ""))) return false;
145
+ for (const [key, value] of claimEntries) {
146
+ if (actor[key] !== value) return false;
147
+ }
148
+ return true;
149
+ };
150
+ if (rule.effect === "deny") {
151
+ deny(rule.action, { where });
152
+ } else {
153
+ allow(rule.action, { where });
154
+ }
155
+ }
156
+ });
157
+ }
158
+ var STANDARD_WRITE_TYPES = ["human", "llm", "wallet", "system"];
159
+ function accessHintForGuard(guard) {
160
+ const allowed = STANDARD_WRITE_TYPES.filter((t) => guard({ type: t, id: "" }, "write"));
161
+ if (allowed.length === 0) return "restricted";
162
+ if (allowed.includes("human") && allowed.includes("llm") && allowed.every((t) => t === "human" || t === "llm" || t === "system")) {
163
+ return "both";
164
+ }
165
+ if (allowed.length === 1) return allowed[0];
166
+ return allowed.join("+");
167
+ }
168
+
71
169
  // src/core/messages.ts
170
+ var START = /* @__PURE__ */ Symbol.for("graphrefly/START");
72
171
  var DATA = /* @__PURE__ */ Symbol.for("graphrefly/DATA");
73
172
  var DIRTY = /* @__PURE__ */ Symbol.for("graphrefly/DIRTY");
74
173
  var RESOLVED = /* @__PURE__ */ Symbol.for("graphrefly/RESOLVED");
@@ -79,6 +178,7 @@ var TEARDOWN = /* @__PURE__ */ Symbol.for("graphrefly/TEARDOWN");
79
178
  var COMPLETE = /* @__PURE__ */ Symbol.for("graphrefly/COMPLETE");
80
179
  var ERROR = /* @__PURE__ */ Symbol.for("graphrefly/ERROR");
81
180
  var knownMessageTypes = [
181
+ START,
82
182
  DATA,
83
183
  DIRTY,
84
184
  RESOLVED,
@@ -89,16 +189,18 @@ var knownMessageTypes = [
89
189
  COMPLETE,
90
190
  ERROR
91
191
  ];
192
+ var knownMessageSet = new Set(knownMessageTypes);
92
193
  function isKnownMessageType(t) {
93
- return knownMessageTypes.includes(t);
194
+ return knownMessageSet.has(t);
94
195
  }
95
196
  function messageTier(t) {
96
- if (t === DIRTY || t === INVALIDATE) return 0;
97
- if (t === PAUSE || t === RESUME) return 1;
98
- if (t === DATA || t === RESOLVED) return 2;
99
- if (t === COMPLETE || t === ERROR) return 3;
100
- if (t === TEARDOWN) return 4;
101
- return 0;
197
+ if (t === START) return 0;
198
+ if (t === DIRTY || t === INVALIDATE) return 1;
199
+ if (t === PAUSE || t === RESUME) return 2;
200
+ if (t === DATA || t === RESOLVED) return 3;
201
+ if (t === COMPLETE || t === ERROR) return 4;
202
+ if (t === TEARDOWN) return 5;
203
+ return 1;
102
204
  }
103
205
  function isPhase2Message(msg) {
104
206
  const t = msg[0];
@@ -107,6 +209,10 @@ function isPhase2Message(msg) {
107
209
  function isTerminalMessage(t) {
108
210
  return t === COMPLETE || t === ERROR;
109
211
  }
212
+ function isLocalOnly(t) {
213
+ if (!knownMessageSet.has(t)) return false;
214
+ return messageTier(t) < 3;
215
+ }
110
216
  function propagatesToMeta(t) {
111
217
  return t === TEARDOWN;
112
218
  }
@@ -267,14 +373,14 @@ function _downSequential(sink, messages, phase = 2) {
267
373
  const dataQueue = phase === 3 ? pendingPhase3 : pendingPhase2;
268
374
  for (const msg of messages) {
269
375
  const tier = messageTier(msg[0]);
270
- if (tier === 2) {
376
+ if (tier === 3) {
271
377
  if (isBatching()) {
272
378
  const m = msg;
273
379
  dataQueue.push(() => sink([m]));
274
380
  } else {
275
381
  sink([msg]);
276
382
  }
277
- } else if (tier >= 3) {
383
+ } else if (tier >= 4) {
278
384
  if (isBatching()) {
279
385
  const m = msg;
280
386
  pendingPhase3.push(() => sink([m]));
@@ -295,104 +401,6 @@ function wallClockNs() {
295
401
  return Date.now() * 1e6;
296
402
  }
297
403
 
298
- // src/core/guard.ts
299
- var GuardDenied = class extends Error {
300
- actor;
301
- action;
302
- nodeName;
303
- /**
304
- * @param details - Actor, action, and optional node name for the denial.
305
- * @param message - Optional override for the default error message.
306
- */
307
- constructor(details, message) {
308
- super(
309
- message ?? `GuardDenied: action "${String(details.action)}" denied for actor type "${String(details.actor.type)}"`
310
- );
311
- this.name = "GuardDenied";
312
- this.actor = details.actor;
313
- this.action = details.action;
314
- this.nodeName = details.nodeName;
315
- }
316
- /** Qualified registry path when known (roadmap diagnostics: same as {@link nodeName}). */
317
- get node() {
318
- return this.nodeName;
319
- }
320
- };
321
- function normalizeActions(action) {
322
- if (Array.isArray(action)) {
323
- return [...action];
324
- }
325
- return [action];
326
- }
327
- function matchesActions(set, action) {
328
- return set.has(action) || set.has("*");
329
- }
330
- function policy(build) {
331
- const rules = [];
332
- const allow = (action, opts) => {
333
- rules.push({
334
- kind: "allow",
335
- actions: new Set(normalizeActions(action)),
336
- where: opts?.where ?? (() => true)
337
- });
338
- };
339
- const deny = (action, opts) => {
340
- rules.push({
341
- kind: "deny",
342
- actions: new Set(normalizeActions(action)),
343
- where: opts?.where ?? (() => true)
344
- });
345
- };
346
- build(allow, deny);
347
- return (actor, action) => {
348
- let denied = false;
349
- let allowed = false;
350
- for (const r of rules) {
351
- if (!matchesActions(r.actions, action)) continue;
352
- if (!r.where(actor)) continue;
353
- if (r.kind === "deny") {
354
- denied = true;
355
- } else {
356
- allowed = true;
357
- }
358
- }
359
- if (denied) return false;
360
- return allowed;
361
- };
362
- }
363
- function policyFromRules(rules) {
364
- return policy((allow, deny) => {
365
- for (const rule of rules) {
366
- const actorTypes = rule.actorType == null ? null : new Set(Array.isArray(rule.actorType) ? rule.actorType : [rule.actorType]);
367
- const actorIds = rule.actorId == null ? null : new Set(Array.isArray(rule.actorId) ? rule.actorId : [rule.actorId]);
368
- const claimEntries = Object.entries(rule.claims ?? {});
369
- const where = (actor) => {
370
- if (actorTypes !== null && !actorTypes.has(String(actor.type))) return false;
371
- if (actorIds !== null && !actorIds.has(String(actor.id ?? ""))) return false;
372
- for (const [key, value] of claimEntries) {
373
- if (actor[key] !== value) return false;
374
- }
375
- return true;
376
- };
377
- if (rule.effect === "deny") {
378
- deny(rule.action, { where });
379
- } else {
380
- allow(rule.action, { where });
381
- }
382
- }
383
- });
384
- }
385
- var STANDARD_WRITE_TYPES = ["human", "llm", "wallet", "system"];
386
- function accessHintForGuard(guard) {
387
- const allowed = STANDARD_WRITE_TYPES.filter((t) => guard({ type: t, id: "" }, "write"));
388
- if (allowed.length === 0) return "restricted";
389
- if (allowed.includes("human") && allowed.includes("llm") && allowed.every((t) => t === "human" || t === "llm" || t === "system")) {
390
- return "both";
391
- }
392
- if (allowed.length === 1) return allowed[0];
393
- return allowed.join("+");
394
- }
395
-
396
404
  // src/core/versioning.ts
397
405
  import { createHash, randomUUID } from "crypto";
398
406
  function canonicalizeForHash(value) {
@@ -448,10 +456,29 @@ function isV1(info) {
448
456
  return "cid" in info;
449
457
  }
450
458
 
451
- // src/core/node.ts
459
+ // src/core/node-base.ts
452
460
  var NO_VALUE = /* @__PURE__ */ Symbol.for("graphrefly/NO_VALUE");
453
461
  var CLEANUP_RESULT = /* @__PURE__ */ Symbol.for("graphrefly/CLEANUP_RESULT");
454
- function createIntBitSet() {
462
+ function cleanupResult(cleanup, ...args) {
463
+ const r = { [CLEANUP_RESULT]: true, cleanup };
464
+ if (args.length > 0) r.value = args[0];
465
+ return r;
466
+ }
467
+ var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
468
+ var isCleanupFn = (value) => typeof value === "function";
469
+ function statusAfterMessage(status, msg) {
470
+ const t = msg[0];
471
+ if (t === DIRTY) return "dirty";
472
+ if (t === DATA) return "settled";
473
+ if (t === RESOLVED) return "resolved";
474
+ if (t === COMPLETE) return "completed";
475
+ if (t === ERROR) return "errored";
476
+ if (t === INVALIDATE) return "dirty";
477
+ if (t === TEARDOWN) return "disconnected";
478
+ return status;
479
+ }
480
+ function createIntBitSet(size) {
481
+ const fullMask = size >= 32 ? -1 : ~(-1 << size);
455
482
  let bits = 0;
456
483
  return {
457
484
  set(i) {
@@ -464,7 +491,8 @@ function createIntBitSet() {
464
491
  return (bits & 1 << i) !== 0;
465
492
  },
466
493
  covers(other) {
467
- return (bits & other._bits()) === other._bits();
494
+ const otherBits = other._bits();
495
+ return (bits & otherBits) === otherBits;
468
496
  },
469
497
  any() {
470
498
  return bits !== 0;
@@ -472,6 +500,9 @@ function createIntBitSet() {
472
500
  reset() {
473
501
  bits = 0;
474
502
  },
503
+ setAll() {
504
+ bits = fullMask;
505
+ },
475
506
  _bits() {
476
507
  return bits;
477
508
  }
@@ -479,6 +510,8 @@ function createIntBitSet() {
479
510
  }
480
511
  function createArrayBitSet(size) {
481
512
  const words = new Uint32Array(Math.ceil(size / 32));
513
+ const lastBits = size % 32;
514
+ const lastWordMask = lastBits === 0 ? 4294967295 : (1 << lastBits) - 1 >>> 0;
482
515
  return {
483
516
  set(i) {
484
517
  words[i >>> 5] |= 1 << (i & 31);
@@ -505,135 +538,103 @@ function createArrayBitSet(size) {
505
538
  reset() {
506
539
  words.fill(0);
507
540
  },
541
+ setAll() {
542
+ for (let w = 0; w < words.length - 1; w++) words[w] = 4294967295;
543
+ if (words.length > 0) words[words.length - 1] = lastWordMask;
544
+ },
508
545
  _words: words
509
546
  };
510
547
  }
511
548
  function createBitSet(size) {
512
- return size <= 31 ? createIntBitSet() : createArrayBitSet(size);
513
- }
514
- var isNodeArray = (value) => Array.isArray(value);
515
- var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
516
- function cleanupResult(cleanup, ...args) {
517
- const r = { [CLEANUP_RESULT]: true, cleanup };
518
- if (args.length > 0) r.value = args[0];
519
- return r;
549
+ return size <= 31 ? createIntBitSet(size) : createArrayBitSet(size);
520
550
  }
521
- var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
522
- var isCleanupFn = (value) => typeof value === "function";
523
- var statusAfterMessage = (status, msg) => {
524
- const t = msg[0];
525
- if (t === DIRTY) return "dirty";
526
- if (t === DATA) return "settled";
527
- if (t === RESOLVED) return "resolved";
528
- if (t === COMPLETE) return "completed";
529
- if (t === ERROR) return "errored";
530
- if (t === INVALIDATE) return "dirty";
531
- if (t === TEARDOWN) return "disconnected";
532
- return status;
533
- };
534
- var NodeImpl = class {
535
- // --- Configuration (set once, never reassigned) ---
551
+ var NodeBase = class {
552
+ // --- Identity (set once) ---
536
553
  _optsName;
537
554
  _registryName;
538
- /** @internal read by {@link describeNode} before inference. */
555
+ /** @internal Read by `describeNode` before inference. */
539
556
  _describeKind;
540
557
  meta;
541
- _deps;
542
- _fn;
543
- _opts;
558
+ // --- Options ---
544
559
  _equals;
560
+ _resubscribable;
561
+ _resetOnTeardown;
562
+ _onResubscribe;
545
563
  _onMessage;
546
- /** @internal read by {@link describeNode} for `accessHintForGuard`. */
564
+ /** @internal Read by `describeNode` for `accessHintForGuard`. */
547
565
  _guard;
566
+ /** @internal Subclasses update this through {@link _recordMutation}. */
548
567
  _lastMutation;
549
- _hasDeps;
550
- _autoComplete;
551
- _isSingleDep;
552
- // --- Mutable state ---
568
+ // --- Versioning ---
569
+ _hashFn;
570
+ _versioning;
571
+ // --- Lifecycle state ---
572
+ /** @internal Read by `describeNode` and `graph.ts`. */
553
573
  _cached;
574
+ /** @internal Read externally via `get status()`. */
554
575
  _status;
555
576
  _terminal = false;
556
- _connected = false;
557
- _producerStarted = false;
558
- _connecting = false;
559
- _manualEmitUsed = false;
577
+ _active = false;
578
+ // --- Sink storage ---
579
+ /** @internal Read by `graph/profile.ts` for subscriber counts. */
560
580
  _sinkCount = 0;
561
581
  _singleDepSinkCount = 0;
562
- // --- Object/collection state ---
563
- _depDirtyMask;
564
- _depSettledMask;
565
- _depCompleteMask;
566
- _allDepsCompleteMask;
567
- _lastDepValues;
568
- _cleanup;
569
- _sinks = null;
570
582
  _singleDepSinks = /* @__PURE__ */ new WeakSet();
571
- _upstreamUnsubs = [];
583
+ _sinks = null;
584
+ // --- Actions + bound helpers ---
572
585
  _actions;
573
586
  _boundDownToSinks;
587
+ // --- Inspector hook (Graph observability) ---
574
588
  _inspectorHook;
575
- _versioning;
576
- _hashFn;
577
- constructor(deps, fn, opts) {
578
- this._deps = deps;
579
- this._fn = fn;
580
- this._opts = opts;
589
+ constructor(opts) {
581
590
  this._optsName = opts.name;
582
591
  this._describeKind = opts.describeKind;
583
592
  this._equals = opts.equals ?? Object.is;
593
+ this._resubscribable = opts.resubscribable ?? false;
594
+ this._resetOnTeardown = opts.resetOnTeardown ?? false;
595
+ this._onResubscribe = opts.onResubscribe;
584
596
  this._onMessage = opts.onMessage;
585
597
  this._guard = opts.guard;
586
- this._hasDeps = deps.length > 0;
587
- this._autoComplete = opts.completeWhenDepsComplete ?? true;
588
- this._isSingleDep = deps.length === 1 && fn != null;
589
598
  this._cached = "initial" in opts ? opts.initial : NO_VALUE;
590
- this._status = this._hasDeps ? "disconnected" : "settled";
599
+ this._status = "disconnected";
591
600
  this._hashFn = opts.versioningHash ?? defaultHash;
592
601
  this._versioning = opts.versioning != null ? createVersioning(opts.versioning, this._cached === NO_VALUE ? void 0 : this._cached, {
593
602
  id: opts.versioningId,
594
603
  hash: this._hashFn
595
604
  }) : void 0;
596
- this._depDirtyMask = createBitSet(deps.length);
597
- this._depSettledMask = createBitSet(deps.length);
598
- this._depCompleteMask = createBitSet(deps.length);
599
- this._allDepsCompleteMask = createBitSet(deps.length);
600
- for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
601
605
  const meta = {};
602
606
  for (const [k, v] of Object.entries(opts.meta ?? {})) {
603
- meta[k] = node({
604
- initial: v,
605
- name: `${opts.name ?? "node"}:meta:${k}`,
606
- describeKind: "state",
607
- ...opts.guard != null ? { guard: opts.guard } : {}
608
- });
607
+ meta[k] = this._createMetaNode(k, v, opts);
609
608
  }
610
609
  Object.freeze(meta);
611
610
  this.meta = meta;
612
611
  const self = this;
613
612
  this._actions = {
614
613
  down(messages) {
615
- self._manualEmitUsed = true;
614
+ self._onManualEmit();
616
615
  self._downInternal(messages);
617
616
  },
618
617
  emit(value) {
619
- self._manualEmitUsed = true;
618
+ self._onManualEmit();
620
619
  self._downAutoValue(value);
621
620
  },
622
621
  up(messages) {
623
622
  self._upInternal(messages);
624
623
  }
625
624
  };
626
- this.down = this.down.bind(this);
627
- this.up = this.up.bind(this);
628
625
  this._boundDownToSinks = this._downToSinks.bind(this);
629
626
  }
627
+ /**
628
+ * Subclass hook invoked by `actions.down` / `actions.emit`. Default no-op;
629
+ * {@link NodeImpl} overrides to set `_manualEmitUsed`.
630
+ */
631
+ _onManualEmit() {
632
+ }
633
+ // --- Identity getters ---
630
634
  get name() {
631
635
  return this._registryName ?? this._optsName;
632
636
  }
633
- /**
634
- * When a node is registered with {@link Graph.add} without an options `name`,
635
- * the graph assigns the registry local name for introspection (parity with graphrefly-py).
636
- */
637
+ /** @internal Assigned by `Graph.add` when registered without an options `name`. */
637
638
  _assignRegistryName(localName) {
638
639
  if (this._optsName !== void 0 || this._registryName !== void 0) return;
639
640
  this._registryName = localName;
@@ -651,7 +652,10 @@ var NodeImpl = class {
651
652
  }
652
653
  };
653
654
  }
654
- // --- Public interface (Node<T>) ---
655
+ /** @internal Used by subclasses to surface inspector events. */
656
+ _emitInspectorHook(event) {
657
+ this._inspectorHook?.(event);
658
+ }
655
659
  get status() {
656
660
  return this._status;
657
661
  }
@@ -661,15 +665,7 @@ var NodeImpl = class {
661
665
  get v() {
662
666
  return this._versioning;
663
667
  }
664
- /**
665
- * Retroactively apply versioning to a node that was created without it.
666
- * No-op if versioning is already enabled.
667
- *
668
- * Version starts at 0 regardless of prior DATA emissions — it tracks
669
- * changes from the moment versioning is enabled, not historical ones.
670
- *
671
- * @internal — used by {@link Graph.setVersioning}.
672
- */
668
+ /** @internal Used by `Graph.setVersioning` to retroactively apply versioning. */
673
669
  _applyVersioning(level, opts) {
674
670
  if (this._versioning != null) return;
675
671
  this._hashFn = opts?.hash ?? this._hashFn;
@@ -689,6 +685,7 @@ var NodeImpl = class {
689
685
  if (this._guard == null) return true;
690
686
  return this._guard(normalizeActor(actor), "observe");
691
687
  }
688
+ // --- Public transport ---
692
689
  get() {
693
690
  return this._cached === NO_VALUE ? void 0 : this._cached;
694
691
  }
@@ -701,43 +698,25 @@ var NodeImpl = class {
701
698
  if (!this._guard(actor, action)) {
702
699
  throw new GuardDenied({ actor, action, nodeName: this.name });
703
700
  }
704
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
701
+ this._recordMutation(actor);
705
702
  }
706
703
  this._downInternal(messages);
707
704
  }
708
- _downInternal(messages) {
709
- if (messages.length === 0) return;
710
- let lifecycleMessages = messages;
711
- let sinkMessages = messages;
712
- if (this._terminal && !this._opts.resubscribable) {
713
- const terminalPassthrough = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
714
- if (terminalPassthrough.length === 0) return;
715
- lifecycleMessages = terminalPassthrough;
716
- sinkMessages = terminalPassthrough;
717
- }
718
- this._handleLocalLifecycle(lifecycleMessages);
719
- if (this._canSkipDirty()) {
720
- let hasPhase2 = false;
721
- for (let i = 0; i < sinkMessages.length; i++) {
722
- const t = sinkMessages[i][0];
723
- if (t === DATA || t === RESOLVED) {
724
- hasPhase2 = true;
725
- break;
726
- }
727
- }
728
- if (hasPhase2) {
729
- const filtered = [];
730
- for (let i = 0; i < sinkMessages.length; i++) {
731
- if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
732
- }
733
- if (filtered.length > 0) {
734
- downWithBatch(this._boundDownToSinks, filtered);
735
- }
736
- return;
737
- }
738
- }
739
- downWithBatch(this._boundDownToSinks, sinkMessages);
705
+ /** @internal Record a successful guarded mutation (called by `down` and subclass `up`). */
706
+ _recordMutation(actor) {
707
+ this._lastMutation = { actor, timestamp_ns: wallClockNs() };
708
+ }
709
+ /**
710
+ * At-most-once deactivation guard. Both TEARDOWN (eager) and
711
+ * unsubscribe-body (lazy) call this. The `_active` flag ensures
712
+ * `_doDeactivate` runs exactly once per activation cycle.
713
+ */
714
+ _onDeactivate() {
715
+ if (!this._active) return;
716
+ this._active = false;
717
+ this._doDeactivate();
740
718
  }
719
+ // --- Subscribe (uniform across node shapes) ---
741
720
  subscribe(sink, hints) {
742
721
  if (hints?.actor != null && this._guard != null) {
743
722
  const actor = normalizeActor(hints.actor);
@@ -745,17 +724,21 @@ var NodeImpl = class {
745
724
  throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
746
725
  }
747
726
  }
748
- if (this._terminal && this._opts.resubscribable) {
727
+ if (this._terminal && this._resubscribable) {
749
728
  this._terminal = false;
750
729
  this._cached = NO_VALUE;
751
- this._status = this._hasDeps ? "disconnected" : "settled";
752
- this._opts.onResubscribe?.();
730
+ this._status = "disconnected";
731
+ this._onResubscribe?.();
753
732
  }
754
733
  this._sinkCount += 1;
755
734
  if (hints?.singleDep) {
756
735
  this._singleDepSinkCount += 1;
757
736
  this._singleDepSinks.add(sink);
758
737
  }
738
+ if (!this._terminal) {
739
+ const startMessages = this._cached === NO_VALUE ? [[START]] : [[START], [DATA, this._cached]];
740
+ downWithBatch(sink, startMessages);
741
+ }
759
742
  if (this._sinks == null) {
760
743
  this._sinks = sink;
761
744
  } else if (typeof this._sinks === "function") {
@@ -763,10 +746,12 @@ var NodeImpl = class {
763
746
  } else {
764
747
  this._sinks.add(sink);
765
748
  }
766
- if (this._hasDeps) {
767
- this._connectUpstream();
768
- } else if (this._fn) {
769
- this._startProducer();
749
+ if (this._sinkCount === 1 && !this._terminal) {
750
+ this._active = true;
751
+ this._onActivate();
752
+ }
753
+ if (!this._terminal && this._status === "disconnected" && this._cached === NO_VALUE) {
754
+ this._status = "pending";
770
755
  }
771
756
  let removed = false;
772
757
  return () => {
@@ -790,39 +775,49 @@ var NodeImpl = class {
790
775
  }
791
776
  }
792
777
  if (this._sinks == null) {
793
- this._disconnectUpstream();
794
- this._stopProducer();
778
+ this._onDeactivate();
795
779
  }
796
780
  };
797
781
  }
798
- up(messages, options) {
799
- if (!this._hasDeps) return;
800
- if (!options?.internal && this._guard != null) {
801
- const actor = normalizeActor(options?.actor);
802
- if (!this._guard(actor, "write")) {
803
- throw new GuardDenied({ actor, action: "write", nodeName: this.name });
804
- }
805
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
782
+ // --- Down pipeline ---
783
+ /**
784
+ * Core outgoing dispatch. Applies terminal filter + local lifecycle
785
+ * update, then hands messages to `downWithBatch` for tier-aware delivery.
786
+ */
787
+ _downInternal(messages) {
788
+ if (messages.length === 0) return;
789
+ let sinkMessages = messages;
790
+ if (this._terminal && !this._resubscribable) {
791
+ const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
792
+ if (pass.length === 0) return;
793
+ sinkMessages = pass;
806
794
  }
807
- for (const dep of this._deps) {
808
- if (options === void 0) {
809
- dep.up?.(messages);
810
- } else {
811
- dep.up?.(messages, options);
795
+ this._handleLocalLifecycle(sinkMessages);
796
+ if (this._canSkipDirty()) {
797
+ let hasPhase2 = false;
798
+ for (let i = 0; i < sinkMessages.length; i++) {
799
+ const t = sinkMessages[i][0];
800
+ if (t === DATA || t === RESOLVED) {
801
+ hasPhase2 = true;
802
+ break;
803
+ }
804
+ }
805
+ if (hasPhase2) {
806
+ const filtered = [];
807
+ for (let i = 0; i < sinkMessages.length; i++) {
808
+ if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
809
+ }
810
+ if (filtered.length > 0) {
811
+ downWithBatch(this._boundDownToSinks, filtered);
812
+ }
813
+ return;
812
814
  }
813
815
  }
816
+ downWithBatch(this._boundDownToSinks, sinkMessages);
814
817
  }
815
- _upInternal(messages) {
816
- if (!this._hasDeps) return;
817
- for (const dep of this._deps) {
818
- dep.up?.(messages, { internal: true });
819
- }
820
- }
821
- unsubscribe() {
822
- if (!this._hasDeps) return;
823
- this._disconnectUpstream();
818
+ _canSkipDirty() {
819
+ return this._sinkCount === 1 && this._singleDepSinkCount === 1;
824
820
  }
825
- // --- Private methods (prototype, _ prefix) ---
826
821
  _downToSinks(messages) {
827
822
  if (this._sinks == null) return;
828
823
  if (typeof this._sinks === "function") {
@@ -834,6 +829,11 @@ var NodeImpl = class {
834
829
  sink(messages);
835
830
  }
836
831
  }
832
+ /**
833
+ * Update `_cached`, `_status`, `_terminal` from message batch before
834
+ * delivery. Subclass hooks `_onInvalidate` / `_onTeardown` let
835
+ * {@link NodeImpl} clear `_lastDepValues` and invoke cleanup fns.
836
+ */
837
837
  _handleLocalLifecycle(messages) {
838
838
  for (const m of messages) {
839
839
  const t = m[0];
@@ -847,28 +847,22 @@ var NodeImpl = class {
847
847
  }
848
848
  }
849
849
  if (t === INVALIDATE) {
850
- const cleanupFn = this._cleanup;
851
- this._cleanup = void 0;
852
- cleanupFn?.();
850
+ this._onInvalidate();
853
851
  this._cached = NO_VALUE;
854
- this._lastDepValues = void 0;
855
852
  }
856
853
  this._status = statusAfterMessage(this._status, m);
857
854
  if (t === COMPLETE || t === ERROR) {
858
855
  this._terminal = true;
859
856
  }
860
857
  if (t === TEARDOWN) {
861
- if (this._opts.resetOnTeardown) {
858
+ if (this._resetOnTeardown) {
862
859
  this._cached = NO_VALUE;
863
860
  }
864
- const teardownCleanup = this._cleanup;
865
- this._cleanup = void 0;
866
- teardownCleanup?.();
861
+ this._onTeardown();
867
862
  try {
868
863
  this._propagateToMeta(t);
869
864
  } finally {
870
- this._disconnectUpstream();
871
- this._stopProducer();
865
+ this._onDeactivate();
872
866
  }
873
867
  }
874
868
  if (t !== TEARDOWN && propagatesToMeta(t)) {
@@ -876,7 +870,20 @@ var NodeImpl = class {
876
870
  }
877
871
  }
878
872
  }
879
- /** Propagate a signal to all companion meta nodes (best-effort). */
873
+ /**
874
+ * Subclass hook: invoked when INVALIDATE arrives (before `_cached` is
875
+ * cleared). {@link NodeImpl} uses this to run the fn cleanup fn and
876
+ * drop `_lastDepValues` so the next wave re-runs fn.
877
+ */
878
+ _onInvalidate() {
879
+ }
880
+ /**
881
+ * Subclass hook: invoked when TEARDOWN arrives, before `_onDeactivate`.
882
+ * {@link NodeImpl} uses this to run any pending cleanup fn.
883
+ */
884
+ _onTeardown() {
885
+ }
886
+ /** Forward a signal to all companion meta nodes (best-effort). */
880
887
  _propagateToMeta(t) {
881
888
  for (const metaNode of Object.values(this.meta)) {
882
889
  try {
@@ -885,9 +892,10 @@ var NodeImpl = class {
885
892
  }
886
893
  }
887
894
  }
888
- _canSkipDirty() {
889
- return this._sinkCount === 1 && this._singleDepSinkCount === 1;
890
- }
895
+ /**
896
+ * Frame a computed value into the right protocol messages and dispatch
897
+ * via `_downInternal`. Used by `_runFn` and `actions.emit`.
898
+ */
891
899
  _downAutoValue(value) {
892
900
  const wasDirty = this._status === "dirty";
893
901
  let unchanged;
@@ -895,7 +903,9 @@ var NodeImpl = class {
895
903
  unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
896
904
  } catch (eqErr) {
897
905
  const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
898
- const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, { cause: eqErr });
906
+ const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, {
907
+ cause: eqErr
908
+ });
899
909
  this._downInternal([[ERROR, wrapped]]);
900
910
  return;
901
911
  }
@@ -905,89 +915,173 @@ var NodeImpl = class {
905
915
  }
906
916
  this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
907
917
  }
908
- _runFn() {
909
- if (!this._fn) return;
910
- if (this._terminal && !this._opts.resubscribable) return;
911
- if (this._connecting) return;
912
- try {
913
- const n = this._deps.length;
914
- const depValues = new Array(n);
915
- for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
916
- const prev = this._lastDepValues;
917
- if (n > 0 && prev != null && prev.length === n) {
918
- let allSame = true;
919
- for (let i = 0; i < n; i++) {
920
- if (!Object.is(depValues[i], prev[i])) {
921
- allSame = false;
922
- break;
923
- }
924
- }
925
- if (allSame) {
926
- if (this._status === "dirty") {
927
- this._downInternal([[RESOLVED]]);
928
- }
929
- return;
930
- }
931
- }
932
- const prevCleanup = this._cleanup;
933
- this._cleanup = void 0;
934
- prevCleanup?.();
935
- this._manualEmitUsed = false;
936
- this._lastDepValues = depValues;
937
- this._inspectorHook?.({ kind: "run", depValues });
938
- const out = this._fn(depValues, this._actions);
939
- if (isCleanupResult(out)) {
940
- this._cleanup = out.cleanup;
941
- if (this._manualEmitUsed) return;
942
- if ("value" in out) {
943
- this._downAutoValue(out.value);
944
- }
945
- return;
918
+ };
919
+
920
+ // src/core/node.ts
921
+ var NodeImpl = class extends NodeBase {
922
+ // --- Dep configuration (set once) ---
923
+ _deps;
924
+ _fn;
925
+ _opts;
926
+ _hasDeps;
927
+ _isSingleDep;
928
+ _autoComplete;
929
+ // --- Wave tracking masks ---
930
+ _depDirtyMask;
931
+ _depSettledMask;
932
+ _depCompleteMask;
933
+ _allDepsCompleteMask;
934
+ // --- Identity-skip optimization ---
935
+ _lastDepValues;
936
+ _cleanup;
937
+ // --- Upstream bookkeeping ---
938
+ _upstreamUnsubs = [];
939
+ // --- Fn behavior flag ---
940
+ /** @internal Read by `describeNode` to infer `"operator"` label. */
941
+ _manualEmitUsed = false;
942
+ constructor(deps, fn, opts) {
943
+ super(opts);
944
+ this._deps = deps;
945
+ this._fn = fn;
946
+ this._opts = opts;
947
+ this._hasDeps = deps.length > 0;
948
+ this._isSingleDep = deps.length === 1 && fn != null;
949
+ this._autoComplete = opts.completeWhenDepsComplete ?? true;
950
+ if (!this._hasDeps && fn == null && this._cached !== NO_VALUE) {
951
+ this._status = "settled";
952
+ }
953
+ this._depDirtyMask = createBitSet(deps.length);
954
+ this._depSettledMask = createBitSet(deps.length);
955
+ this._depCompleteMask = createBitSet(deps.length);
956
+ this._allDepsCompleteMask = createBitSet(deps.length);
957
+ for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
958
+ this.down = this.down.bind(this);
959
+ this.up = this.up.bind(this);
960
+ }
961
+ // --- Meta node factory (called from base constructor) ---
962
+ _createMetaNode(key, initialValue, opts) {
963
+ return node({
964
+ initial: initialValue,
965
+ name: `${opts.name ?? "node"}:meta:${key}`,
966
+ describeKind: "state",
967
+ ...opts.guard != null ? { guard: opts.guard } : {}
968
+ });
969
+ }
970
+ // --- Manual emit tracker (set by actions.down / actions.emit) ---
971
+ _onManualEmit() {
972
+ this._manualEmitUsed = true;
973
+ }
974
+ // --- Up / unsubscribe ---
975
+ up(messages, options) {
976
+ if (!this._hasDeps) return;
977
+ if (!options?.internal && this._guard != null) {
978
+ const actor = normalizeActor(options?.actor);
979
+ if (!this._guard(actor, "write")) {
980
+ throw new GuardDenied({ actor, action: "write", nodeName: this.name });
946
981
  }
947
- if (isCleanupFn(out)) {
948
- this._cleanup = out;
949
- return;
982
+ this._recordMutation(actor);
983
+ }
984
+ for (const dep of this._deps) {
985
+ if (options === void 0) {
986
+ dep.up?.(messages);
987
+ } else {
988
+ dep.up?.(messages, options);
950
989
  }
951
- if (this._manualEmitUsed) return;
952
- if (out === void 0) return;
953
- this._downAutoValue(out);
954
- } catch (err) {
955
- const errMsg = err instanceof Error ? err.message : String(err);
956
- const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
957
- this._downInternal([[ERROR, wrapped]]);
958
990
  }
959
991
  }
960
- _onDepDirty(index) {
961
- const wasDirty = this._depDirtyMask.has(index);
962
- this._depDirtyMask.set(index);
963
- this._depSettledMask.clear(index);
964
- if (!wasDirty) {
965
- this._downInternal([[DIRTY]]);
992
+ _upInternal(messages) {
993
+ if (!this._hasDeps) return;
994
+ for (const dep of this._deps) {
995
+ dep.up?.(messages, { internal: true });
966
996
  }
967
997
  }
968
- _onDepSettled(index) {
969
- if (!this._depDirtyMask.has(index)) {
970
- this._onDepDirty(index);
998
+ unsubscribe() {
999
+ if (!this._hasDeps) return;
1000
+ this._disconnectUpstream();
1001
+ }
1002
+ // --- Activation (first-subscriber / last-subscriber hooks) ---
1003
+ _onActivate() {
1004
+ if (this._hasDeps) {
1005
+ this._connectUpstream();
1006
+ return;
971
1007
  }
972
- this._depSettledMask.set(index);
973
- if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
974
- this._depDirtyMask.reset();
975
- this._depSettledMask.reset();
1008
+ if (this._fn) {
976
1009
  this._runFn();
1010
+ return;
977
1011
  }
978
1012
  }
979
- _maybeCompleteFromDeps() {
980
- if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
981
- this._downInternal([[COMPLETE]]);
1013
+ _doDeactivate() {
1014
+ this._disconnectUpstream();
1015
+ const cleanup = this._cleanup;
1016
+ this._cleanup = void 0;
1017
+ cleanup?.();
1018
+ if (this._fn != null) {
1019
+ this._cached = NO_VALUE;
1020
+ this._lastDepValues = void 0;
982
1021
  }
1022
+ if (this._hasDeps || this._fn != null) {
1023
+ this._status = "disconnected";
1024
+ }
1025
+ }
1026
+ // --- INVALIDATE / TEARDOWN hooks (clear fn state) ---
1027
+ _onInvalidate() {
1028
+ const cleanup = this._cleanup;
1029
+ this._cleanup = void 0;
1030
+ cleanup?.();
1031
+ this._lastDepValues = void 0;
983
1032
  }
1033
+ _onTeardown() {
1034
+ const cleanup = this._cleanup;
1035
+ this._cleanup = void 0;
1036
+ cleanup?.();
1037
+ }
1038
+ // --- Upstream connect / disconnect ---
1039
+ _connectUpstream() {
1040
+ if (!this._hasDeps) return;
1041
+ if (this._upstreamUnsubs.length > 0) return;
1042
+ this._depDirtyMask.setAll();
1043
+ this._depSettledMask.reset();
1044
+ this._depCompleteMask.reset();
1045
+ const depValuesBefore = this._lastDepValues;
1046
+ const subHints = this._isSingleDep ? { singleDep: true } : void 0;
1047
+ for (let i = 0; i < this._deps.length; i += 1) {
1048
+ const dep = this._deps[i];
1049
+ this._upstreamUnsubs.push(
1050
+ dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
1051
+ );
1052
+ }
1053
+ if (this._fn && this._onMessage && !this._terminal && this._lastDepValues === depValuesBefore) {
1054
+ this._runFn();
1055
+ }
1056
+ }
1057
+ _disconnectUpstream() {
1058
+ if (this._upstreamUnsubs.length === 0) return;
1059
+ for (const unsub of this._upstreamUnsubs.splice(0)) {
1060
+ unsub();
1061
+ }
1062
+ this._depDirtyMask.reset();
1063
+ this._depSettledMask.reset();
1064
+ this._depCompleteMask.reset();
1065
+ }
1066
+ // --- Wave handling ---
984
1067
  _handleDepMessages(index, messages) {
985
1068
  for (const msg of messages) {
986
- this._inspectorHook?.({ kind: "dep_message", depIndex: index, message: msg });
1069
+ this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
987
1070
  const t = msg[0];
988
1071
  if (this._onMessage) {
989
1072
  try {
990
- if (this._onMessage(msg, index, this._actions)) continue;
1073
+ const consumed = this._onMessage(msg, index, this._actions);
1074
+ if (consumed) {
1075
+ if (t === START) {
1076
+ this._depDirtyMask.clear(index);
1077
+ if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1078
+ this._depDirtyMask.reset();
1079
+ this._depSettledMask.reset();
1080
+ this._runFn();
1081
+ }
1082
+ }
1083
+ continue;
1084
+ }
991
1085
  } catch (err) {
992
1086
  const errMsg = err instanceof Error ? err.message : String(err);
993
1087
  const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
@@ -997,6 +1091,7 @@ var NodeImpl = class {
997
1091
  return;
998
1092
  }
999
1093
  }
1094
+ if (messageTier(t) < 1) continue;
1000
1095
  if (!this._fn) {
1001
1096
  if (t === COMPLETE && this._deps.length > 1) {
1002
1097
  this._depCompleteMask.set(index);
@@ -1040,53 +1135,85 @@ var NodeImpl = class {
1040
1135
  this._downInternal([msg]);
1041
1136
  }
1042
1137
  }
1043
- _connectUpstream() {
1044
- if (!this._hasDeps || this._connected) return;
1045
- this._connected = true;
1046
- this._depDirtyMask.reset();
1047
- this._depSettledMask.reset();
1048
- this._depCompleteMask.reset();
1049
- this._status = "settled";
1050
- const subHints = this._isSingleDep ? { singleDep: true } : void 0;
1051
- this._connecting = true;
1052
- try {
1053
- for (let i = 0; i < this._deps.length; i += 1) {
1054
- const dep = this._deps[i];
1055
- this._upstreamUnsubs.push(
1056
- dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
1057
- );
1058
- }
1059
- } finally {
1060
- this._connecting = false;
1138
+ _onDepDirty(index) {
1139
+ const wasDirty = this._depDirtyMask.has(index);
1140
+ this._depDirtyMask.set(index);
1141
+ this._depSettledMask.clear(index);
1142
+ if (!wasDirty) {
1143
+ this._downInternal([[DIRTY]]);
1061
1144
  }
1062
- if (this._fn) {
1145
+ }
1146
+ _onDepSettled(index) {
1147
+ if (!this._depDirtyMask.has(index)) {
1148
+ this._onDepDirty(index);
1149
+ }
1150
+ this._depSettledMask.set(index);
1151
+ if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1152
+ this._depDirtyMask.reset();
1153
+ this._depSettledMask.reset();
1063
1154
  this._runFn();
1064
1155
  }
1065
1156
  }
1066
- _stopProducer() {
1067
- if (!this._producerStarted) return;
1068
- this._producerStarted = false;
1069
- const producerCleanup = this._cleanup;
1070
- this._cleanup = void 0;
1071
- producerCleanup?.();
1072
- }
1073
- _startProducer() {
1074
- if (this._deps.length !== 0 || !this._fn || this._producerStarted) return;
1075
- this._producerStarted = true;
1076
- this._runFn();
1157
+ _maybeCompleteFromDeps() {
1158
+ if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
1159
+ this._downInternal([[COMPLETE]]);
1160
+ }
1077
1161
  }
1078
- _disconnectUpstream() {
1079
- if (!this._connected) return;
1080
- for (const unsub of this._upstreamUnsubs.splice(0)) {
1081
- unsub();
1162
+ // --- Fn execution ---
1163
+ _runFn() {
1164
+ if (!this._fn) return;
1165
+ if (this._terminal && !this._resubscribable) return;
1166
+ try {
1167
+ const n = this._deps.length;
1168
+ const depValues = new Array(n);
1169
+ for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
1170
+ const prev = this._lastDepValues;
1171
+ if (n > 0 && prev != null && prev.length === n) {
1172
+ let allSame = true;
1173
+ for (let i = 0; i < n; i++) {
1174
+ if (!Object.is(depValues[i], prev[i])) {
1175
+ allSame = false;
1176
+ break;
1177
+ }
1178
+ }
1179
+ if (allSame) {
1180
+ if (this._status === "dirty") {
1181
+ this._downInternal([[RESOLVED]]);
1182
+ }
1183
+ return;
1184
+ }
1185
+ }
1186
+ const prevCleanup = this._cleanup;
1187
+ this._cleanup = void 0;
1188
+ prevCleanup?.();
1189
+ this._manualEmitUsed = false;
1190
+ this._lastDepValues = depValues;
1191
+ this._emitInspectorHook({ kind: "run", depValues });
1192
+ const out = this._fn(depValues, this._actions);
1193
+ if (isCleanupResult(out)) {
1194
+ this._cleanup = out.cleanup;
1195
+ if (this._manualEmitUsed) return;
1196
+ if ("value" in out) {
1197
+ this._downAutoValue(out.value);
1198
+ }
1199
+ return;
1200
+ }
1201
+ if (isCleanupFn(out)) {
1202
+ this._cleanup = out;
1203
+ return;
1204
+ }
1205
+ if (this._manualEmitUsed) return;
1206
+ if (out === void 0) return;
1207
+ this._downAutoValue(out);
1208
+ } catch (err) {
1209
+ const errMsg = err instanceof Error ? err.message : String(err);
1210
+ const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
1211
+ this._downInternal([[ERROR, wrapped]]);
1082
1212
  }
1083
- this._connected = false;
1084
- this._depDirtyMask.reset();
1085
- this._depSettledMask.reset();
1086
- this._depCompleteMask.reset();
1087
- this._status = "disconnected";
1088
1213
  }
1089
1214
  };
1215
+ var isNodeArray = (value) => Array.isArray(value);
1216
+ var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
1090
1217
  function node(depsOrFn, fnOrOpts, optsArg) {
1091
1218
  const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
1092
1219
  const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
@@ -1102,230 +1229,47 @@ function node(depsOrFn, fnOrOpts, optsArg) {
1102
1229
  }
1103
1230
 
1104
1231
  // src/core/dynamic-node.ts
1232
+ var MAX_RERUN = 16;
1105
1233
  function dynamicNode(fn, opts) {
1106
1234
  return new DynamicNodeImpl(fn, opts ?? {});
1107
1235
  }
1108
- var DynamicNodeImpl = class {
1109
- _optsName;
1110
- _registryName;
1111
- _describeKind;
1112
- meta;
1236
+ var DynamicNodeImpl = class extends NodeBase {
1113
1237
  _fn;
1114
- _equals;
1115
- _resubscribable;
1116
- _resetOnTeardown;
1117
1238
  _autoComplete;
1118
- _onMessage;
1119
- _onResubscribe;
1120
- /** @internal — read by {@link describeNode} for `accessHintForGuard`. */
1121
- _guard;
1122
- _lastMutation;
1123
- _inspectorHook;
1124
- // Sink tracking
1125
- _sinkCount = 0;
1126
- _singleDepSinkCount = 0;
1127
- _singleDepSinks = /* @__PURE__ */ new WeakSet();
1128
- // Actions object (for onMessage handler)
1129
- _actions;
1130
- _boundDownToSinks;
1131
- // Mutable state
1132
- _cached = NO_VALUE;
1133
- _status = "disconnected";
1134
- _terminal = false;
1135
- _connected = false;
1136
- _rewiring = false;
1137
- // re-entrancy guard
1138
1239
  // Dynamic deps tracking
1240
+ /** @internal Read by `describeNode`. */
1139
1241
  _deps = [];
1140
1242
  _depUnsubs = [];
1141
1243
  _depIndexMap = /* @__PURE__ */ new Map();
1142
- // node index in _deps
1143
- _dirtyBits = /* @__PURE__ */ new Set();
1144
- _settledBits = /* @__PURE__ */ new Set();
1145
- _completeBits = /* @__PURE__ */ new Set();
1146
- // Sinks
1147
- _sinks = null;
1244
+ _depDirtyBits = /* @__PURE__ */ new Set();
1245
+ _depSettledBits = /* @__PURE__ */ new Set();
1246
+ _depCompleteBits = /* @__PURE__ */ new Set();
1247
+ // Execution state
1248
+ _running = false;
1249
+ _rewiring = false;
1250
+ _bufferedDepMessages = [];
1251
+ _trackedValues = /* @__PURE__ */ new Map();
1252
+ _rerunCount = 0;
1148
1253
  constructor(fn, opts) {
1254
+ super(opts);
1149
1255
  this._fn = fn;
1150
- this._optsName = opts.name;
1151
- this._describeKind = opts.describeKind;
1152
- this._equals = opts.equals ?? Object.is;
1153
- this._resubscribable = opts.resubscribable ?? false;
1154
- this._resetOnTeardown = opts.resetOnTeardown ?? false;
1155
1256
  this._autoComplete = opts.completeWhenDepsComplete ?? true;
1156
- this._onMessage = opts.onMessage;
1157
- this._onResubscribe = opts.onResubscribe;
1158
- this._guard = opts.guard;
1159
- this._inspectorHook = void 0;
1160
- const meta = {};
1161
- for (const [k, v] of Object.entries(opts.meta ?? {})) {
1162
- meta[k] = node({
1163
- initial: v,
1164
- name: `${opts.name ?? "dynamicNode"}:meta:${k}`,
1165
- describeKind: "state",
1166
- ...opts.guard != null ? { guard: opts.guard } : {}
1167
- });
1168
- }
1169
- Object.freeze(meta);
1170
- this.meta = meta;
1171
- const self = this;
1172
- this._actions = {
1173
- down(messages) {
1174
- self._downInternal(messages);
1175
- },
1176
- emit(value) {
1177
- self._downAutoValue(value);
1178
- },
1179
- up(messages) {
1180
- for (const dep of self._deps) {
1181
- dep.up?.(messages, { internal: true });
1182
- }
1183
- }
1184
- };
1185
- this._boundDownToSinks = this._downToSinks.bind(this);
1186
- }
1187
- get name() {
1188
- return this._registryName ?? this._optsName;
1189
- }
1190
- /** @internal */
1191
- _assignRegistryName(localName) {
1192
- if (this._optsName !== void 0 || this._registryName !== void 0) return;
1193
- this._registryName = localName;
1194
- }
1195
- /**
1196
- * @internal Attach/remove inspector hook for graph-level observability.
1197
- * Returns a disposer that restores the previous hook.
1198
- */
1199
- _setInspectorHook(hook) {
1200
- const prev = this._inspectorHook;
1201
- this._inspectorHook = hook;
1202
- return () => {
1203
- if (this._inspectorHook === hook) {
1204
- this._inspectorHook = prev;
1205
- }
1206
- };
1207
- }
1208
- get status() {
1209
- return this._status;
1257
+ this.down = this.down.bind(this);
1258
+ this.up = this.up.bind(this);
1210
1259
  }
1211
- get lastMutation() {
1212
- return this._lastMutation;
1260
+ _createMetaNode(key, initialValue, opts) {
1261
+ return node({
1262
+ initial: initialValue,
1263
+ name: `${opts.name ?? "dynamicNode"}:meta:${key}`,
1264
+ describeKind: "state",
1265
+ ...opts.guard != null ? { guard: opts.guard } : {}
1266
+ });
1213
1267
  }
1214
- /** Versioning not yet supported on DynamicNodeImpl. */
1268
+ /** Versioning not supported on DynamicNodeImpl (override base). */
1215
1269
  get v() {
1216
1270
  return void 0;
1217
1271
  }
1218
- hasGuard() {
1219
- return this._guard != null;
1220
- }
1221
- allowsObserve(actor) {
1222
- if (this._guard == null) return true;
1223
- return this._guard(normalizeActor(actor), "observe");
1224
- }
1225
- get() {
1226
- return this._cached === NO_VALUE ? void 0 : this._cached;
1227
- }
1228
- down(messages, options) {
1229
- if (messages.length === 0) return;
1230
- if (!options?.internal && this._guard != null) {
1231
- const actor = normalizeActor(options?.actor);
1232
- const delivery = options?.delivery ?? "write";
1233
- const action = delivery === "signal" ? "signal" : "write";
1234
- if (!this._guard(actor, action)) {
1235
- throw new GuardDenied({ actor, action, nodeName: this.name });
1236
- }
1237
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
1238
- }
1239
- this._downInternal(messages);
1240
- }
1241
- _downInternal(messages) {
1242
- if (messages.length === 0) return;
1243
- let sinkMessages = messages;
1244
- if (this._terminal && !this._resubscribable) {
1245
- const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
1246
- if (pass.length === 0) return;
1247
- sinkMessages = pass;
1248
- }
1249
- this._handleLocalLifecycle(sinkMessages);
1250
- if (this._canSkipDirty()) {
1251
- let hasPhase2 = false;
1252
- for (let i = 0; i < sinkMessages.length; i++) {
1253
- const t = sinkMessages[i][0];
1254
- if (t === DATA || t === RESOLVED) {
1255
- hasPhase2 = true;
1256
- break;
1257
- }
1258
- }
1259
- if (hasPhase2) {
1260
- const filtered = [];
1261
- for (let i = 0; i < sinkMessages.length; i++) {
1262
- if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
1263
- }
1264
- if (filtered.length > 0) {
1265
- downWithBatch(this._boundDownToSinks, filtered);
1266
- }
1267
- return;
1268
- }
1269
- }
1270
- downWithBatch(this._boundDownToSinks, sinkMessages);
1271
- }
1272
- _canSkipDirty() {
1273
- return this._sinkCount === 1 && this._singleDepSinkCount === 1;
1274
- }
1275
- subscribe(sink, hints) {
1276
- if (hints?.actor != null && this._guard != null) {
1277
- const actor = normalizeActor(hints.actor);
1278
- if (!this._guard(actor, "observe")) {
1279
- throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
1280
- }
1281
- }
1282
- if (this._terminal && this._resubscribable) {
1283
- this._terminal = false;
1284
- this._cached = NO_VALUE;
1285
- this._status = "disconnected";
1286
- this._onResubscribe?.();
1287
- }
1288
- this._sinkCount += 1;
1289
- if (hints?.singleDep) {
1290
- this._singleDepSinkCount += 1;
1291
- this._singleDepSinks.add(sink);
1292
- }
1293
- if (this._sinks == null) {
1294
- this._sinks = sink;
1295
- } else if (typeof this._sinks === "function") {
1296
- this._sinks = /* @__PURE__ */ new Set([this._sinks, sink]);
1297
- } else {
1298
- this._sinks.add(sink);
1299
- }
1300
- if (!this._connected) {
1301
- this._connect();
1302
- }
1303
- let removed = false;
1304
- return () => {
1305
- if (removed) return;
1306
- removed = true;
1307
- this._sinkCount -= 1;
1308
- if (this._singleDepSinks.has(sink)) {
1309
- this._singleDepSinkCount -= 1;
1310
- this._singleDepSinks.delete(sink);
1311
- }
1312
- if (this._sinks == null) return;
1313
- if (typeof this._sinks === "function") {
1314
- if (this._sinks === sink) this._sinks = null;
1315
- } else {
1316
- this._sinks.delete(sink);
1317
- if (this._sinks.size === 1) {
1318
- const [only] = this._sinks;
1319
- this._sinks = only;
1320
- } else if (this._sinks.size === 0) {
1321
- this._sinks = null;
1322
- }
1323
- }
1324
- if (this._sinks == null) {
1325
- this._disconnect();
1326
- }
1327
- };
1328
- }
1272
+ // --- Up / unsubscribe ---
1329
1273
  up(messages, options) {
1330
1274
  if (this._deps.length === 0) return;
1331
1275
  if (!options?.internal && this._guard != null) {
@@ -1333,221 +1277,227 @@ var DynamicNodeImpl = class {
1333
1277
  if (!this._guard(actor, "write")) {
1334
1278
  throw new GuardDenied({ actor, action: "write", nodeName: this.name });
1335
1279
  }
1336
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
1280
+ this._recordMutation(actor);
1337
1281
  }
1338
1282
  for (const dep of this._deps) {
1339
1283
  dep.up?.(messages, options);
1340
1284
  }
1341
1285
  }
1342
- unsubscribe() {
1343
- this._disconnect();
1344
- }
1345
- // --- Private methods ---
1346
- _downToSinks(messages) {
1347
- if (this._sinks == null) return;
1348
- if (typeof this._sinks === "function") {
1349
- this._sinks(messages);
1350
- return;
1351
- }
1352
- const snapshot = [...this._sinks];
1353
- for (const sink of snapshot) {
1354
- sink(messages);
1355
- }
1356
- }
1357
- _handleLocalLifecycle(messages) {
1358
- for (const m of messages) {
1359
- const t = m[0];
1360
- if (t === DATA) this._cached = m[1];
1361
- if (t === INVALIDATE) {
1362
- this._cached = NO_VALUE;
1363
- this._status = "dirty";
1364
- }
1365
- if (t === DATA) {
1366
- this._status = "settled";
1367
- } else if (t === RESOLVED) {
1368
- this._status = "resolved";
1369
- } else if (t === DIRTY) {
1370
- this._status = "dirty";
1371
- } else if (t === COMPLETE) {
1372
- this._status = "completed";
1373
- this._terminal = true;
1374
- } else if (t === ERROR) {
1375
- this._status = "errored";
1376
- this._terminal = true;
1377
- }
1378
- if (t === TEARDOWN) {
1379
- if (this._resetOnTeardown) this._cached = NO_VALUE;
1380
- try {
1381
- this._propagateToMeta(t);
1382
- } finally {
1383
- this._disconnect();
1384
- }
1385
- }
1386
- if (t !== TEARDOWN && propagatesToMeta(t)) {
1387
- this._propagateToMeta(t);
1388
- }
1389
- }
1390
- }
1391
- /** Propagate a signal to all companion meta nodes (best-effort). */
1392
- _propagateToMeta(t) {
1393
- for (const metaNode of Object.values(this.meta)) {
1394
- try {
1395
- metaNode.down([[t]], { internal: true });
1396
- } catch {
1397
- }
1286
+ _upInternal(messages) {
1287
+ for (const dep of this._deps) {
1288
+ dep.up?.(messages, { internal: true });
1398
1289
  }
1399
1290
  }
1400
- _downAutoValue(value) {
1401
- const wasDirty = this._status === "dirty";
1402
- let unchanged;
1403
- try {
1404
- unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
1405
- } catch (eqErr) {
1406
- const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
1407
- const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, { cause: eqErr });
1408
- this._downInternal([[ERROR, wrapped]]);
1409
- return;
1410
- }
1411
- if (unchanged) {
1412
- this._downInternal(wasDirty ? [[RESOLVED]] : [[DIRTY], [RESOLVED]]);
1413
- return;
1414
- }
1415
- this._cached = value;
1416
- this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
1291
+ unsubscribe() {
1292
+ this._disconnect();
1417
1293
  }
1418
- _connect() {
1419
- if (this._connected) return;
1420
- this._connected = true;
1421
- this._status = "settled";
1422
- this._dirtyBits.clear();
1423
- this._settledBits.clear();
1424
- this._completeBits.clear();
1294
+ // --- Activation hooks ---
1295
+ _onActivate() {
1425
1296
  this._runFn();
1426
1297
  }
1298
+ _doDeactivate() {
1299
+ this._disconnect();
1300
+ }
1427
1301
  _disconnect() {
1428
- if (!this._connected) return;
1429
1302
  for (const unsub of this._depUnsubs) unsub();
1430
1303
  this._depUnsubs = [];
1431
1304
  this._deps = [];
1432
1305
  this._depIndexMap.clear();
1433
- this._dirtyBits.clear();
1434
- this._settledBits.clear();
1435
- this._completeBits.clear();
1436
- this._connected = false;
1306
+ this._depDirtyBits.clear();
1307
+ this._depSettledBits.clear();
1308
+ this._depCompleteBits.clear();
1309
+ this._cached = NO_VALUE;
1437
1310
  this._status = "disconnected";
1438
1311
  }
1312
+ // --- Fn execution with rewire buffer ---
1439
1313
  _runFn() {
1440
1314
  if (this._terminal && !this._resubscribable) return;
1441
- if (this._rewiring) return;
1442
- const trackedDeps = [];
1443
- const trackedSet = /* @__PURE__ */ new Set();
1444
- const get = (dep) => {
1445
- if (!trackedSet.has(dep)) {
1446
- trackedSet.add(dep);
1447
- trackedDeps.push(dep);
1448
- }
1449
- return dep.get();
1450
- };
1315
+ if (this._running) return;
1316
+ this._running = true;
1317
+ this._rerunCount = 0;
1318
+ let result;
1451
1319
  try {
1452
- const depValues = [];
1453
- for (const dep of this._deps) {
1454
- depValues.push(dep.get());
1320
+ for (; ; ) {
1321
+ const trackedDeps = [];
1322
+ const trackedValuesMap = /* @__PURE__ */ new Map();
1323
+ const trackedSet = /* @__PURE__ */ new Set();
1324
+ const get = (dep) => {
1325
+ if (!trackedSet.has(dep)) {
1326
+ trackedSet.add(dep);
1327
+ trackedDeps.push(dep);
1328
+ trackedValuesMap.set(dep, dep.get());
1329
+ }
1330
+ return dep.get();
1331
+ };
1332
+ this._trackedValues = trackedValuesMap;
1333
+ const depValues = [];
1334
+ for (const dep of this._deps) depValues.push(dep.get());
1335
+ this._emitInspectorHook({ kind: "run", depValues });
1336
+ try {
1337
+ result = this._fn(get);
1338
+ } catch (err) {
1339
+ const errMsg = err instanceof Error ? err.message : String(err);
1340
+ const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, {
1341
+ cause: err
1342
+ });
1343
+ this._downInternal([[ERROR, wrapped]]);
1344
+ return;
1345
+ }
1346
+ this._rewiring = true;
1347
+ this._bufferedDepMessages = [];
1348
+ try {
1349
+ this._rewire(trackedDeps);
1350
+ } finally {
1351
+ this._rewiring = false;
1352
+ }
1353
+ let needsRerun = false;
1354
+ for (const entry of this._bufferedDepMessages) {
1355
+ for (const msg of entry.msgs) {
1356
+ if (msg[0] === DATA) {
1357
+ const dep = this._deps[entry.index];
1358
+ const trackedValue = dep != null ? this._trackedValues.get(dep) : void 0;
1359
+ const actualValue = msg[1];
1360
+ if (!this._equals(trackedValue, actualValue)) {
1361
+ needsRerun = true;
1362
+ break;
1363
+ }
1364
+ }
1365
+ }
1366
+ if (needsRerun) break;
1367
+ }
1368
+ if (needsRerun) {
1369
+ this._rerunCount += 1;
1370
+ if (this._rerunCount > MAX_RERUN) {
1371
+ this._bufferedDepMessages = [];
1372
+ this._downInternal([
1373
+ [
1374
+ ERROR,
1375
+ new Error(
1376
+ `dynamicNode "${this.name ?? "anonymous"}": rewire did not stabilize within ${MAX_RERUN} iterations`
1377
+ )
1378
+ ]
1379
+ ]);
1380
+ return;
1381
+ }
1382
+ this._bufferedDepMessages = [];
1383
+ continue;
1384
+ }
1385
+ const drain = this._bufferedDepMessages;
1386
+ this._bufferedDepMessages = [];
1387
+ for (const entry of drain) {
1388
+ for (const msg of entry.msgs) {
1389
+ this._updateMasksForMessage(entry.index, msg);
1390
+ }
1391
+ }
1392
+ this._depDirtyBits.clear();
1393
+ this._depSettledBits.clear();
1394
+ break;
1455
1395
  }
1456
- this._inspectorHook?.({ kind: "run", depValues });
1457
- const result = this._fn(get);
1458
- this._rewire(trackedDeps);
1459
- if (result === void 0) return;
1460
- this._downAutoValue(result);
1461
- } catch (err) {
1462
- this._downInternal([[ERROR, err]]);
1396
+ } finally {
1397
+ this._running = false;
1463
1398
  }
1399
+ this._downAutoValue(result);
1464
1400
  }
1465
1401
  _rewire(newDeps) {
1466
- this._rewiring = true;
1467
- try {
1468
- const oldMap = this._depIndexMap;
1469
- const newMap = /* @__PURE__ */ new Map();
1470
- const newUnsubs = [];
1471
- for (let i = 0; i < newDeps.length; i++) {
1472
- const dep = newDeps[i];
1473
- newMap.set(dep, i);
1474
- const oldIdx = oldMap.get(dep);
1475
- if (oldIdx !== void 0) {
1476
- newUnsubs.push(this._depUnsubs[oldIdx]);
1477
- this._depUnsubs[oldIdx] = () => {
1478
- };
1479
- } else {
1480
- const idx = i;
1481
- const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
1482
- newUnsubs.push(unsub);
1483
- }
1402
+ const oldMap = this._depIndexMap;
1403
+ const newMap = /* @__PURE__ */ new Map();
1404
+ const newUnsubs = [];
1405
+ for (let i = 0; i < newDeps.length; i++) {
1406
+ const dep = newDeps[i];
1407
+ newMap.set(dep, i);
1408
+ const oldIdx = oldMap.get(dep);
1409
+ if (oldIdx !== void 0) {
1410
+ newUnsubs.push(this._depUnsubs[oldIdx]);
1411
+ this._depUnsubs[oldIdx] = () => {
1412
+ };
1413
+ } else {
1414
+ const idx = i;
1415
+ const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
1416
+ newUnsubs.push(unsub);
1484
1417
  }
1485
- for (const [dep, oldIdx] of oldMap) {
1486
- if (!newMap.has(dep)) {
1487
- this._depUnsubs[oldIdx]();
1488
- }
1418
+ }
1419
+ for (const [dep, oldIdx] of oldMap) {
1420
+ if (!newMap.has(dep)) {
1421
+ this._depUnsubs[oldIdx]();
1489
1422
  }
1490
- this._deps = newDeps;
1491
- this._depUnsubs = newUnsubs;
1492
- this._depIndexMap = newMap;
1493
- this._dirtyBits.clear();
1494
- this._settledBits.clear();
1495
- const newCompleteBits = /* @__PURE__ */ new Set();
1496
- for (const oldIdx of this._completeBits) {
1497
- const dep = [...oldMap.entries()].find(([, idx]) => idx === oldIdx)?.[0];
1498
- if (dep && newMap.has(dep)) {
1423
+ }
1424
+ this._deps = newDeps;
1425
+ this._depUnsubs = newUnsubs;
1426
+ this._depIndexMap = newMap;
1427
+ this._depDirtyBits.clear();
1428
+ this._depSettledBits.clear();
1429
+ const newCompleteBits = /* @__PURE__ */ new Set();
1430
+ for (const oldIdx of this._depCompleteBits) {
1431
+ for (const [dep, idx] of oldMap) {
1432
+ if (idx === oldIdx && newMap.has(dep)) {
1499
1433
  newCompleteBits.add(newMap.get(dep));
1434
+ break;
1500
1435
  }
1501
1436
  }
1502
- this._completeBits = newCompleteBits;
1503
- } finally {
1504
- this._rewiring = false;
1505
1437
  }
1438
+ this._depCompleteBits = newCompleteBits;
1506
1439
  }
1440
+ // --- Dep message handling ---
1507
1441
  _handleDepMessages(index, messages) {
1508
- if (this._rewiring) return;
1442
+ if (this._rewiring) {
1443
+ this._bufferedDepMessages.push({ index, msgs: messages });
1444
+ return;
1445
+ }
1509
1446
  for (const msg of messages) {
1510
- this._inspectorHook?.({ kind: "dep_message", depIndex: index, message: msg });
1447
+ this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
1511
1448
  const t = msg[0];
1512
1449
  if (this._onMessage) {
1513
1450
  try {
1514
1451
  if (this._onMessage(msg, index, this._actions)) continue;
1515
1452
  } catch (err) {
1516
- this._downInternal([[ERROR, err]]);
1453
+ const errMsg = err instanceof Error ? err.message : String(err);
1454
+ const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
1455
+ cause: err
1456
+ });
1457
+ this._downInternal([[ERROR, wrapped]]);
1517
1458
  return;
1518
1459
  }
1519
1460
  }
1461
+ if (messageTier(t) < 1) continue;
1520
1462
  if (t === DIRTY) {
1521
- this._dirtyBits.add(index);
1522
- this._settledBits.delete(index);
1523
- if (this._dirtyBits.size === 1) {
1524
- downWithBatch(this._boundDownToSinks, [[DIRTY]]);
1463
+ const wasEmpty = this._depDirtyBits.size === 0;
1464
+ this._depDirtyBits.add(index);
1465
+ this._depSettledBits.delete(index);
1466
+ if (wasEmpty) {
1467
+ this._downInternal([[DIRTY]]);
1525
1468
  }
1526
1469
  continue;
1527
1470
  }
1528
1471
  if (t === DATA || t === RESOLVED) {
1529
- if (!this._dirtyBits.has(index)) {
1530
- this._dirtyBits.add(index);
1531
- downWithBatch(this._boundDownToSinks, [[DIRTY]]);
1472
+ if (!this._depDirtyBits.has(index)) {
1473
+ const wasEmpty = this._depDirtyBits.size === 0;
1474
+ this._depDirtyBits.add(index);
1475
+ if (wasEmpty) {
1476
+ this._downInternal([[DIRTY]]);
1477
+ }
1532
1478
  }
1533
- this._settledBits.add(index);
1479
+ this._depSettledBits.add(index);
1534
1480
  if (this._allDirtySettled()) {
1535
- this._dirtyBits.clear();
1536
- this._settledBits.clear();
1537
- this._runFn();
1481
+ this._depDirtyBits.clear();
1482
+ this._depSettledBits.clear();
1483
+ if (!this._running) {
1484
+ if (this._depValuesDifferFromTracked()) {
1485
+ this._runFn();
1486
+ }
1487
+ }
1538
1488
  }
1539
1489
  continue;
1540
1490
  }
1541
1491
  if (t === COMPLETE) {
1542
- this._completeBits.add(index);
1543
- this._dirtyBits.delete(index);
1544
- this._settledBits.delete(index);
1492
+ this._depCompleteBits.add(index);
1493
+ this._depDirtyBits.delete(index);
1494
+ this._depSettledBits.delete(index);
1545
1495
  if (this._allDirtySettled()) {
1546
- this._dirtyBits.clear();
1547
- this._settledBits.clear();
1548
- this._runFn();
1496
+ this._depDirtyBits.clear();
1497
+ this._depSettledBits.clear();
1498
+ if (!this._running) this._runFn();
1549
1499
  }
1550
- if (this._autoComplete && this._completeBits.size >= this._deps.length && this._deps.length > 0) {
1500
+ if (this._autoComplete && this._depCompleteBits.size >= this._deps.length && this._deps.length > 0) {
1551
1501
  this._downInternal([[COMPLETE]]);
1552
1502
  }
1553
1503
  continue;
@@ -1563,13 +1513,46 @@ var DynamicNodeImpl = class {
1563
1513
  this._downInternal([msg]);
1564
1514
  }
1565
1515
  }
1516
+ /**
1517
+ * Update dep masks for a message without triggering `_runFn` — used
1518
+ * during post-rewire drain so the wave state is consistent with the
1519
+ * buffered activation cascade without recursing.
1520
+ */
1521
+ _updateMasksForMessage(index, msg) {
1522
+ const t = msg[0];
1523
+ if (t === DIRTY) {
1524
+ this._depDirtyBits.add(index);
1525
+ this._depSettledBits.delete(index);
1526
+ } else if (t === DATA || t === RESOLVED) {
1527
+ this._depDirtyBits.add(index);
1528
+ this._depSettledBits.add(index);
1529
+ } else if (t === COMPLETE) {
1530
+ this._depCompleteBits.add(index);
1531
+ this._depDirtyBits.delete(index);
1532
+ this._depSettledBits.delete(index);
1533
+ }
1534
+ }
1566
1535
  _allDirtySettled() {
1567
- if (this._dirtyBits.size === 0) return false;
1568
- for (const idx of this._dirtyBits) {
1569
- if (!this._settledBits.has(idx)) return false;
1536
+ if (this._depDirtyBits.size === 0) return false;
1537
+ for (const idx of this._depDirtyBits) {
1538
+ if (!this._depSettledBits.has(idx)) return false;
1570
1539
  }
1571
1540
  return true;
1572
1541
  }
1542
+ /**
1543
+ * True if any current dep value differs from what the last `_runFn`
1544
+ * saw via `get()`. Used to suppress redundant re-runs when deferred
1545
+ * handshake messages arrive after `_rewire` for a dep whose value
1546
+ * already matches `_trackedValues`.
1547
+ */
1548
+ _depValuesDifferFromTracked() {
1549
+ for (const dep of this._deps) {
1550
+ const current = dep.get();
1551
+ const tracked = this._trackedValues.get(dep);
1552
+ if (!this._equals(current, tracked)) return true;
1553
+ }
1554
+ return false;
1555
+ }
1573
1556
  };
1574
1557
 
1575
1558
  // src/core/sugar.ts
@@ -1601,6 +1584,11 @@ export {
1601
1584
  __decorateElement,
1602
1585
  DEFAULT_ACTOR,
1603
1586
  normalizeActor,
1587
+ GuardDenied,
1588
+ policy,
1589
+ policyFromRules,
1590
+ accessHintForGuard,
1591
+ START,
1604
1592
  DATA,
1605
1593
  DIRTY,
1606
1594
  RESOLVED,
@@ -1615,6 +1603,7 @@ export {
1615
1603
  messageTier,
1616
1604
  isPhase2Message,
1617
1605
  isTerminalMessage,
1606
+ isLocalOnly,
1618
1607
  propagatesToMeta,
1619
1608
  isBatching,
1620
1609
  batch,
@@ -1622,10 +1611,6 @@ export {
1622
1611
  downWithBatch,
1623
1612
  monotonicNs,
1624
1613
  wallClockNs,
1625
- GuardDenied,
1626
- policy,
1627
- policyFromRules,
1628
- accessHintForGuard,
1629
1614
  defaultHash,
1630
1615
  createVersioning,
1631
1616
  advanceVersion,
@@ -1643,4 +1628,4 @@ export {
1643
1628
  effect,
1644
1629
  pipe
1645
1630
  };
1646
- //# sourceMappingURL=chunk-BV3TPSBK.js.map
1631
+ //# sourceMappingURL=chunk-YXR3WW3Q.js.map