@graphrefly/graphrefly 0.25.0 → 0.27.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 (231) hide show
  1. package/README.md +8 -0
  2. package/dist/ai-CaR_912Q.d.cts +1033 -0
  3. package/dist/ai-WlRltJV7.d.ts +1033 -0
  4. package/dist/audit-ClmqGOCx.d.cts +245 -0
  5. package/dist/audit-DRlSzBu9.d.ts +245 -0
  6. package/dist/{chunk-QOWVNWOC.js → chunk-3ZWCKRHX.js} +27 -25
  7. package/dist/{chunk-QOWVNWOC.js.map → chunk-3ZWCKRHX.js.map} +1 -1
  8. package/dist/chunk-APFNLIRG.js +62 -0
  9. package/dist/chunk-APFNLIRG.js.map +1 -0
  10. package/dist/chunk-AT5LKYNL.js +395 -0
  11. package/dist/chunk-AT5LKYNL.js.map +1 -0
  12. package/dist/{chunk-IAHGTNOZ.js → chunk-BQ6RQQFF.js} +351 -2095
  13. package/dist/chunk-BQ6RQQFF.js.map +1 -0
  14. package/dist/{chunk-L2GLW2U7.js → chunk-BVZYTZ5H.js} +9 -103
  15. package/dist/chunk-BVZYTZ5H.js.map +1 -0
  16. package/dist/{chunk-EVR6UFUV.js → chunk-DST5DKZS.js} +19 -15
  17. package/dist/{chunk-EVR6UFUV.js.map → chunk-DST5DKZS.js.map} +1 -1
  18. package/dist/{chunk-TKE3JGOH.js → chunk-GTE6PWRZ.js} +5 -692
  19. package/dist/chunk-GTE6PWRZ.js.map +1 -0
  20. package/dist/chunk-HXZEYDUR.js +94 -0
  21. package/dist/chunk-HXZEYDUR.js.map +1 -0
  22. package/dist/chunk-J22W6HV3.js +107 -0
  23. package/dist/chunk-J22W6HV3.js.map +1 -0
  24. package/dist/{chunk-PY4XCDLR.js → chunk-J2VBW3DZ.js} +6 -95
  25. package/dist/chunk-J2VBW3DZ.js.map +1 -0
  26. package/dist/{chunk-HWPIFSW2.js → chunk-JSCT3CR4.js} +6 -4
  27. package/dist/{chunk-HWPIFSW2.js.map → chunk-JSCT3CR4.js.map} +1 -1
  28. package/dist/chunk-JWBCY4NC.js +330 -0
  29. package/dist/chunk-JWBCY4NC.js.map +1 -0
  30. package/dist/chunk-K2AUJHVP.js +2251 -0
  31. package/dist/chunk-K2AUJHVP.js.map +1 -0
  32. package/dist/chunk-MJ2NKQQL.js +119 -0
  33. package/dist/chunk-MJ2NKQQL.js.map +1 -0
  34. package/dist/chunk-N6UR7YVY.js +198 -0
  35. package/dist/chunk-N6UR7YVY.js.map +1 -0
  36. package/dist/chunk-NC6S43JJ.js +456 -0
  37. package/dist/chunk-NC6S43JJ.js.map +1 -0
  38. package/dist/chunk-OFVJBJXR.js +98 -0
  39. package/dist/chunk-OFVJBJXR.js.map +1 -0
  40. package/dist/chunk-OHISZPOJ.js +97 -0
  41. package/dist/chunk-OHISZPOJ.js.map +1 -0
  42. package/dist/chunk-OU5CQKNW.js +102 -0
  43. package/dist/chunk-OU5CQKNW.js.map +1 -0
  44. package/dist/{chunk-XOFWRC73.js → chunk-PF7GRZMW.js} +316 -21
  45. package/dist/chunk-PF7GRZMW.js.map +1 -0
  46. package/dist/{chunk-5DJTTKX3.js → chunk-PHOUUNK7.js} +74 -111
  47. package/dist/chunk-PHOUUNK7.js.map +1 -0
  48. package/dist/chunk-RNHBMHKA.js +1665 -0
  49. package/dist/chunk-RNHBMHKA.js.map +1 -0
  50. package/dist/chunk-SX52TAR4.js +110 -0
  51. package/dist/chunk-SX52TAR4.js.map +1 -0
  52. package/dist/{chunk-H4RVA4VE.js → chunk-VYPWMZ6H.js} +2 -2
  53. package/dist/chunk-WBZOVTYK.js +171 -0
  54. package/dist/chunk-WBZOVTYK.js.map +1 -0
  55. package/dist/chunk-WKNUIZOY.js +354 -0
  56. package/dist/chunk-WKNUIZOY.js.map +1 -0
  57. package/dist/chunk-X3VMZYBT.js +713 -0
  58. package/dist/chunk-X3VMZYBT.js.map +1 -0
  59. package/dist/chunk-X5R3GL6H.js +525 -0
  60. package/dist/chunk-X5R3GL6H.js.map +1 -0
  61. package/dist/chunk-XGPU467M.js +136 -0
  62. package/dist/chunk-XGPU467M.js.map +1 -0
  63. package/dist/compat/index.cjs +7656 -0
  64. package/dist/compat/index.cjs.map +1 -0
  65. package/dist/compat/index.d.cts +18 -0
  66. package/dist/compat/index.d.ts +18 -0
  67. package/dist/compat/index.js +50 -0
  68. package/dist/compat/index.js.map +1 -0
  69. package/dist/compat/jotai/index.cjs +2048 -0
  70. package/dist/compat/jotai/index.cjs.map +1 -0
  71. package/dist/compat/jotai/index.d.cts +2 -0
  72. package/dist/compat/jotai/index.d.ts +2 -0
  73. package/dist/compat/jotai/index.js +9 -0
  74. package/dist/compat/jotai/index.js.map +1 -0
  75. package/dist/compat/nanostores/index.cjs +2175 -0
  76. package/dist/compat/nanostores/index.cjs.map +1 -0
  77. package/dist/compat/nanostores/index.d.cts +2 -0
  78. package/dist/compat/nanostores/index.d.ts +2 -0
  79. package/dist/compat/nanostores/index.js +23 -0
  80. package/dist/compat/nanostores/index.js.map +1 -0
  81. package/dist/compat/nestjs/index.cjs +350 -16
  82. package/dist/compat/nestjs/index.cjs.map +1 -1
  83. package/dist/compat/nestjs/index.d.cts +6 -6
  84. package/dist/compat/nestjs/index.d.ts +6 -6
  85. package/dist/compat/nestjs/index.js +11 -9
  86. package/dist/compat/react/index.cjs +141 -0
  87. package/dist/compat/react/index.cjs.map +1 -0
  88. package/dist/compat/react/index.d.cts +2 -0
  89. package/dist/compat/react/index.d.ts +2 -0
  90. package/dist/compat/react/index.js +12 -0
  91. package/dist/compat/react/index.js.map +1 -0
  92. package/dist/compat/solid/index.cjs +128 -0
  93. package/dist/compat/solid/index.cjs.map +1 -0
  94. package/dist/compat/solid/index.d.cts +2 -0
  95. package/dist/compat/solid/index.d.ts +2 -0
  96. package/dist/compat/solid/index.js +12 -0
  97. package/dist/compat/solid/index.js.map +1 -0
  98. package/dist/compat/svelte/index.cjs +131 -0
  99. package/dist/compat/svelte/index.cjs.map +1 -0
  100. package/dist/compat/svelte/index.d.cts +2 -0
  101. package/dist/compat/svelte/index.d.ts +2 -0
  102. package/dist/compat/svelte/index.js +12 -0
  103. package/dist/compat/svelte/index.js.map +1 -0
  104. package/dist/compat/vue/index.cjs +146 -0
  105. package/dist/compat/vue/index.cjs.map +1 -0
  106. package/dist/compat/vue/index.d.cts +3 -0
  107. package/dist/compat/vue/index.d.ts +3 -0
  108. package/dist/compat/vue/index.js +12 -0
  109. package/dist/compat/vue/index.js.map +1 -0
  110. package/dist/compat/zustand/index.cjs +4931 -0
  111. package/dist/compat/zustand/index.cjs.map +1 -0
  112. package/dist/compat/zustand/index.d.cts +5 -0
  113. package/dist/compat/zustand/index.d.ts +5 -0
  114. package/dist/compat/zustand/index.js +12 -0
  115. package/dist/compat/zustand/index.js.map +1 -0
  116. package/dist/composite-C7PcQvcs.d.cts +303 -0
  117. package/dist/composite-aUCvjZVR.d.ts +303 -0
  118. package/dist/core/index.cjs +53 -4
  119. package/dist/core/index.cjs.map +1 -1
  120. package/dist/core/index.d.cts +4 -3
  121. package/dist/core/index.d.ts +4 -3
  122. package/dist/core/index.js +26 -24
  123. package/dist/demo-shell-BDkOptd6.d.ts +102 -0
  124. package/dist/demo-shell-Crid1WdR.d.cts +102 -0
  125. package/dist/extra/index.cjs +222 -110
  126. package/dist/extra/index.cjs.map +1 -1
  127. package/dist/extra/index.d.cts +6 -4
  128. package/dist/extra/index.d.ts +6 -4
  129. package/dist/extra/index.js +72 -65
  130. package/dist/extra/sources.cjs +2486 -0
  131. package/dist/extra/sources.cjs.map +1 -0
  132. package/dist/extra/sources.d.cts +465 -0
  133. package/dist/extra/sources.d.ts +465 -0
  134. package/dist/extra/sources.js +57 -0
  135. package/dist/extra/sources.js.map +1 -0
  136. package/dist/graph/index.cjs +408 -14
  137. package/dist/graph/index.cjs.map +1 -1
  138. package/dist/graph/index.d.cts +5 -5
  139. package/dist/graph/index.d.ts +5 -5
  140. package/dist/graph/index.js +13 -5
  141. package/dist/{graph-D-3JIQme.d.cts → graph-CCwGKLCm.d.ts} +195 -4
  142. package/dist/{graph-B6NFqv3z.d.ts → graph-DNCrvZSn.d.cts} +195 -4
  143. package/dist/index-3lsddbbS.d.ts +86 -0
  144. package/dist/index-B1tloyhO.d.cts +34 -0
  145. package/dist/{index-CYkjxu3s.d.ts → index-B6D3QNSA.d.ts} +33 -4
  146. package/dist/index-B6EhDnjH.d.cts +37 -0
  147. package/dist/index-B9B7_HEY.d.ts +37 -0
  148. package/dist/{index-Ds23Wvou.d.ts → index-BHlKbUwO.d.cts} +131 -883
  149. package/dist/{index-DiobMNwE.d.ts → index-BPVt8kqc.d.ts} +3 -3
  150. package/dist/index-BaSM3aYt.d.ts +195 -0
  151. package/dist/index-BuEoe-Qu.d.ts +121 -0
  152. package/dist/{index-Ch0IpIO0.d.cts → index-BwfLUNw4.d.ts} +131 -883
  153. package/dist/index-ByQxazQJ.d.cts +86 -0
  154. package/dist/index-C0svESO4.d.ts +127 -0
  155. package/dist/{index-OXImXMq6.d.ts → index-C8oil6M6.d.ts} +18 -196
  156. package/dist/{index-DKE1EATr.d.cts → index-CI3DprxP.d.cts} +18 -196
  157. package/dist/{index-AMWewNDe.d.cts → index-CO8uBlUh.d.cts} +33 -4
  158. package/dist/index-CxFrXH4m.d.ts +45 -0
  159. package/dist/index-D8wS_PeY.d.cts +121 -0
  160. package/dist/index-DO_6JN9Z.d.cts +127 -0
  161. package/dist/index-DVGiGFGT.d.cts +195 -0
  162. package/dist/index-DYme44FM.d.cts +44 -0
  163. package/dist/{index-J7Kc0oIQ.d.cts → index-DlLp-2Xn.d.cts} +3 -3
  164. package/dist/index-Dzk2hrlR.d.ts +44 -0
  165. package/dist/index-VHqptjhu.d.cts +45 -0
  166. package/dist/index-VdHQMPy1.d.ts +36 -0
  167. package/dist/index-Xi3u0HCQ.d.cts +36 -0
  168. package/dist/index-wEn0eFe8.d.ts +34 -0
  169. package/dist/index.cjs +1780 -176
  170. package/dist/index.cjs.map +1 -1
  171. package/dist/index.d.cts +784 -2082
  172. package/dist/index.d.ts +784 -2082
  173. package/dist/index.js +955 -4349
  174. package/dist/index.js.map +1 -1
  175. package/dist/memory-C6Z2tGpC.d.cts +139 -0
  176. package/dist/memory-li6FL5RM.d.ts +139 -0
  177. package/dist/messaging-Gt4LPbyA.d.cts +269 -0
  178. package/dist/messaging-XDoYablx.d.ts +269 -0
  179. package/dist/{meta-DWbkoq1s.d.cts → meta-BxCA7rcr.d.cts} +1 -1
  180. package/dist/{meta-CnkLA_43.d.ts → meta-CbznRPYJ.d.ts} +1 -1
  181. package/dist/{node-B-f-Lu-k.d.cts → node-BmerH3kS.d.cts} +26 -1
  182. package/dist/{node-B-f-Lu-k.d.ts → node-BmerH3kS.d.ts} +26 -1
  183. package/dist/{observable-uP-wy_uK.d.ts → observable-BgGUwcqp.d.ts} +1 -1
  184. package/dist/{observable-DBnrwcar.d.cts → observable-DJt_AxzQ.d.cts} +1 -1
  185. package/dist/patterns/ai.cjs +7930 -0
  186. package/dist/patterns/ai.cjs.map +1 -0
  187. package/dist/patterns/ai.d.cts +10 -0
  188. package/dist/patterns/ai.d.ts +10 -0
  189. package/dist/patterns/ai.js +71 -0
  190. package/dist/patterns/ai.js.map +1 -0
  191. package/dist/patterns/audit.cjs +5805 -0
  192. package/dist/patterns/audit.cjs.map +1 -0
  193. package/dist/patterns/audit.d.cts +6 -0
  194. package/dist/patterns/audit.d.ts +6 -0
  195. package/dist/patterns/audit.js +29 -0
  196. package/dist/patterns/audit.js.map +1 -0
  197. package/dist/patterns/demo-shell.cjs +5604 -0
  198. package/dist/patterns/demo-shell.cjs.map +1 -0
  199. package/dist/patterns/demo-shell.d.cts +6 -0
  200. package/dist/patterns/demo-shell.d.ts +6 -0
  201. package/dist/patterns/demo-shell.js +15 -0
  202. package/dist/patterns/demo-shell.js.map +1 -0
  203. package/dist/patterns/memory.cjs +5283 -0
  204. package/dist/patterns/memory.cjs.map +1 -0
  205. package/dist/patterns/memory.d.cts +5 -0
  206. package/dist/patterns/memory.d.ts +5 -0
  207. package/dist/patterns/memory.js +20 -0
  208. package/dist/patterns/memory.js.map +1 -0
  209. package/dist/patterns/reactive-layout/index.cjs +355 -13
  210. package/dist/patterns/reactive-layout/index.cjs.map +1 -1
  211. package/dist/patterns/reactive-layout/index.d.cts +6 -5
  212. package/dist/patterns/reactive-layout/index.d.ts +6 -5
  213. package/dist/patterns/reactive-layout/index.js +15 -12
  214. package/dist/reactive-layout-MQP--J3F.d.cts +183 -0
  215. package/dist/reactive-layout-u5Ulnqag.d.ts +183 -0
  216. package/dist/{storage-BuTdpCI1.d.cts → storage-CMjUUuxn.d.ts} +10 -2
  217. package/dist/{storage-F2X1U1x0.d.ts → storage-DdWlZo6U.d.cts} +10 -2
  218. package/dist/sugar-CCOxXK1e.d.ts +201 -0
  219. package/dist/sugar-D02n5JjF.d.cts +201 -0
  220. package/package.json +63 -3
  221. package/dist/chunk-5DJTTKX3.js.map +0 -1
  222. package/dist/chunk-IAHGTNOZ.js.map +0 -1
  223. package/dist/chunk-L2GLW2U7.js.map +0 -1
  224. package/dist/chunk-MW4VAKAO.js +0 -47
  225. package/dist/chunk-MW4VAKAO.js.map +0 -1
  226. package/dist/chunk-PY4XCDLR.js.map +0 -1
  227. package/dist/chunk-TKE3JGOH.js.map +0 -1
  228. package/dist/chunk-XOFWRC73.js.map +0 -1
  229. package/dist/index-BJB7t9gg.d.cts +0 -392
  230. package/dist/index-C-TXEa7C.d.ts +0 -392
  231. /package/dist/{chunk-H4RVA4VE.js.map → chunk-VYPWMZ6H.js.map} +0 -0
@@ -0,0 +1,4931 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/compat/zustand/index.ts
21
+ var zustand_exports = {};
22
+ __export(zustand_exports, {
23
+ create: () => create
24
+ });
25
+ module.exports = __toCommonJS(zustand_exports);
26
+
27
+ // src/core/messages.ts
28
+ var START = /* @__PURE__ */ Symbol.for("graphrefly/START");
29
+ var DATA = /* @__PURE__ */ Symbol.for("graphrefly/DATA");
30
+ var DIRTY = /* @__PURE__ */ Symbol.for("graphrefly/DIRTY");
31
+ var RESOLVED = /* @__PURE__ */ Symbol.for("graphrefly/RESOLVED");
32
+ var INVALIDATE = /* @__PURE__ */ Symbol.for("graphrefly/INVALIDATE");
33
+ var PAUSE = /* @__PURE__ */ Symbol.for("graphrefly/PAUSE");
34
+ var RESUME = /* @__PURE__ */ Symbol.for("graphrefly/RESUME");
35
+ var TEARDOWN = /* @__PURE__ */ Symbol.for("graphrefly/TEARDOWN");
36
+ var COMPLETE = /* @__PURE__ */ Symbol.for("graphrefly/COMPLETE");
37
+ var ERROR = /* @__PURE__ */ Symbol.for("graphrefly/ERROR");
38
+ var DIRTY_MSG = Object.freeze([DIRTY]);
39
+ var RESOLVED_MSG = Object.freeze([RESOLVED]);
40
+ var INVALIDATE_MSG = Object.freeze([INVALIDATE]);
41
+ var START_MSG = Object.freeze([START]);
42
+ var COMPLETE_MSG = Object.freeze([COMPLETE]);
43
+ var TEARDOWN_MSG = Object.freeze([TEARDOWN]);
44
+ var DIRTY_ONLY_BATCH = Object.freeze([DIRTY_MSG]);
45
+ var RESOLVED_ONLY_BATCH = Object.freeze([RESOLVED_MSG]);
46
+ var INVALIDATE_ONLY_BATCH = Object.freeze([INVALIDATE_MSG]);
47
+ var COMPLETE_ONLY_BATCH = Object.freeze([COMPLETE_MSG]);
48
+ var TEARDOWN_ONLY_BATCH = Object.freeze([TEARDOWN_MSG]);
49
+
50
+ // src/graph/codec.ts
51
+ var JsonCodec = {
52
+ name: "json",
53
+ version: 1,
54
+ contentType: "application/json",
55
+ encode(snapshot) {
56
+ const json = JSON.stringify(snapshot);
57
+ return new TextEncoder().encode(json);
58
+ },
59
+ decode(buffer, _codecVersion) {
60
+ const json = new TextDecoder().decode(buffer);
61
+ return JSON.parse(json);
62
+ }
63
+ };
64
+ var ENVELOPE_VERSION = 1;
65
+ var ENVELOPE_MIN_LEN = 4;
66
+ function encodeEnvelope(codec, payload) {
67
+ const nameBytes = new TextEncoder().encode(codec.name);
68
+ if (nameBytes.length === 0 || nameBytes.length > 255) {
69
+ throw new Error(
70
+ `encodeEnvelope: codec name "${codec.name}" encodes to ${nameBytes.length} bytes (must be 1\u2013255)`
71
+ );
72
+ }
73
+ const cv = codec.version;
74
+ if (!Number.isInteger(cv) || cv < 0 || cv > 65535) {
75
+ throw new Error(
76
+ `encodeEnvelope: codec.version ${cv} out of u16 range (expected integer 0\u201365535)`
77
+ );
78
+ }
79
+ const totalLen = 1 + 1 + nameBytes.length + 2 + payload.length;
80
+ if (totalLen > 4294967295) {
81
+ throw new Error(
82
+ `encodeEnvelope: total envelope size ${totalLen} exceeds 2^32-1 bytes (payload ${payload.length} bytes)`
83
+ );
84
+ }
85
+ const out = new Uint8Array(totalLen);
86
+ let i = 0;
87
+ out[i++] = ENVELOPE_VERSION;
88
+ out[i++] = nameBytes.length;
89
+ out.set(nameBytes, i);
90
+ i += nameBytes.length;
91
+ out[i++] = cv >>> 8 & 255;
92
+ out[i++] = cv & 255;
93
+ out.set(payload, i);
94
+ return out;
95
+ }
96
+ function decodeEnvelope(bytes, config) {
97
+ if (bytes.length < ENVELOPE_MIN_LEN) {
98
+ throw new Error(`decodeEnvelope: bytes too short (${bytes.length} < ${ENVELOPE_MIN_LEN})`);
99
+ }
100
+ let i = 0;
101
+ const envVersion = bytes[i++];
102
+ if (envVersion !== ENVELOPE_VERSION) {
103
+ throw new Error(
104
+ `decodeEnvelope: unsupported envelope version ${envVersion} (expected ${ENVELOPE_VERSION})`
105
+ );
106
+ }
107
+ const nameLen = bytes[i++];
108
+ if (nameLen === 0) {
109
+ throw new Error("decodeEnvelope: name_len must be >= 1");
110
+ }
111
+ if (i + nameLen + 2 > bytes.length) {
112
+ throw new Error(
113
+ `decodeEnvelope: envelope truncated (need ${i + nameLen + 2} bytes, have ${bytes.length})`
114
+ );
115
+ }
116
+ const name = new TextDecoder().decode(bytes.subarray(i, i + nameLen));
117
+ i += nameLen;
118
+ const codecVersion = (bytes[i] << 8 | bytes[i + 1]) >>> 0;
119
+ i += 2;
120
+ const payload = bytes.subarray(i);
121
+ const codec = config.lookupCodec(name);
122
+ if (codec == null) {
123
+ throw new Error(
124
+ `decodeEnvelope: codec "${name}" not registered (envelope codec_v=${codecVersion})`
125
+ );
126
+ }
127
+ return { codec, codecVersion, payload };
128
+ }
129
+ function registerBuiltinCodecs(config) {
130
+ config.registerCodec(JsonCodec);
131
+ }
132
+
133
+ // src/core/actor.ts
134
+ var DEFAULT_ACTOR = { type: "system", id: "" };
135
+ function normalizeActor(actor) {
136
+ if (actor == null) return DEFAULT_ACTOR;
137
+ const { type, id, ...rest } = actor;
138
+ return {
139
+ type: type ?? "system",
140
+ id: id ?? "",
141
+ ...rest
142
+ };
143
+ }
144
+
145
+ // src/core/batch.ts
146
+ var MAX_DRAIN_ITERATIONS = 1e3;
147
+ var batchDepth = 0;
148
+ var flushInProgress = false;
149
+ var drainPhase2 = [];
150
+ var drainPhase3 = [];
151
+ var drainPhase4 = [];
152
+ var flushHooks = [];
153
+ function isBatching() {
154
+ return batchDepth > 0 || flushInProgress;
155
+ }
156
+ function isExplicitlyBatching() {
157
+ return batchDepth > 0;
158
+ }
159
+ function registerBatchFlushHook(hook) {
160
+ if (batchDepth > 0) {
161
+ flushHooks.push(hook);
162
+ } else {
163
+ hook();
164
+ }
165
+ }
166
+ function batch(fn) {
167
+ batchDepth += 1;
168
+ let threw = false;
169
+ try {
170
+ fn();
171
+ } catch (e) {
172
+ threw = true;
173
+ throw e;
174
+ } finally {
175
+ batchDepth -= 1;
176
+ if (batchDepth === 0) {
177
+ if (threw) {
178
+ if (!flushInProgress) {
179
+ const hooks = flushHooks.splice(0);
180
+ for (const h of hooks) {
181
+ try {
182
+ h();
183
+ } catch {
184
+ }
185
+ }
186
+ drainPhase2.length = 0;
187
+ drainPhase3.length = 0;
188
+ drainPhase4.length = 0;
189
+ }
190
+ } else {
191
+ drainPending();
192
+ }
193
+ }
194
+ }
195
+ }
196
+ function drainPending() {
197
+ const ownsFlush = !flushInProgress;
198
+ if (ownsFlush) flushInProgress = true;
199
+ const errors = [];
200
+ let iterations = 0;
201
+ try {
202
+ while (drainPhase2.length > 0 || drainPhase3.length > 0 || drainPhase4.length > 0 || ownsFlush && flushHooks.length > 0) {
203
+ if (ownsFlush && flushHooks.length > 0) {
204
+ const hooks = flushHooks.splice(0);
205
+ for (const h of hooks) {
206
+ try {
207
+ h();
208
+ } catch (e) {
209
+ errors.push(e);
210
+ }
211
+ }
212
+ continue;
213
+ }
214
+ iterations += 1;
215
+ if (iterations > MAX_DRAIN_ITERATIONS) {
216
+ drainPhase2.length = 0;
217
+ drainPhase3.length = 0;
218
+ drainPhase4.length = 0;
219
+ throw new Error(
220
+ `batch drain exceeded ${MAX_DRAIN_ITERATIONS} iterations \u2014 likely a reactive cycle`
221
+ );
222
+ }
223
+ const queue = drainPhase2.length > 0 ? drainPhase2 : drainPhase3.length > 0 ? drainPhase3 : drainPhase4;
224
+ const ops = queue.splice(0);
225
+ for (const run of ops) {
226
+ try {
227
+ run();
228
+ } catch (e) {
229
+ errors.push(e);
230
+ }
231
+ }
232
+ }
233
+ } finally {
234
+ if (ownsFlush) flushInProgress = false;
235
+ }
236
+ if (errors.length === 1) throw errors[0];
237
+ if (errors.length > 1) {
238
+ throw new AggregateError(errors, "batch drain: multiple callbacks threw");
239
+ }
240
+ }
241
+ function downWithBatch(sink, messages, tierOf) {
242
+ if (messages.length === 0) return;
243
+ if (messages.length === 1) {
244
+ const tier = tierOf(messages[0][0]);
245
+ if (tier < 3 || !isBatching()) {
246
+ sink(messages);
247
+ return;
248
+ }
249
+ const queue = tier >= 5 ? drainPhase4 : tier === 4 ? drainPhase3 : drainPhase2;
250
+ queue.push(() => sink(messages));
251
+ return;
252
+ }
253
+ const n = messages.length;
254
+ let phase2Start = n;
255
+ let phase3Start = n;
256
+ let phase4Start = n;
257
+ let i = 0;
258
+ while (i < n && tierOf(messages[i][0]) < 3) i++;
259
+ phase2Start = i;
260
+ while (i < n && tierOf(messages[i][0]) === 3) i++;
261
+ phase3Start = i;
262
+ while (i < n && tierOf(messages[i][0]) === 4) i++;
263
+ phase4Start = i;
264
+ const batching = isBatching();
265
+ if (phase2Start > 0) {
266
+ const immediate = messages.slice(0, phase2Start);
267
+ sink(immediate);
268
+ }
269
+ if (phase3Start > phase2Start) {
270
+ const phase2 = messages.slice(phase2Start, phase3Start);
271
+ if (batching) drainPhase2.push(() => sink(phase2));
272
+ else sink(phase2);
273
+ }
274
+ if (phase4Start > phase3Start) {
275
+ const phase3 = messages.slice(phase3Start, phase4Start);
276
+ if (batching) drainPhase3.push(() => sink(phase3));
277
+ else sink(phase3);
278
+ }
279
+ if (n > phase4Start) {
280
+ const phase4 = messages.slice(phase4Start, n);
281
+ if (batching) drainPhase4.push(() => sink(phase4));
282
+ else sink(phase4);
283
+ }
284
+ }
285
+
286
+ // src/core/clock.ts
287
+ function monotonicNs() {
288
+ return Math.trunc(performance.now() * 1e6);
289
+ }
290
+ function wallClockNs() {
291
+ return Date.now() * 1e6;
292
+ }
293
+
294
+ // src/core/config.ts
295
+ var GraphReFlyConfig = class {
296
+ _messageTypes = /* @__PURE__ */ new Map();
297
+ _codecs = /* @__PURE__ */ new Map();
298
+ _onMessage;
299
+ _onSubscribe;
300
+ _defaultVersioning;
301
+ _defaultHashFn;
302
+ _inspectorEnabled = !(typeof process !== "undefined" && process.env?.NODE_ENV === "production");
303
+ _globalInspector;
304
+ _frozen = false;
305
+ /**
306
+ * Pre-bound tier lookup — shared by every node bound to this config. Since
307
+ * the registry is frozen on first hook access, this closure can be built
308
+ * once in the constructor and handed directly to `downWithBatch` /
309
+ * `_frameBatch` paths without per-node or per-emission `.bind(config)`
310
+ * allocation.
311
+ */
312
+ tierOf;
313
+ constructor(init) {
314
+ this._onMessage = init.onMessage;
315
+ this._onSubscribe = init.onSubscribe;
316
+ this._defaultVersioning = init.defaultVersioning;
317
+ this._defaultHashFn = init.defaultHashFn;
318
+ this.tierOf = (t) => {
319
+ const reg = this._messageTypes.get(t);
320
+ return reg != null ? reg.tier : 1;
321
+ };
322
+ }
323
+ // --- Hook getters (freeze on read) ---
324
+ get onMessage() {
325
+ this._frozen = true;
326
+ return this._onMessage;
327
+ }
328
+ get onSubscribe() {
329
+ this._frozen = true;
330
+ return this._onSubscribe;
331
+ }
332
+ // --- Hook setters (throw when frozen) ---
333
+ set onMessage(v) {
334
+ this._assertUnfrozen();
335
+ this._onMessage = v;
336
+ }
337
+ set onSubscribe(v) {
338
+ this._assertUnfrozen();
339
+ this._onSubscribe = v;
340
+ }
341
+ /**
342
+ * Default versioning level applied to every node bound to this config,
343
+ * unless the node's own `opts.versioning` provides an explicit override.
344
+ * Setting this is only allowed before the config freezes (i.e., before
345
+ * the first node is created) so every node in the graph sees a
346
+ * consistent starting level. Individual nodes can still opt into a
347
+ * higher level via `opts.versioning`, or post-hoc via
348
+ * `NodeImpl._applyVersioning(level)` when the node is quiescent.
349
+ *
350
+ * v0 is the minimum opt-in — unversioned nodes (`undefined`) skip
351
+ * the version counter entirely. v1 adds content-addressed cid.
352
+ * Future levels (v2, v3) are reserved for linked-history and
353
+ * cryptographic attestation extensions.
354
+ */
355
+ get defaultVersioning() {
356
+ return this._defaultVersioning;
357
+ }
358
+ set defaultVersioning(v) {
359
+ this._assertUnfrozen();
360
+ this._defaultVersioning = v;
361
+ }
362
+ /**
363
+ * Default content-hash function applied to every versioned node bound
364
+ * to this config, unless the node's own `opts.versioningHash` provides
365
+ * an explicit override. Use this when a graph needs a non-default hash
366
+ * — e.g., swap the vendored sync SHA-256 for a faster non-crypto hash
367
+ * (xxHash, FNV-1a) in hot-path workloads, or a stronger hash when
368
+ * versioning v1 cids are used as audit anchors.
369
+ *
370
+ * Only settable before the config freezes. Individual nodes can still
371
+ * override via `opts.versioningHash`.
372
+ */
373
+ get defaultHashFn() {
374
+ return this._defaultHashFn;
375
+ }
376
+ set defaultHashFn(v) {
377
+ this._assertUnfrozen();
378
+ this._defaultHashFn = v;
379
+ }
380
+ /**
381
+ * When `false`, structured observation options (`causal`, `timeline`)
382
+ * and `Graph.trace()` writes are no-ops. Raw `Graph.observe()` always
383
+ * works. Default: `true` outside production (`NODE_ENV !== "production"`).
384
+ *
385
+ * Settable at any time — inspector gating is an operational concern, not
386
+ * a protocol invariant, so it does NOT require freeze before node creation.
387
+ */
388
+ get inspectorEnabled() {
389
+ return this._inspectorEnabled;
390
+ }
391
+ set inspectorEnabled(v) {
392
+ this._inspectorEnabled = v;
393
+ }
394
+ /**
395
+ * Process-global observability hook (Redux-DevTools-style full-graph
396
+ * tracer). Fires once per outgoing batch from every node bound to this
397
+ * config, gated by {@link inspectorEnabled}. See {@link GlobalInspectorHook}.
398
+ *
399
+ * Settable at any time — like {@link inspectorEnabled} this is operational,
400
+ * not protocol-shaping, so it does NOT trigger config freeze.
401
+ */
402
+ get globalInspector() {
403
+ return this._globalInspector;
404
+ }
405
+ set globalInspector(v) {
406
+ this._globalInspector = v;
407
+ }
408
+ // --- Registry (writes require unfrozen; reads are free lookups) ---
409
+ /**
410
+ * Register a custom message type. Must be called before any node that
411
+ * uses this config has been created — otherwise throws. Default
412
+ * `wireCrossing` is `tier >= 3`.
413
+ */
414
+ registerMessageType(t, input) {
415
+ this._assertUnfrozen();
416
+ this._messageTypes.set(t, {
417
+ tier: input.tier,
418
+ wireCrossing: input.wireCrossing ?? input.tier >= 3,
419
+ metaPassthrough: input.metaPassthrough ?? true
420
+ });
421
+ return this;
422
+ }
423
+ /** Tier for `t`. Unknown types default to tier 1 (immediate, after START). */
424
+ messageTier(t) {
425
+ const reg = this._messageTypes.get(t);
426
+ return reg != null ? reg.tier : 1;
427
+ }
428
+ /**
429
+ * Whether `t` is registered as wire-crossing. Unknown types default to
430
+ * `true` (spec §1.3.6 forward-compat — unknowns cross the wire).
431
+ */
432
+ isWireCrossing(t) {
433
+ const reg = this._messageTypes.get(t);
434
+ return reg != null ? reg.wireCrossing : true;
435
+ }
436
+ /** Convenience inverse of {@link isWireCrossing}. */
437
+ isLocalOnly(t) {
438
+ return !this.isWireCrossing(t);
439
+ }
440
+ /**
441
+ * Whether `t` is forwarded to meta companions by `Graph.signal`. Defaults
442
+ * to `true` for unknowns (forward-compat — new types pass through meta by
443
+ * default; opt-in filter via `registerMessageType({metaPassthrough: false})`).
444
+ */
445
+ isMetaPassthrough(t) {
446
+ const reg = this._messageTypes.get(t);
447
+ return reg != null ? reg.metaPassthrough : true;
448
+ }
449
+ /** Whether `t` is a registered (built-in or custom) type. */
450
+ isKnownMessageType(t) {
451
+ return this._messageTypes.has(t);
452
+ }
453
+ // --- Codec registry (writes require unfrozen; reads are free lookups) ---
454
+ /**
455
+ * Register a graph codec by `codec.name`. Used by the envelope-based
456
+ * `graph.snapshot({format: "bytes", codec: name})` path and
457
+ * `Graph.decode(bytes)` auto-dispatch. Must be called before any node
458
+ * bound to this config is created — otherwise throws.
459
+ *
460
+ * Re-registering the same name overwrites, so user codecs can shadow
461
+ * built-in ones before freeze (e.g., to swap a zstd-wrapped dag-cbor in
462
+ * for `"dag-cbor"`).
463
+ */
464
+ registerCodec(codec) {
465
+ this._assertUnfrozen();
466
+ this._codecs.set(codec.name, codec);
467
+ return this;
468
+ }
469
+ /**
470
+ * Resolve a registered codec by name. Returns `undefined` for unknown
471
+ * names. Typed callers cast to their concrete codec interface (e.g.,
472
+ * `config.lookupCodec<GraphCodec>("json")`) — this method stays
473
+ * layer-pure (no import of graph-layer types into `core/`).
474
+ */
475
+ lookupCodec(name) {
476
+ return this._codecs.get(name);
477
+ }
478
+ /** @internal Used by tests and dev tooling — check freeze state without triggering it. */
479
+ _isFrozen() {
480
+ return this._frozen;
481
+ }
482
+ _assertUnfrozen() {
483
+ if (this._frozen) {
484
+ throw new Error(
485
+ "GraphReFlyConfig is frozen: a node has already captured this config. Register custom types and set hooks before creating any node."
486
+ );
487
+ }
488
+ }
489
+ };
490
+ function registerBuiltins(cfg) {
491
+ cfg.registerMessageType(START, { tier: 0, wireCrossing: false });
492
+ cfg.registerMessageType(DIRTY, { tier: 1, wireCrossing: false });
493
+ cfg.registerMessageType(INVALIDATE, {
494
+ tier: 1,
495
+ wireCrossing: false,
496
+ metaPassthrough: false
497
+ });
498
+ cfg.registerMessageType(PAUSE, { tier: 2, wireCrossing: false });
499
+ cfg.registerMessageType(RESUME, { tier: 2, wireCrossing: false });
500
+ cfg.registerMessageType(DATA, { tier: 3, wireCrossing: true });
501
+ cfg.registerMessageType(RESOLVED, { tier: 3, wireCrossing: true });
502
+ cfg.registerMessageType(COMPLETE, {
503
+ tier: 4,
504
+ wireCrossing: true,
505
+ metaPassthrough: false
506
+ });
507
+ cfg.registerMessageType(ERROR, {
508
+ tier: 4,
509
+ wireCrossing: true,
510
+ metaPassthrough: false
511
+ });
512
+ cfg.registerMessageType(TEARDOWN, {
513
+ tier: 5,
514
+ wireCrossing: true,
515
+ metaPassthrough: false
516
+ });
517
+ }
518
+
519
+ // src/core/guard.ts
520
+ var GuardDenied = class extends Error {
521
+ actor;
522
+ action;
523
+ nodeName;
524
+ /**
525
+ * @param details - Actor, action, and optional node name for the denial.
526
+ * @param message - Optional override for the default error message.
527
+ */
528
+ constructor(details, message) {
529
+ super(
530
+ message ?? `GuardDenied: action "${String(details.action)}" denied for actor type "${String(details.actor.type)}"`
531
+ );
532
+ this.name = "GuardDenied";
533
+ this.actor = details.actor;
534
+ this.action = details.action;
535
+ this.nodeName = details.nodeName;
536
+ }
537
+ /** Qualified registry path when known (roadmap diagnostics: same as {@link nodeName}). */
538
+ get node() {
539
+ return this.nodeName;
540
+ }
541
+ };
542
+ var STANDARD_WRITE_TYPES = ["human", "llm", "wallet", "system"];
543
+ function accessHintForGuard(guard) {
544
+ const allowed = STANDARD_WRITE_TYPES.filter((t) => guard({ type: t, id: "" }, "write"));
545
+ if (allowed.length === 0) return "restricted";
546
+ if (allowed.includes("human") && allowed.includes("llm") && allowed.every((t) => t === "human" || t === "llm" || t === "system")) {
547
+ return "both";
548
+ }
549
+ if (allowed.length === 1) return allowed[0];
550
+ return allowed.join("+");
551
+ }
552
+
553
+ // src/core/versioning.ts
554
+ function canonicalizeForHash(value) {
555
+ if (value === void 0) return null;
556
+ if (typeof value === "number") {
557
+ if (!Number.isFinite(value)) {
558
+ throw new TypeError(`Cannot hash non-finite number: ${value}`);
559
+ }
560
+ if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
561
+ throw new TypeError(
562
+ `Cannot hash integer outside safe range (|n| > 2^53-1): ${value}. Cross-language cid parity is not guaranteed for unsafe integers.`
563
+ );
564
+ }
565
+ return value;
566
+ }
567
+ if (typeof value === "string" || typeof value === "boolean" || value === null) {
568
+ return value;
569
+ }
570
+ if (Array.isArray(value)) {
571
+ return value.map(canonicalizeForHash);
572
+ }
573
+ if (typeof value === "object" && value !== null) {
574
+ const sorted = {};
575
+ for (const k of Object.keys(value).sort()) {
576
+ sorted[k] = canonicalizeForHash(value[k]);
577
+ }
578
+ return sorted;
579
+ }
580
+ return null;
581
+ }
582
+ var SHA256_K = /* @__PURE__ */ new Uint32Array([
583
+ 1116352408,
584
+ 1899447441,
585
+ 3049323471,
586
+ 3921009573,
587
+ 961987163,
588
+ 1508970993,
589
+ 2453635748,
590
+ 2870763221,
591
+ 3624381080,
592
+ 310598401,
593
+ 607225278,
594
+ 1426881987,
595
+ 1925078388,
596
+ 2162078206,
597
+ 2614888103,
598
+ 3248222580,
599
+ 3835390401,
600
+ 4022224774,
601
+ 264347078,
602
+ 604807628,
603
+ 770255983,
604
+ 1249150122,
605
+ 1555081692,
606
+ 1996064986,
607
+ 2554220882,
608
+ 2821834349,
609
+ 2952996808,
610
+ 3210313671,
611
+ 3336571891,
612
+ 3584528711,
613
+ 113926993,
614
+ 338241895,
615
+ 666307205,
616
+ 773529912,
617
+ 1294757372,
618
+ 1396182291,
619
+ 1695183700,
620
+ 1986661051,
621
+ 2177026350,
622
+ 2456956037,
623
+ 2730485921,
624
+ 2820302411,
625
+ 3259730800,
626
+ 3345764771,
627
+ 3516065817,
628
+ 3600352804,
629
+ 4094571909,
630
+ 275423344,
631
+ 430227734,
632
+ 506948616,
633
+ 659060556,
634
+ 883997877,
635
+ 958139571,
636
+ 1322822218,
637
+ 1537002063,
638
+ 1747873779,
639
+ 1955562222,
640
+ 2024104815,
641
+ 2227730452,
642
+ 2361852424,
643
+ 2428436474,
644
+ 2756734187,
645
+ 3204031479,
646
+ 3329325298
647
+ ]);
648
+ var UTF8_ENCODER = /* @__PURE__ */ new TextEncoder();
649
+ function sha256Hex(msg) {
650
+ const bytes = UTF8_ENCODER.encode(msg);
651
+ const msgLen = bytes.length;
652
+ const bitLen = msgLen * 8;
653
+ const totalLen = msgLen + 9 + 63 & ~63;
654
+ const padded = new Uint8Array(totalLen);
655
+ padded.set(bytes);
656
+ padded[msgLen] = 128;
657
+ const dv = new DataView(padded.buffer);
658
+ dv.setUint32(totalLen - 4, bitLen >>> 0, false);
659
+ dv.setUint32(totalLen - 8, Math.floor(bitLen / 4294967296) >>> 0, false);
660
+ let h0 = 1779033703;
661
+ let h1 = 3144134277;
662
+ let h2 = 1013904242;
663
+ let h3 = 2773480762;
664
+ let h4 = 1359893119;
665
+ let h5 = 2600822924;
666
+ let h6 = 528734635;
667
+ let h7 = 1541459225;
668
+ const W = new Uint32Array(64);
669
+ const rotr = (x, n) => x >>> n | x << 32 - n;
670
+ for (let off = 0; off < totalLen; off += 64) {
671
+ for (let i = 0; i < 16; i++) W[i] = dv.getUint32(off + i * 4, false);
672
+ for (let i = 16; i < 64; i++) {
673
+ const w15 = W[i - 15];
674
+ const w2 = W[i - 2];
675
+ const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ w15 >>> 3;
676
+ const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ w2 >>> 10;
677
+ W[i] = W[i - 16] + s0 + W[i - 7] + s1 >>> 0;
678
+ }
679
+ let a = h0;
680
+ let b = h1;
681
+ let c = h2;
682
+ let d = h3;
683
+ let e = h4;
684
+ let f = h5;
685
+ let g = h6;
686
+ let h = h7;
687
+ for (let i = 0; i < 64; i++) {
688
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
689
+ const ch = e & f ^ ~e & g;
690
+ const t1 = h + S1 + ch + SHA256_K[i] + W[i] >>> 0;
691
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
692
+ const mj = a & b ^ a & c ^ b & c;
693
+ const t2 = S0 + mj >>> 0;
694
+ h = g;
695
+ g = f;
696
+ f = e;
697
+ e = d + t1 >>> 0;
698
+ d = c;
699
+ c = b;
700
+ b = a;
701
+ a = t1 + t2 >>> 0;
702
+ }
703
+ h0 = h0 + a >>> 0;
704
+ h1 = h1 + b >>> 0;
705
+ h2 = h2 + c >>> 0;
706
+ h3 = h3 + d >>> 0;
707
+ h4 = h4 + e >>> 0;
708
+ h5 = h5 + f >>> 0;
709
+ h6 = h6 + g >>> 0;
710
+ h7 = h7 + h >>> 0;
711
+ }
712
+ const toHex = (x) => x.toString(16).padStart(8, "0");
713
+ return toHex(h0) + toHex(h1) + toHex(h2) + toHex(h3) + toHex(h4) + toHex(h5) + toHex(h6) + toHex(h7);
714
+ }
715
+ function defaultHash(value) {
716
+ const canonical = canonicalizeForHash(value ?? null);
717
+ const json = JSON.stringify(canonical);
718
+ return sha256Hex(json).slice(0, 16);
719
+ }
720
+ function randomUuid() {
721
+ const c = globalThis.crypto;
722
+ if (c?.randomUUID) return c.randomUUID();
723
+ const r = () => Math.floor(Math.random() * 4294967296).toString(16).padStart(8, "0");
724
+ const hex = r() + r() + r() + r();
725
+ 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)}`;
726
+ }
727
+ function createVersioning(level, initialValue, opts) {
728
+ const id = opts?.id ?? randomUuid();
729
+ if (level === 0) {
730
+ return { id, version: 0 };
731
+ }
732
+ const hash = opts?.hash ?? defaultHash;
733
+ const cid = hash(initialValue);
734
+ return { id, version: 0, cid, prev: null };
735
+ }
736
+ function advanceVersion(info, newValue, hashFn) {
737
+ info.version += 1;
738
+ if ("cid" in info) {
739
+ info.prev = info.cid;
740
+ info.cid = hashFn(newValue);
741
+ }
742
+ }
743
+
744
+ // src/core/node.ts
745
+ var noopUnsub = () => {
746
+ };
747
+ var MAX_RERUN_DEPTH = 100;
748
+ function createDepRecord(n) {
749
+ return {
750
+ node: n,
751
+ unsub: null,
752
+ prevData: void 0,
753
+ dirty: false,
754
+ involvedThisWave: false,
755
+ dataBatch: [],
756
+ terminal: void 0
757
+ };
758
+ }
759
+ function resetDepRecord(d) {
760
+ d.prevData = void 0;
761
+ d.dirty = false;
762
+ d.involvedThisWave = false;
763
+ d.dataBatch.length = 0;
764
+ d.terminal = void 0;
765
+ }
766
+ function normalizeMessages(input) {
767
+ if (input.length === 0) return input;
768
+ return typeof input[0] === "symbol" ? [input] : input;
769
+ }
770
+ var defaultOnMessage = (node2, msg, ctx, _actions) => {
771
+ if (ctx.direction === "down-in") {
772
+ node2._onDepMessage(ctx.depIndex, msg);
773
+ }
774
+ return void 0;
775
+ };
776
+ var defaultOnSubscribe = (node2, sink, _ctx, _actions) => {
777
+ const impl = node2;
778
+ if (impl._status === "completed" || impl._status === "errored") return;
779
+ const cached = impl._cached;
780
+ const initial = cached === void 0 ? [START_MSG] : [START_MSG, [DATA, cached]];
781
+ if (impl._status === "dirty") initial.push(DIRTY_MSG);
782
+ downWithBatch(sink, initial, impl._config.tierOf);
783
+ };
784
+ var defaultConfig = new GraphReFlyConfig({
785
+ onMessage: defaultOnMessage,
786
+ onSubscribe: defaultOnSubscribe
787
+ });
788
+ registerBuiltins(defaultConfig);
789
+ registerBuiltinCodecs(defaultConfig);
790
+ var NodeImpl = class _NodeImpl {
791
+ // --- Identity ---
792
+ _optsName;
793
+ _describeKind;
794
+ meta;
795
+ /**
796
+ * Cached `Object.keys(meta).length > 0` check. `meta` is frozen at
797
+ * construction so this boolean never flips. Used by `_emit` to skip
798
+ * the meta TEARDOWN fan-out block allocation on the common "no meta"
799
+ * hot path.
800
+ */
801
+ _hasMeta;
802
+ // --- Config ---
803
+ _config;
804
+ // --- Topology ---
805
+ /** Mutable for autoTrackNode / Graph.connect() post-construction dep addition. */
806
+ _deps;
807
+ _sinks = null;
808
+ _sinkCount = 0;
809
+ // --- State ---
810
+ _cached;
811
+ _status;
812
+ _cleanup;
813
+ _store = {};
814
+ _waveHasNewData = false;
815
+ _hasNewTerminal = false;
816
+ _hasCalledFnOnce = false;
817
+ _paused = false;
818
+ _pendingWave = false;
819
+ _isExecutingFn = false;
820
+ _pendingRerun = false;
821
+ _rerunDepth = 0;
822
+ // --- Settlement counter (A3) ---
823
+ /**
824
+ * Count of deps currently in `dirty === true`. `_maybeRunFnOnSettlement`
825
+ * treats `0` as "wave settled" — O(1) check for full dep settlement.
826
+ */
827
+ _dirtyDepCount = 0;
828
+ // --- Per-batch emit accumulator (Bug 2: K+1 fan-in fix) ---
829
+ /**
830
+ * Inside an explicit `batch(() => ...)` scope, every `_emit` accumulates
831
+ * its already-framed messages here instead of dispatching synchronously.
832
+ * At batch end, `_flushBatchPending` runs (registered via
833
+ * `registerBatchFlushHook`) and delivers the whole accumulated batch as
834
+ * one `downWithBatch` call — collapsing what would otherwise be K
835
+ * separate sink invocations into one. This is the fix for the diamond
836
+ * fan-in K+1 over-fire.
837
+ *
838
+ * `null` outside batch (or after flush). Only ever appended to within
839
+ * a single explicit batch lifetime; reset to `null` on flush. State
840
+ * updates (cache, version, status) still happen per-emit via
841
+ * `_updateState` — only the downstream delivery is coalesced.
842
+ */
843
+ _batchPendingMessages = null;
844
+ // --- PAUSE/RESUME lock tracking (C0) ---
845
+ /**
846
+ * Set of active pause locks held against this node. Every `[PAUSE, lockId]`
847
+ * adds its `lockId` to the set; every `[RESUME, lockId]` removes it.
848
+ * `_paused` is a derived quantity: `_pauseLocks.size > 0`. Multi-pauser
849
+ * correctness — one controller releasing its lock does NOT resume the
850
+ * node while another controller still holds its lock.
851
+ */
852
+ _pauseLocks = null;
853
+ /**
854
+ * Buffered DATA messages held while paused. Only populated when
855
+ * `_pausable === "resumeAll"` (bufferAll mode). On final lock release
856
+ * the buffer is replayed through the node's outgoing pipeline in the
857
+ * order received. Non-bufferAll pause mode drops DATA on the floor
858
+ * (upstream is expected to honor PAUSE by suppressing production).
859
+ */
860
+ _pauseBuffer = null;
861
+ // --- Options (frozen at construction) ---
862
+ _fn;
863
+ _equals;
864
+ _resubscribable;
865
+ _resetOnTeardown;
866
+ _autoComplete;
867
+ _autoError;
868
+ _pausable;
869
+ _guard;
870
+ /**
871
+ * @internal Additional guards stacked at runtime via {@link NodeImpl._pushGuard}
872
+ * (e.g. by `policyEnforcer({ mode: "enforce" })`, roadmap §9.2). Effective
873
+ * write/signal/observe checks AND the original `_guard` with every entry here.
874
+ */
875
+ _extraGuards;
876
+ _hashFn;
877
+ _versioning;
878
+ /**
879
+ * Explicit versioning level, tracked separately from `_versioning` so
880
+ * monotonicity checks and future v2/v3 extensions don't rely on the
881
+ * fragile `"cid" in _versioning` shape discriminator. `undefined` means
882
+ * the node has no versioning attached; `0` / `1` / future levels name
883
+ * the tier. Mutated in lockstep with `_versioning` by the constructor
884
+ * and by `_applyVersioning`.
885
+ */
886
+ _versioningLevel;
887
+ // --- ABAC ---
888
+ _lastMutation;
889
+ /**
890
+ * @internal Per-node inspector hooks for `Graph.observe(path,
891
+ * { causal, derived })`. Fires in `_onDepMessage` and `_execFn`.
892
+ * Attached via `_setInspectorHook` (returns a disposer). Multiple
893
+ * observers can attach simultaneously — all registered hooks fire for
894
+ * every event.
895
+ */
896
+ _inspectorHooks;
897
+ // --- Actions (built once in the constructor) ---
898
+ _actions;
899
+ constructor(deps, fn, opts) {
900
+ this._config = opts.config ?? defaultConfig;
901
+ void this._config.onMessage;
902
+ this._optsName = opts.name;
903
+ this._describeKind = opts.describeKind;
904
+ this._equals = opts.equals ?? Object.is;
905
+ this._resubscribable = opts.resubscribable ?? false;
906
+ this._resetOnTeardown = opts.resetOnTeardown ?? false;
907
+ this._autoComplete = opts.completeWhenDepsComplete ?? true;
908
+ this._autoError = opts.errorWhenDepsError ?? true;
909
+ this._pausable = opts.pausable ?? true;
910
+ this._guard = opts.guard;
911
+ this._fn = fn;
912
+ this._cached = opts.initial !== void 0 ? opts.initial : void 0;
913
+ this._status = deps.length === 0 && fn == null && this._cached !== void 0 ? "settled" : "sentinel";
914
+ this._hashFn = opts.versioningHash ?? this._config.defaultHashFn ?? defaultHash;
915
+ const versioningLevel = opts.versioning ?? this._config.defaultVersioning;
916
+ this._versioningLevel = versioningLevel;
917
+ this._versioning = versioningLevel != null ? createVersioning(versioningLevel, this._cached === void 0 ? void 0 : this._cached, {
918
+ id: opts.versioningId,
919
+ hash: this._hashFn
920
+ }) : void 0;
921
+ this._deps = deps.map(createDepRecord);
922
+ const meta = {};
923
+ for (const [k, v] of Object.entries(opts.meta ?? {})) {
924
+ const metaOpts = {
925
+ initial: v,
926
+ name: `${opts.name ?? "node"}:meta:${k}`,
927
+ describeKind: "state",
928
+ config: this._config
929
+ };
930
+ if (opts.guard != null) metaOpts.guard = opts.guard;
931
+ meta[k] = new _NodeImpl([], void 0, metaOpts);
932
+ }
933
+ Object.freeze(meta);
934
+ this.meta = meta;
935
+ this._hasMeta = Object.keys(meta).length > 0;
936
+ const self = this;
937
+ this._actions = {
938
+ emit(value) {
939
+ self._emit([[DATA, value]]);
940
+ },
941
+ down(messageOrMessages) {
942
+ self._emit(normalizeMessages(messageOrMessages));
943
+ },
944
+ up(messageOrMessages) {
945
+ self._emitUp(normalizeMessages(messageOrMessages));
946
+ }
947
+ };
948
+ this.down = this.down.bind(this);
949
+ this.up = this.up.bind(this);
950
+ }
951
+ // --- Derived state ---
952
+ get _isTerminal() {
953
+ return this._status === "completed" || this._status === "errored";
954
+ }
955
+ // --- Public getters ---
956
+ get name() {
957
+ return this._optsName;
958
+ }
959
+ get status() {
960
+ return this._status;
961
+ }
962
+ get cache() {
963
+ return this._cached === void 0 ? void 0 : this._cached;
964
+ }
965
+ get lastMutation() {
966
+ return this._lastMutation;
967
+ }
968
+ get v() {
969
+ return this._versioning;
970
+ }
971
+ hasGuard() {
972
+ return this._guard != null;
973
+ }
974
+ /**
975
+ * @internal Retroactively attach (or upgrade) versioning state on this
976
+ * node. Intended for `Graph.setVersioning(level)` bulk application and
977
+ * for rare cases where a specific node needs to be bumped to a higher
978
+ * level (e.g., `v0 → v1`) after construction.
979
+ *
980
+ * **Safety:** the mutation is rejected mid-wave. Specifically,
981
+ * throws if the node is currently executing its fn (`_isExecutingFn`).
982
+ * Callers at quiescent points — before the first sink subscribes, or
983
+ * after all sinks unsubscribe, or between external `down()` / `emit()`
984
+ * invocations — are safe. The re-entrance window that motivated §10.6.4
985
+ * removal was the "transition `_versioning` from `undefined` to a fresh
986
+ * object mid-`_updateState`" case; that path is now guarded.
987
+ *
988
+ * **Monotonicity:** levels can only go up. Downgrade (e.g., `v1 → v0`)
989
+ * is a no-op — once a node carries higher-level metadata, dropping it
990
+ * mid-graph would tear the linked-history invariant for v1 and above.
991
+ *
992
+ * **Linked-history boundary (D1, 2026-04-13):** upgrading v0 → v1
993
+ * produces a **fresh history root**. The new v1 state has `cid =
994
+ * hash(currentCachedValue)` and `prev = null`, not a synthetic `prev`
995
+ * anchored to any previous v0 value. The v0 monotonic `version` counter
996
+ * is preserved across the upgrade, but the linked-cid chain (spec §7)
997
+ * starts fresh at the upgrade point. Downstream audit tools that walk
998
+ * `v.cid.prev` backwards through time will see a `null` boundary at
999
+ * the upgrade — **this is intentional**: v0 had no cid to link to, and
1000
+ * fabricating one would lie about the hash. Callers that require an
1001
+ * unbroken cid chain from birth must attach versioning at construction
1002
+ * via `opts.versioning` or `config.defaultVersioning`, not retroactively.
1003
+ *
1004
+ * @param level - New minimum versioning level.
1005
+ * @param opts - Optional id / hash overrides; applied only if the
1006
+ * node currently has no versioning state.
1007
+ */
1008
+ _applyVersioning(level, opts) {
1009
+ if (this._isExecutingFn) {
1010
+ throw new Error(
1011
+ `Node "${this.name}": _applyVersioning cannot run mid-fn \u2014 call it outside of \`_execFn\` (typically at graph setup time before the first subscribe).`
1012
+ );
1013
+ }
1014
+ const currentLevel = this._versioningLevel;
1015
+ if (currentLevel != null && level <= currentLevel) {
1016
+ return;
1017
+ }
1018
+ const hash = opts?.hash ?? this._hashFn;
1019
+ if (hash !== this._hashFn) this._hashFn = hash;
1020
+ const initialValue = this._cached === void 0 ? void 0 : this._cached;
1021
+ const current = this._versioning;
1022
+ const preservedId = current?.id ?? opts?.id;
1023
+ const preservedVersion = current?.version ?? 0;
1024
+ const fresh = createVersioning(level, initialValue, {
1025
+ id: preservedId,
1026
+ hash
1027
+ });
1028
+ fresh.version = preservedVersion;
1029
+ this._versioning = fresh;
1030
+ this._versioningLevel = level;
1031
+ }
1032
+ /**
1033
+ * @internal Attach an inspector hook. Returns a disposer that removes
1034
+ * the hook. Used by `Graph.observe(path, { causal, derived })` to build
1035
+ * causal traces. Multiple hooks may be attached concurrently — all fire
1036
+ * for every event in registration order. Passing `undefined` is a no-op
1037
+ * and returns a no-op disposer.
1038
+ */
1039
+ _setInspectorHook(hook) {
1040
+ if (hook == null) return () => {
1041
+ };
1042
+ if (this._inspectorHooks == null) this._inspectorHooks = /* @__PURE__ */ new Set();
1043
+ this._inspectorHooks.add(hook);
1044
+ return () => {
1045
+ this._inspectorHooks?.delete(hook);
1046
+ if (this._inspectorHooks?.size === 0) this._inspectorHooks = void 0;
1047
+ };
1048
+ }
1049
+ /**
1050
+ * @internal Push an additional guard onto this node. Effective enforcement
1051
+ * is the AND of `_guard` and every guard pushed via this hook — any one
1052
+ * rejecting throws {@link GuardDenied}. Returns a disposer that removes
1053
+ * the pushed guard. Multiple guards may be stacked simultaneously.
1054
+ *
1055
+ * Used by `policyEnforcer({ mode: "enforce" })` (roadmap §9.2) to overlay
1056
+ * runtime constraint enforcement onto an existing graph without rebuilding
1057
+ * its nodes. Pre-1.0 internal API; not part of the public surface.
1058
+ *
1059
+ * **Identity semantics:** guards are tracked in a `Set`, so pushing the
1060
+ * same `NodeGuard` reference twice is a single registration. Wrap each
1061
+ * push in a unique closure if independent stacking is needed.
1062
+ *
1063
+ * **Iteration order:** insertion-ordered (`Set` semantics). Determinism
1064
+ * follows from single-threaded JS execution; nested re-entry from inside
1065
+ * a guard body (push/pop while iterating) is undefined-but-survivable.
1066
+ */
1067
+ _pushGuard(guard) {
1068
+ if (this._extraGuards == null) this._extraGuards = /* @__PURE__ */ new Set();
1069
+ this._extraGuards.add(guard);
1070
+ return () => {
1071
+ this._extraGuards?.delete(guard);
1072
+ if (this._extraGuards?.size === 0) this._extraGuards = void 0;
1073
+ };
1074
+ }
1075
+ allowsObserve(actor) {
1076
+ if (this._guard == null && this._extraGuards == null) return true;
1077
+ const a = normalizeActor(actor);
1078
+ if (this._guard != null && !this._guard(a, "observe")) return false;
1079
+ if (this._extraGuards != null) {
1080
+ for (const eg of this._extraGuards) {
1081
+ if (!eg(a, "observe")) return false;
1082
+ }
1083
+ }
1084
+ return true;
1085
+ }
1086
+ // --- Guard helper ---
1087
+ _checkGuard(options) {
1088
+ if (options?.internal) return;
1089
+ const hasGuard = this._guard != null || this._extraGuards != null;
1090
+ const hasActor = options?.actor != null;
1091
+ if (!hasGuard && !hasActor) return;
1092
+ const actor = normalizeActor(options?.actor);
1093
+ const action = options?.delivery === "signal" ? "signal" : "write";
1094
+ if (this._guard != null && !this._guard(actor, action)) {
1095
+ throw new GuardDenied({ actor, action, nodeName: this.name });
1096
+ }
1097
+ if (this._extraGuards != null) {
1098
+ for (const eg of this._extraGuards) {
1099
+ if (!eg(actor, action)) {
1100
+ throw new GuardDenied({ actor, action, nodeName: this.name });
1101
+ }
1102
+ }
1103
+ }
1104
+ this._lastMutation = { actor, timestamp_ns: wallClockNs() };
1105
+ }
1106
+ // --- Public transport ---
1107
+ down(messageOrMessages, options) {
1108
+ const messages = normalizeMessages(messageOrMessages);
1109
+ if (messages.length === 0) return;
1110
+ this._checkGuard(options);
1111
+ this._emit(messages);
1112
+ }
1113
+ emit(value, options) {
1114
+ this._checkGuard(options);
1115
+ this._emit([[DATA, value]]);
1116
+ }
1117
+ up(messageOrMessages, options) {
1118
+ if (this._deps.length === 0) return;
1119
+ const messages = normalizeMessages(messageOrMessages);
1120
+ if (messages.length === 0) return;
1121
+ this._checkGuard(options);
1122
+ const forwardOpts = options ?? { internal: true };
1123
+ this._validateUpTiers(messages);
1124
+ for (const d of this._deps) {
1125
+ d.node.up?.(messages, forwardOpts);
1126
+ }
1127
+ }
1128
+ /**
1129
+ * @internal Internal up-path used by `actions.up(...)` from inside fn.
1130
+ * Same tier validation as public `up`, but bypasses the guard check
1131
+ * since the fn context is already inside an authorized operation.
1132
+ */
1133
+ _emitUp(messages) {
1134
+ if (this._deps.length === 0) return;
1135
+ if (messages.length === 0) return;
1136
+ this._validateUpTiers(messages);
1137
+ for (const d of this._deps) {
1138
+ d.node.up?.(messages, { internal: true });
1139
+ }
1140
+ }
1141
+ /**
1142
+ * @internal Enforce spec §1.2 — up-direction messages are restricted to
1143
+ * tier 0–2 and tier 5 (START, DIRTY, INVALIDATE, PAUSE, RESUME,
1144
+ * TEARDOWN). Tier 3 (DATA/RESOLVED) and tier 4 (COMPLETE/ERROR) are
1145
+ * downstream-only. Emitting tier-3/4 via `up` would bypass equals
1146
+ * substitution and cache advance entirely and is a protocol bug.
1147
+ */
1148
+ _validateUpTiers(messages) {
1149
+ const tierOf = this._config.tierOf;
1150
+ for (const m of messages) {
1151
+ const tier = tierOf(m[0]);
1152
+ if (tier === 3 || tier === 4) {
1153
+ throw new Error(
1154
+ `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).`
1155
+ );
1156
+ }
1157
+ }
1158
+ }
1159
+ subscribe(sink, actor) {
1160
+ if (actor != null && this._guard != null) {
1161
+ const a = normalizeActor(actor);
1162
+ if (!this._guard(a, "observe")) {
1163
+ throw new GuardDenied({ actor: a, action: "observe", nodeName: this.name });
1164
+ }
1165
+ }
1166
+ const wasTerminal = this._isTerminal;
1167
+ const afterTerminalReset = wasTerminal && this._resubscribable;
1168
+ if (afterTerminalReset) {
1169
+ this._cached = void 0;
1170
+ this._status = "sentinel";
1171
+ this._store = {};
1172
+ this._hasCalledFnOnce = false;
1173
+ this._waveHasNewData = false;
1174
+ this._hasNewTerminal = false;
1175
+ this._paused = false;
1176
+ this._pendingWave = false;
1177
+ this._pendingRerun = false;
1178
+ this._isExecutingFn = false;
1179
+ this._rerunDepth = 0;
1180
+ this._dirtyDepCount = 0;
1181
+ this._pauseLocks = null;
1182
+ this._pauseBuffer = null;
1183
+ for (const d of this._deps) resetDepRecord(d);
1184
+ }
1185
+ this._sinkCount += 1;
1186
+ let subCleanup;
1187
+ try {
1188
+ subCleanup = this._config.onSubscribe(
1189
+ this,
1190
+ sink,
1191
+ { sinkCount: this._sinkCount, afterTerminalReset },
1192
+ this._actions
1193
+ );
1194
+ } catch (err) {
1195
+ this._sinkCount -= 1;
1196
+ throw err;
1197
+ }
1198
+ if (this._sinks == null) {
1199
+ this._sinks = sink;
1200
+ } else if (typeof this._sinks === "function") {
1201
+ this._sinks = /* @__PURE__ */ new Set([this._sinks, sink]);
1202
+ } else {
1203
+ this._sinks.add(sink);
1204
+ }
1205
+ const isTerminalNow = this._isTerminal;
1206
+ if (this._sinkCount === 1 && !isTerminalNow) {
1207
+ try {
1208
+ this._activate();
1209
+ } catch (err) {
1210
+ this._sinkCount -= 1;
1211
+ this._removeSink(sink);
1212
+ if (this._sinkCount === 0) this._status = "sentinel";
1213
+ if (typeof subCleanup === "function") {
1214
+ try {
1215
+ subCleanup();
1216
+ } catch {
1217
+ }
1218
+ }
1219
+ throw err;
1220
+ }
1221
+ }
1222
+ if (this._status === "sentinel" && this._cached === void 0) {
1223
+ this._status = "pending";
1224
+ }
1225
+ let removed = false;
1226
+ return () => {
1227
+ if (removed) return;
1228
+ removed = true;
1229
+ this._sinkCount -= 1;
1230
+ this._removeSink(sink);
1231
+ if (typeof subCleanup === "function") subCleanup();
1232
+ if (this._sinks == null) this._deactivate();
1233
+ };
1234
+ }
1235
+ _removeSink(sink) {
1236
+ if (this._sinks === sink) {
1237
+ this._sinks = null;
1238
+ } else if (this._sinks != null && typeof this._sinks !== "function") {
1239
+ this._sinks.delete(sink);
1240
+ if (this._sinks.size === 1) {
1241
+ const [only] = this._sinks;
1242
+ this._sinks = only;
1243
+ } else if (this._sinks.size === 0) {
1244
+ this._sinks = null;
1245
+ }
1246
+ }
1247
+ }
1248
+ // --- Lifecycle ---
1249
+ /**
1250
+ * @internal First-sink activation. For a producer (no deps + fn),
1251
+ * invokes fn once. For a compute node (has deps), subscribes to every
1252
+ * dep with the pre-set-dirty trick so the first-run gate waits for
1253
+ * every dep to settle at least once.
1254
+ */
1255
+ _activate() {
1256
+ if (this._deps.length === 0) {
1257
+ if (this._fn) this._execFn();
1258
+ return;
1259
+ }
1260
+ this._dirtyDepCount = 0;
1261
+ const initialLen = this._deps.length;
1262
+ let subscribedCount = 0;
1263
+ try {
1264
+ for (let i = 0; i < initialLen; i++) {
1265
+ const depIdx = i;
1266
+ const dep = this._deps[i];
1267
+ dep.unsub = noopUnsub;
1268
+ dep.unsub = dep.node.subscribe((msgs) => {
1269
+ if (dep.unsub === null) return;
1270
+ const tierOf = this._config.tierOf;
1271
+ let sawSettlement = false;
1272
+ for (const m of msgs) {
1273
+ if (tierOf(m[0]) >= 3) sawSettlement = true;
1274
+ this._config.onMessage(
1275
+ this,
1276
+ m,
1277
+ { direction: "down-in", depIndex: depIdx },
1278
+ this._actions
1279
+ );
1280
+ }
1281
+ if (sawSettlement) this._maybeRunFnOnSettlement();
1282
+ });
1283
+ subscribedCount++;
1284
+ }
1285
+ } catch (err) {
1286
+ this._deps[subscribedCount].unsub = null;
1287
+ for (let j = 0; j < subscribedCount; j++) {
1288
+ const d = this._deps[j];
1289
+ if (d.unsub != null) {
1290
+ const u = d.unsub;
1291
+ d.unsub = null;
1292
+ try {
1293
+ u();
1294
+ } catch {
1295
+ }
1296
+ resetDepRecord(d);
1297
+ }
1298
+ }
1299
+ this._dirtyDepCount = 0;
1300
+ throw err;
1301
+ }
1302
+ }
1303
+ /**
1304
+ * @internal Append a dep post-construction. Used by `autoTrackNode`
1305
+ * (runtime dep discovery) and `Graph.connect()` (post-construction
1306
+ * wiring). Subscribes immediately — if DATA arrives synchronously
1307
+ * during subscribe and fn is currently executing, the re-run is
1308
+ * deferred via `_pendingRerun` flag (see `_execFn` guard).
1309
+ *
1310
+ * **Dedup:** idempotent on duplicate `depNode` — if `depNode` is
1311
+ * already in `_deps`, returns the existing index without mutating
1312
+ * state. Callers can safely invoke `_addDep` without their own
1313
+ * "already added" check. `autoTrackNode` still keeps a `depIndexMap`
1314
+ * as a fast-path lookup for known deps (returning cached `data[idx]`
1315
+ * without calling `_addDep` at all); this internal dedup is the
1316
+ * backstop for any caller that doesn't track its own dep set.
1317
+ *
1318
+ * @returns The index of the new dep in `_deps`, or the existing index
1319
+ * if the dep was already present.
1320
+ */
1321
+ _addDep(depNode) {
1322
+ for (let i = 0; i < this._deps.length; i++) {
1323
+ if (this._deps[i].node === depNode) return i;
1324
+ }
1325
+ const depIdx = this._deps.length;
1326
+ const record = createDepRecord(depNode);
1327
+ this._deps.push(record);
1328
+ if (this._sinks == null) return depIdx;
1329
+ record.dirty = true;
1330
+ this._dirtyDepCount++;
1331
+ if (this._status !== "dirty") this._emit(DIRTY_ONLY_BATCH);
1332
+ record.unsub = noopUnsub;
1333
+ try {
1334
+ record.unsub = depNode.subscribe((msgs) => {
1335
+ if (record.unsub === null) return;
1336
+ const tierOf = this._config.tierOf;
1337
+ let sawSettlement = false;
1338
+ for (const m of msgs) {
1339
+ if (tierOf(m[0]) >= 3) sawSettlement = true;
1340
+ this._config.onMessage(
1341
+ this,
1342
+ m,
1343
+ { direction: "down-in", depIndex: depIdx },
1344
+ this._actions
1345
+ );
1346
+ }
1347
+ if (sawSettlement) this._maybeRunFnOnSettlement();
1348
+ });
1349
+ } catch (err) {
1350
+ record.unsub = null;
1351
+ this._deps.pop();
1352
+ this._dirtyDepCount--;
1353
+ throw err;
1354
+ }
1355
+ return depIdx;
1356
+ }
1357
+ /**
1358
+ * @internal Unsubscribes from deps, fires fn cleanup (both shapes),
1359
+ * clears wave/store state, and (for compute nodes) drops `_cached` per
1360
+ * the ROM/RAM rule. Idempotent: second call is a no-op.
1361
+ *
1362
+ * @param skipStatusUpdate — When `true`, the caller takes responsibility
1363
+ * for setting `_status` after deactivation (e.g. TEARDOWN always sets
1364
+ * `"sentinel"` unconditionally). When `false` (default), deactivation
1365
+ * applies the ROM rule: compute nodes → `"sentinel"`, state nodes
1366
+ * preserve their current status.
1367
+ */
1368
+ _deactivate(skipStatusUpdate = false) {
1369
+ const cleanup = this._cleanup;
1370
+ this._cleanup = void 0;
1371
+ if (typeof cleanup === "function") {
1372
+ try {
1373
+ cleanup();
1374
+ } catch (err) {
1375
+ this._emit([[ERROR, this._wrapFnError("cleanup threw", err)]]);
1376
+ }
1377
+ } else if (cleanup != null && typeof cleanup.deactivation === "function") {
1378
+ try {
1379
+ cleanup.deactivation();
1380
+ } catch (err) {
1381
+ this._emit([[ERROR, this._wrapFnError("cleanup.deactivation threw", err)]]);
1382
+ }
1383
+ }
1384
+ for (const d of this._deps) {
1385
+ if (d.unsub != null) {
1386
+ const u = d.unsub;
1387
+ d.unsub = null;
1388
+ try {
1389
+ u();
1390
+ } catch {
1391
+ }
1392
+ }
1393
+ resetDepRecord(d);
1394
+ }
1395
+ this._waveHasNewData = false;
1396
+ this._hasNewTerminal = false;
1397
+ this._hasCalledFnOnce = false;
1398
+ this._paused = false;
1399
+ this._pendingWave = false;
1400
+ this._pendingRerun = false;
1401
+ this._rerunDepth = 0;
1402
+ this._store = {};
1403
+ this._dirtyDepCount = 0;
1404
+ this._pauseLocks = null;
1405
+ this._pauseBuffer = null;
1406
+ if (this._fn != null) {
1407
+ this._cached = void 0;
1408
+ }
1409
+ if (!skipStatusUpdate) {
1410
+ if (this._fn != null || this._deps.length > 0) {
1411
+ if (!this._isTerminal || this._resubscribable) {
1412
+ this._status = "sentinel";
1413
+ }
1414
+ }
1415
+ }
1416
+ }
1417
+ // --- Dep message dispatch (§3.5 singleton default) ---
1418
+ /**
1419
+ * @internal Default per-tier dispatch for incoming dep messages. Called
1420
+ * by `defaultOnMessage`. Updates the DepRecord, triggers wave
1421
+ * completion, and forwards passthrough traffic.
1422
+ */
1423
+ _onDepMessage(depIndex, msg) {
1424
+ const dep = this._deps[depIndex];
1425
+ const t = msg[0];
1426
+ if (this._inspectorHooks != null) {
1427
+ const ev = { kind: "dep_message", depIndex, message: msg };
1428
+ for (const hook of this._inspectorHooks) hook(ev);
1429
+ }
1430
+ if (t === START) return;
1431
+ if (t === DIRTY) {
1432
+ this._depDirtied(dep);
1433
+ return;
1434
+ }
1435
+ if (t === INVALIDATE) {
1436
+ this._depInvalidated(dep);
1437
+ this._emit(INVALIDATE_ONLY_BATCH);
1438
+ return;
1439
+ }
1440
+ if (t === PAUSE || t === RESUME) {
1441
+ this._emit([msg]);
1442
+ return;
1443
+ }
1444
+ if (t === TEARDOWN) {
1445
+ this._emit(TEARDOWN_ONLY_BATCH);
1446
+ return;
1447
+ }
1448
+ if (t === DATA) {
1449
+ this._depSettledAsData(dep, msg[1]);
1450
+ } else if (t === RESOLVED) {
1451
+ this._depSettledAsResolved(dep);
1452
+ } else if (t === COMPLETE) {
1453
+ this._depSettledAsTerminal(dep, true);
1454
+ } else if (t === ERROR) {
1455
+ this._depSettledAsTerminal(dep, msg[1]);
1456
+ } else {
1457
+ this._emit([msg]);
1458
+ return;
1459
+ }
1460
+ if (!this._fn) {
1461
+ if (t === DATA || t === RESOLVED) {
1462
+ this._emit([msg]);
1463
+ }
1464
+ if (t === COMPLETE || t === ERROR) {
1465
+ this._maybeAutoTerminalAfterWave();
1466
+ }
1467
+ return;
1468
+ }
1469
+ }
1470
+ // --- Centralized dep-state transitions (A3 settlement counters) ---
1471
+ //
1472
+ // Every mutation to `DepRecord.dirty` / `DepRecord.prevData` /
1473
+ // `DepRecord.terminal` must go through one of these helpers so the
1474
+ // `_dirtyDepCount` and `_sentinelDepCount` counters stay in sync with
1475
+ // the per-record flags. `_maybeRunFnOnSettlement` reads the counters
1476
+ // and never re-scans the `_deps` array.
1477
+ /**
1478
+ * Called when a dep transitions `dirty: false → true` (either from an
1479
+ * incoming DIRTY, or pre-set during `_activate` / `_addDep` /
1480
+ * `_depInvalidated`). No-op if the dep is already dirty. Fires the
1481
+ * downstream DIRTY emit if we're the first to dirty this wave.
1482
+ */
1483
+ _depDirtied(dep) {
1484
+ if (dep.dirty) return;
1485
+ dep.dirty = true;
1486
+ dep.involvedThisWave = true;
1487
+ this._dirtyDepCount++;
1488
+ if (this._status !== "dirty") {
1489
+ this._emit(DIRTY_ONLY_BATCH);
1490
+ }
1491
+ }
1492
+ /**
1493
+ * Called when a dep delivers new DATA: clears dirty, stores the payload,
1494
+ * marks wave-has-data, and — if this is the dep's first DATA — clears
1495
+ * its sentinel slot so the first-run gate can open.
1496
+ */
1497
+ _depSettledAsData(dep, value) {
1498
+ if (dep.dirty) {
1499
+ dep.dirty = false;
1500
+ this._dirtyDepCount--;
1501
+ }
1502
+ dep.involvedThisWave = true;
1503
+ dep.dataBatch.push(value);
1504
+ this._waveHasNewData = true;
1505
+ }
1506
+ /**
1507
+ * Called when a dep emits RESOLVED (wave settled, value unchanged).
1508
+ * Clears dirty; does NOT touch `prevData` / `terminal` / sentinel
1509
+ * count — sentinel only exits on first DATA or terminal, not RESOLVED.
1510
+ */
1511
+ _depSettledAsResolved(dep) {
1512
+ if (dep.dirty) {
1513
+ dep.dirty = false;
1514
+ this._dirtyDepCount--;
1515
+ }
1516
+ }
1517
+ /**
1518
+ * Called when a dep delivers COMPLETE (`terminal = true`) or ERROR
1519
+ * (`terminal = errorPayload`). Clears dirty, stores the terminal, and
1520
+ * — if the dep had never contributed a DATA yet — leaves sentinel
1521
+ * since the gate treats "terminated without data" as gate-open too.
1522
+ */
1523
+ _depSettledAsTerminal(dep, terminal) {
1524
+ if (dep.dirty) {
1525
+ dep.dirty = false;
1526
+ this._dirtyDepCount--;
1527
+ }
1528
+ dep.terminal = terminal;
1529
+ dep.involvedThisWave = true;
1530
+ this._hasNewTerminal = true;
1531
+ }
1532
+ /**
1533
+ * Called when a dep emits INVALIDATE: clears prevData, terminal, and
1534
+ * dataBatch. The dep is now back in the "never delivered a real value"
1535
+ * state — `prevData === undefined` so the sentinel check in fn will fire.
1536
+ */
1537
+ _depInvalidated(dep) {
1538
+ dep.prevData = void 0;
1539
+ dep.terminal = void 0;
1540
+ dep.dataBatch.length = 0;
1541
+ if (!dep.dirty) {
1542
+ dep.dirty = true;
1543
+ dep.involvedThisWave = true;
1544
+ this._dirtyDepCount++;
1545
+ } else {
1546
+ dep.involvedThisWave = false;
1547
+ }
1548
+ }
1549
+ _maybeRunFnOnSettlement() {
1550
+ if (this._isTerminal && !this._resubscribable) return;
1551
+ if (this._dirtyDepCount > 0) return;
1552
+ if (this._paused) {
1553
+ this._pendingWave = true;
1554
+ return;
1555
+ }
1556
+ if (!this._waveHasNewData && !this._hasNewTerminal && this._hasCalledFnOnce) {
1557
+ this._clearWaveFlags();
1558
+ this._emit(RESOLVED_ONLY_BATCH);
1559
+ this._maybeAutoTerminalAfterWave();
1560
+ return;
1561
+ }
1562
+ if (this._fn) this._execFn();
1563
+ this._maybeAutoTerminalAfterWave();
1564
+ }
1565
+ _maybeAutoTerminalAfterWave() {
1566
+ if (this._deps.length === 0) return;
1567
+ if (this._isTerminal) return;
1568
+ const erroredDep = this._deps.find((d) => d.terminal !== void 0 && d.terminal !== true);
1569
+ if (erroredDep != null) {
1570
+ if (this._autoError) {
1571
+ this._emit([[ERROR, erroredDep.terminal]]);
1572
+ }
1573
+ return;
1574
+ }
1575
+ if (this._autoComplete && this._deps.every((d) => d.terminal !== void 0)) {
1576
+ this._emit(COMPLETE_ONLY_BATCH);
1577
+ }
1578
+ }
1579
+ // --- Fn execution ---
1580
+ /**
1581
+ * @internal Runs the node fn once. Default cleanup (function form) fires
1582
+ * before the new run; `{ deactivation }` cleanup survives.
1583
+ */
1584
+ _execFn() {
1585
+ if (!this._fn) return;
1586
+ if (this._isTerminal && !this._resubscribable) return;
1587
+ if (this._isExecutingFn) {
1588
+ this._pendingRerun = true;
1589
+ return;
1590
+ }
1591
+ const prevCleanup = this._cleanup;
1592
+ if (typeof prevCleanup === "function") {
1593
+ this._cleanup = void 0;
1594
+ try {
1595
+ prevCleanup();
1596
+ } catch (err) {
1597
+ this._emit([[ERROR, this._wrapFnError("cleanup threw", err)]]);
1598
+ return;
1599
+ }
1600
+ }
1601
+ const batchData = this._deps.map(
1602
+ (d) => !d.involvedThisWave ? void 0 : d.dataBatch.length > 0 ? [...d.dataBatch] : []
1603
+ );
1604
+ const prevData = this._deps.map((d) => d.prevData);
1605
+ for (let i = 0; i < this._deps.length; i++) {
1606
+ const batch2 = batchData[i];
1607
+ if (batch2 != null && batch2.length > 0) {
1608
+ this._deps[i].prevData = batch2[batch2.length - 1];
1609
+ }
1610
+ }
1611
+ const terminalDeps = this._deps.map((d) => d.terminal);
1612
+ const ctx = { prevData, terminalDeps, store: this._store };
1613
+ this._hasCalledFnOnce = true;
1614
+ this._clearWaveFlags();
1615
+ if (this._inspectorHooks != null) {
1616
+ const ev = { kind: "run", batchData, prevData };
1617
+ for (const hook of this._inspectorHooks) hook(ev);
1618
+ }
1619
+ this._isExecutingFn = true;
1620
+ try {
1621
+ const result = this._fn(batchData, this._actions, ctx);
1622
+ if (typeof result === "function") {
1623
+ this._cleanup = result;
1624
+ } else if (result != null && typeof result === "object" && typeof result.deactivation === "function") {
1625
+ this._cleanup = result;
1626
+ }
1627
+ } catch (err) {
1628
+ this._emit([[ERROR, this._wrapFnError("fn threw", err)]]);
1629
+ } finally {
1630
+ this._isExecutingFn = false;
1631
+ if (this._pendingRerun) {
1632
+ this._pendingRerun = false;
1633
+ this._rerunDepth += 1;
1634
+ if (this._rerunDepth > MAX_RERUN_DEPTH) {
1635
+ this._rerunDepth = 0;
1636
+ this._emit([
1637
+ [
1638
+ ERROR,
1639
+ new Error(
1640
+ `Node "${this.name}": _pendingRerun depth exceeded ${MAX_RERUN_DEPTH} \u2014 likely a reactive cycle`
1641
+ )
1642
+ ]
1643
+ ]);
1644
+ } else {
1645
+ this._maybeRunFnOnSettlement();
1646
+ }
1647
+ } else {
1648
+ this._rerunDepth = 0;
1649
+ }
1650
+ this._clearWaveFlags();
1651
+ }
1652
+ }
1653
+ _clearWaveFlags() {
1654
+ this._waveHasNewData = false;
1655
+ this._hasNewTerminal = false;
1656
+ for (const d of this._deps) {
1657
+ d.involvedThisWave = false;
1658
+ d.dataBatch.length = 0;
1659
+ }
1660
+ }
1661
+ _wrapFnError(label, err) {
1662
+ const msg = err instanceof Error ? err.message : String(err);
1663
+ return new Error(`Node "${this.name}": ${label}: ${msg}`, { cause: err });
1664
+ }
1665
+ // --- Framing (tier sort + synthetic DIRTY prefix) ---
1666
+ /**
1667
+ * @internal Stable tier sort + synthetic DIRTY prefix for an outgoing
1668
+ * batch. Fast path: already-monotone single-tier batches (the common
1669
+ * case from interned singletons like `DIRTY_ONLY_BATCH`) return the
1670
+ * input unchanged. General path: decorate-sort-undecorate into a new
1671
+ * array, then prepend `[DIRTY]` after any tier-0 START entries when
1672
+ * a tier-3 payload is present and the node isn't already dirty.
1673
+ *
1674
+ * Single source of truth for the spec §1.3.1 framing invariant. Every
1675
+ * outgoing path hits `_frameBatch` exactly once via `_emit`.
1676
+ */
1677
+ _frameBatch(messages) {
1678
+ const tierOf = this._config.tierOf;
1679
+ if (messages.length === 1) {
1680
+ const t = tierOf(messages[0][0]);
1681
+ if (t === 3 && this._status !== "dirty") {
1682
+ return [DIRTY_MSG, messages[0]];
1683
+ }
1684
+ return messages;
1685
+ }
1686
+ let monotone = true;
1687
+ let hasTier3 = false;
1688
+ let hasDirty = false;
1689
+ let prevTier = -1;
1690
+ for (const m of messages) {
1691
+ const tier = tierOf(m[0]);
1692
+ if (tier < prevTier) monotone = false;
1693
+ if (tier === 3) hasTier3 = true;
1694
+ if (m[0] === DIRTY) hasDirty = true;
1695
+ prevTier = tier;
1696
+ }
1697
+ let sorted = messages;
1698
+ if (!monotone) {
1699
+ const indexed = messages.map((m, i) => ({ m, i, tier: tierOf(m[0]) }));
1700
+ indexed.sort((a, b) => a.tier - b.tier || a.i - b.i);
1701
+ sorted = indexed.map((x) => x.m);
1702
+ }
1703
+ if (hasTier3 && !hasDirty && this._status !== "dirty") {
1704
+ let insertAt = 0;
1705
+ while (insertAt < sorted.length && tierOf(sorted[insertAt][0]) === 0) insertAt++;
1706
+ if (insertAt === 0) return [DIRTY_MSG, ...sorted];
1707
+ return [...sorted.slice(0, insertAt), DIRTY_MSG, ...sorted.slice(insertAt)];
1708
+ }
1709
+ return sorted;
1710
+ }
1711
+ // --- Emit pipeline ---
1712
+ /**
1713
+ * @internal The unified dispatch waist — one call = one wave.
1714
+ * See `GRAPHREFLY-SPEC.md` §1.3.1 for protocol context — the stages
1715
+ * below are the implementation order.
1716
+ *
1717
+ * Pipeline stages, in order:
1718
+ *
1719
+ * 1. Terminal filter — post-COMPLETE/ERROR only TEARDOWN/INVALIDATE
1720
+ * still propagate so graph teardown and cache-clear still work.
1721
+ * 2. Tier sort (stable) — the batch can be in any order when it
1722
+ * arrives; the walker downstream (`downWithBatch`) assumes
1723
+ * ascending tier monotone, and so does `_updateState`'s tier-3
1724
+ * slice walk. This is the single source of truth for ordering.
1725
+ * 3. Synthetic DIRTY prefix — if a tier-3 payload is present, no
1726
+ * DIRTY is already in the batch, and the node isn't already in
1727
+ * `"dirty"` status, prepend `[DIRTY]` after any tier-0 START
1728
+ * entries. Guarantees spec §1.3.1 (DIRTY precedes DATA within
1729
+ * the same batch) uniformly across every entry point.
1730
+ * 4. PAUSE/RESUME lock bookkeeping (C0) — update `_pauseLocks`,
1731
+ * derive `_paused`, filter unknown-lockId RESUME, replay
1732
+ * bufferAll buffer on final lock release.
1733
+ * 5. Meta TEARDOWN fan-out — notify meta children before
1734
+ * `_updateState`'s TEARDOWN branch calls `_deactivate`. Hoisted
1735
+ * out of the walk to keep `_updateState` re-entrance-free.
1736
+ * 6. `_updateState` — walk the batch in tier order, advancing
1737
+ * `_cached` / `_status` / `_versioning` and running equals
1738
+ * substitution on tier-3 DATA (§3.5.1). Returns
1739
+ * `{finalMessages, equalsError?}`.
1740
+ * 7. `downWithBatch` dispatch (or bufferAll capture if paused with
1741
+ * `pausable: "resumeAll"`).
1742
+ * 8. Recursive ERROR emission if equals threw mid-walk.
1743
+ */
1744
+ _emit(messages) {
1745
+ if (messages.length === 0) return;
1746
+ let deliverable = messages;
1747
+ const terminal = this._isTerminal;
1748
+ if (terminal && !this._resubscribable) {
1749
+ const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
1750
+ if (pass.length === 0) return;
1751
+ deliverable = pass;
1752
+ }
1753
+ deliverable = this._frameBatch(deliverable);
1754
+ let filtered = null;
1755
+ for (let i = 0; i < deliverable.length; i++) {
1756
+ const m = deliverable[i];
1757
+ const t = m[0];
1758
+ if (t !== PAUSE && t !== RESUME) {
1759
+ if (filtered != null) filtered.push(m);
1760
+ continue;
1761
+ }
1762
+ if (m.length < 2) {
1763
+ throw new Error(
1764
+ `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]]\`.`
1765
+ );
1766
+ }
1767
+ let forward = true;
1768
+ if (this._pausable !== false) {
1769
+ const lockId = m[1];
1770
+ if (t === PAUSE) {
1771
+ if (this._pauseLocks == null) this._pauseLocks = /* @__PURE__ */ new Set();
1772
+ this._pauseLocks.add(lockId);
1773
+ this._paused = true;
1774
+ if (this._pausable === "resumeAll" && this._pauseBuffer == null) {
1775
+ this._pauseBuffer = [];
1776
+ }
1777
+ } else {
1778
+ if (this._pauseLocks == null || !this._pauseLocks.has(lockId)) {
1779
+ forward = false;
1780
+ } else {
1781
+ this._pauseLocks.delete(lockId);
1782
+ if (this._pauseLocks.size === 0) {
1783
+ this._paused = false;
1784
+ if (this._pauseBuffer != null && this._pauseBuffer.length > 0) {
1785
+ const drain = this._pauseBuffer;
1786
+ this._pauseBuffer = [];
1787
+ this._emit(drain);
1788
+ }
1789
+ if (this._pendingWave) {
1790
+ this._pendingWave = false;
1791
+ this._maybeRunFnOnSettlement();
1792
+ }
1793
+ }
1794
+ }
1795
+ }
1796
+ }
1797
+ if (!forward) {
1798
+ if (filtered == null) filtered = deliverable.slice(0, i);
1799
+ } else if (filtered != null) {
1800
+ filtered.push(m);
1801
+ }
1802
+ }
1803
+ if (filtered != null) {
1804
+ if (filtered.length === 0) return;
1805
+ deliverable = filtered;
1806
+ }
1807
+ if (this._hasMeta && deliverable.some((m) => m[0] === TEARDOWN)) {
1808
+ for (const k of Object.keys(this.meta)) {
1809
+ try {
1810
+ this.meta[k]._emit(TEARDOWN_ONLY_BATCH);
1811
+ } catch {
1812
+ }
1813
+ }
1814
+ }
1815
+ const { finalMessages, equalsError } = this._updateState(deliverable);
1816
+ if (finalMessages.length > 0 && this._config.inspectorEnabled) {
1817
+ const inspector = this._config.globalInspector;
1818
+ if (inspector != null) {
1819
+ try {
1820
+ inspector({ kind: "emit", node: this, messages: finalMessages });
1821
+ } catch {
1822
+ }
1823
+ }
1824
+ }
1825
+ if (finalMessages.length > 0) {
1826
+ if (this._paused && this._pausable === "resumeAll" && this._pauseBuffer != null) {
1827
+ const tierOf = this._config.tierOf;
1828
+ const immediate = [];
1829
+ for (const m of finalMessages) {
1830
+ const tier = tierOf(m[0]);
1831
+ if (tier < 3 || tier === 5) {
1832
+ immediate.push(m);
1833
+ } else {
1834
+ this._pauseBuffer.push(m);
1835
+ }
1836
+ }
1837
+ if (immediate.length > 0) {
1838
+ this._dispatchOrAccumulate(immediate);
1839
+ }
1840
+ } else {
1841
+ this._dispatchOrAccumulate(finalMessages);
1842
+ }
1843
+ }
1844
+ if (equalsError != null) {
1845
+ this._emit([[ERROR, equalsError]]);
1846
+ }
1847
+ }
1848
+ /**
1849
+ * @internal Walk an outgoing (already-framed) batch, updating own
1850
+ * cache / status / versioning and running equals substitution on
1851
+ * every tier-3 DATA (§3.5.1). Framing — tier sort and synthetic
1852
+ * DIRTY prefix — has already happened upstream in `_frameBatch`.
1853
+ * This walk trusts the input is in monotone tier order and that the
1854
+ * spec §1.3.1 DIRTY/RESOLVED precedence invariant is already
1855
+ * satisfied by the frame.
1856
+ *
1857
+ * Equals substitution: every DATA payload is compared against the
1858
+ * live `_cached`; when equal, the tuple is rewritten to `[RESOLVED]`
1859
+ * in a per-call copy and cache is not re-advanced. `.cache` remains
1860
+ * coherent with "the last DATA payload this node actually sent
1861
+ * downstream".
1862
+ *
1863
+ * Returns `{ finalMessages, equalsError? }`:
1864
+ * - `finalMessages` — the array to deliver to sinks (may be
1865
+ * `messages` unchanged, a rewritten copy with DATA→RESOLVED
1866
+ * substitutions, or a truncated prefix when equals throws mid-walk).
1867
+ * - `equalsError` — present only when the configured `equals` function
1868
+ * threw on some DATA message. `_emit` delivers the prefix first,
1869
+ * then emits a fresh ERROR batch via a recursive `_emit` call so
1870
+ * subscribers observe `[...walked_prefix, ERROR]` in order.
1871
+ */
1872
+ _updateState(messages) {
1873
+ const tierOf = this._config.tierOf;
1874
+ let rewritten;
1875
+ let equalsError;
1876
+ let abortedAt = -1;
1877
+ let dataCount = 0;
1878
+ for (const m of messages) {
1879
+ if (tierOf(m[0]) === 3) dataCount++;
1880
+ }
1881
+ const checkEquals = dataCount <= 1;
1882
+ let lastDataIdx = -1;
1883
+ if (this._versioning != null && dataCount > 1) {
1884
+ for (let i = messages.length - 1; i >= 0; i--) {
1885
+ if (messages[i][0] === DATA) {
1886
+ lastDataIdx = i;
1887
+ break;
1888
+ }
1889
+ }
1890
+ }
1891
+ for (let i = 0; i < messages.length; i++) {
1892
+ const m = messages[i];
1893
+ const t = m[0];
1894
+ if (t === DATA) {
1895
+ if (m.length >= 2) {
1896
+ let unchanged = false;
1897
+ if (checkEquals && this._cached !== void 0) {
1898
+ try {
1899
+ unchanged = this._equals(this._cached, m[1]);
1900
+ } catch (err) {
1901
+ equalsError = this._wrapFnError("equals threw", err);
1902
+ abortedAt = i;
1903
+ break;
1904
+ }
1905
+ }
1906
+ if (unchanged) {
1907
+ if (rewritten == null) rewritten = messages.slice(0, i);
1908
+ rewritten.push(RESOLVED_MSG);
1909
+ this._status = "resolved";
1910
+ continue;
1911
+ }
1912
+ this._cached = m[1];
1913
+ if (this._versioning != null) {
1914
+ if (lastDataIdx < 0 || i === lastDataIdx) {
1915
+ advanceVersion(this._versioning, m[1], this._hashFn);
1916
+ }
1917
+ }
1918
+ }
1919
+ this._status = "settled";
1920
+ if (rewritten != null) rewritten.push(m);
1921
+ } else {
1922
+ if (rewritten != null) rewritten.push(m);
1923
+ if (t === DIRTY) {
1924
+ this._status = "dirty";
1925
+ } else if (t === RESOLVED) {
1926
+ this._status = "resolved";
1927
+ } else if (t === COMPLETE) {
1928
+ this._status = "completed";
1929
+ } else if (t === ERROR) {
1930
+ this._status = "errored";
1931
+ } else if (t === INVALIDATE) {
1932
+ this._cached = void 0;
1933
+ this._status = "dirty";
1934
+ const c = this._cleanup;
1935
+ if (typeof c === "function") {
1936
+ this._cleanup = void 0;
1937
+ try {
1938
+ c();
1939
+ } catch {
1940
+ }
1941
+ }
1942
+ } else if (t === TEARDOWN) {
1943
+ if (this._resetOnTeardown) this._cached = void 0;
1944
+ this._deactivate(
1945
+ /* skipStatusUpdate */
1946
+ true
1947
+ );
1948
+ this._status = "sentinel";
1949
+ }
1950
+ }
1951
+ }
1952
+ const base = abortedAt >= 0 ? rewritten ?? messages.slice(0, abortedAt) : rewritten ?? messages;
1953
+ return equalsError != null ? { finalMessages: base, equalsError } : { finalMessages: base };
1954
+ }
1955
+ _deliverToSinks = (messages) => {
1956
+ if (this._sinks == null) return;
1957
+ if (typeof this._sinks === "function") {
1958
+ this._sinks(messages);
1959
+ return;
1960
+ }
1961
+ const snapshot = [...this._sinks];
1962
+ for (const sink of snapshot) sink(messages);
1963
+ };
1964
+ /**
1965
+ * @internal Dispatch entry point that respects the per-batch emit
1966
+ * accumulator (Bug 2). Inside an explicit `batch()` scope, append to
1967
+ * `_batchPendingMessages` and register a flush hook on first append.
1968
+ * Outside batch — or during a drain (where `flushInProgress` is true
1969
+ * but `batchDepth` is 0) — dispatch synchronously through `downWithBatch`.
1970
+ *
1971
+ * Per-emit state updates (`_frameBatch`, `_updateState`) have already
1972
+ * happened by the time we reach here; only the **downstream delivery**
1973
+ * is coalesced. Cache, version, and status are visible mid-batch on
1974
+ * the emitting node itself.
1975
+ */
1976
+ _dispatchOrAccumulate(messages) {
1977
+ if (isExplicitlyBatching()) {
1978
+ if (this._batchPendingMessages === null) {
1979
+ this._batchPendingMessages = [];
1980
+ registerBatchFlushHook(() => this._flushBatchPending());
1981
+ }
1982
+ for (const m of messages) this._batchPendingMessages.push(m);
1983
+ return;
1984
+ }
1985
+ downWithBatch(this._deliverToSinks, messages, this._config.tierOf);
1986
+ }
1987
+ /**
1988
+ * @internal Flushes the accumulated batch through `downWithBatch` and
1989
+ * clears the pending state. Idempotent — safe to call when pending is
1990
+ * already null or empty (e.g. on a `batch()` throw, where the hook
1991
+ * fires for cleanup but the drainPhase queues are wiped after).
1992
+ *
1993
+ * Critical: the accumulated batch is interleaved per-emit framings like
1994
+ * `[DIRTY, DATA(1), DIRTY, DATA(2)]` — non-monotone tier order. We must
1995
+ * re-frame to sort by tier before handing to `downWithBatch`, which
1996
+ * assumes pre-sorted input. `_frameBatch` also handles the synthetic
1997
+ * DIRTY prepend rule (no-op here — `hasDirty` is true since each
1998
+ * accumulated emit already carries its own DIRTY prefix).
1999
+ */
2000
+ _flushBatchPending() {
2001
+ const pending = this._batchPendingMessages;
2002
+ if (pending === null) return;
2003
+ this._batchPendingMessages = null;
2004
+ if (pending.length === 0) return;
2005
+ const framed = this._frameBatch(pending);
2006
+ downWithBatch(this._deliverToSinks, framed, this._config.tierOf);
2007
+ }
2008
+ };
2009
+ var isNodeArray = (value) => Array.isArray(value);
2010
+ var isNodeOptionsObject = (value) => typeof value === "object" && value != null && !Array.isArray(value);
2011
+ function node(depsOrFn, fnOrOpts, optsArg) {
2012
+ const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
2013
+ const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
2014
+ let opts = {};
2015
+ if (isNodeArray(depsOrFn)) {
2016
+ opts = (isNodeOptionsObject(fnOrOpts) ? fnOrOpts : optsArg) ?? {};
2017
+ } else if (isNodeOptionsObject(depsOrFn)) {
2018
+ opts = depsOrFn;
2019
+ } else {
2020
+ opts = (isNodeOptionsObject(fnOrOpts) ? fnOrOpts : optsArg) ?? {};
2021
+ }
2022
+ return new NodeImpl(deps, fn, opts);
2023
+ }
2024
+
2025
+ // src/core/sugar.ts
2026
+ function state(initial, opts) {
2027
+ return node([], { ...opts, initial });
2028
+ }
2029
+ function producer(fn, opts) {
2030
+ const wrapped = (_data, actions, ctx) => fn(actions, ctx) ?? void 0;
2031
+ return node(wrapped, { describeKind: "producer", ...opts });
2032
+ }
2033
+
2034
+ // src/core/meta.ts
2035
+ function resolveDescribeFields(detail, fields) {
2036
+ if (fields != null && fields.length > 0) return new Set(fields);
2037
+ switch (detail) {
2038
+ case "standard":
2039
+ return /* @__PURE__ */ new Set(["type", "status", "value", "deps", "meta", "v"]);
2040
+ case "full":
2041
+ return null;
2042
+ default:
2043
+ return /* @__PURE__ */ new Set(["type", "deps"]);
2044
+ }
2045
+ }
2046
+ function inferDescribeType(n) {
2047
+ if (n._describeKind != null) return n._describeKind;
2048
+ const hasDeps = n._deps.length > 0;
2049
+ if (!hasDeps) return n._fn != null ? "producer" : "state";
2050
+ return "derived";
2051
+ }
2052
+ function metaSnapshot(node2) {
2053
+ const out = {};
2054
+ for (const [key, child] of Object.entries(node2.meta)) {
2055
+ try {
2056
+ out[key] = child.cache;
2057
+ } catch {
2058
+ }
2059
+ }
2060
+ return out;
2061
+ }
2062
+ function describeNode(node2, includeFields) {
2063
+ const all = includeFields == null;
2064
+ const metaKeys = !all && includeFields != null ? [...includeFields].filter((f) => f.startsWith("meta.")).map((f) => f.slice(5)) : null;
2065
+ const wantsMeta = all || includeFields.has("meta") || metaKeys != null && metaKeys.length > 0;
2066
+ let type = "state";
2067
+ let deps = [];
2068
+ if (node2 instanceof NodeImpl) {
2069
+ type = inferDescribeType(node2);
2070
+ deps = node2._deps.map((d) => d.node.name ?? "");
2071
+ }
2072
+ const out = { type, deps };
2073
+ if (all || includeFields.has("status")) {
2074
+ out.status = node2.status;
2075
+ }
2076
+ const guard = node2 instanceof NodeImpl ? node2._guard : void 0;
2077
+ if (wantsMeta) {
2078
+ const rawMeta = { ...metaSnapshot(node2) };
2079
+ if (guard != null && rawMeta.access === void 0) {
2080
+ rawMeta.access = accessHintForGuard(guard);
2081
+ }
2082
+ if (metaKeys != null && metaKeys.length > 0 && !includeFields.has("meta")) {
2083
+ const filtered = {};
2084
+ for (const k of metaKeys) {
2085
+ if (k in rawMeta) filtered[k] = rawMeta[k];
2086
+ }
2087
+ out.meta = filtered;
2088
+ } else {
2089
+ out.meta = rawMeta;
2090
+ }
2091
+ }
2092
+ if (node2.name != null) {
2093
+ out.name = node2.name;
2094
+ }
2095
+ if (all || includeFields.has("value")) {
2096
+ if (node2.status === "sentinel") out.sentinel = true;
2097
+ try {
2098
+ out.value = node2.cache;
2099
+ } catch {
2100
+ }
2101
+ }
2102
+ if ((all || includeFields.has("v")) && node2.v != null) {
2103
+ const vInfo = {
2104
+ id: node2.v.id,
2105
+ version: node2.v.version
2106
+ };
2107
+ if ("cid" in node2.v) {
2108
+ vInfo.cid = node2.v.cid;
2109
+ vInfo.prev = node2.v.prev;
2110
+ }
2111
+ out.v = vInfo;
2112
+ }
2113
+ if (all || includeFields.has("guard")) {
2114
+ if (guard != null) out.guard = accessHintForGuard(guard);
2115
+ }
2116
+ if (all || includeFields.has("lastMutation")) {
2117
+ if (node2.lastMutation != null) out.lastMutation = node2.lastMutation;
2118
+ }
2119
+ return out;
2120
+ }
2121
+
2122
+ // src/extra/timer.ts
2123
+ var ResettableTimer = class {
2124
+ _timer;
2125
+ _gen = 0;
2126
+ /** Schedule callback after delayMs. Cancels any pending timer. */
2127
+ start(delayMs, callback) {
2128
+ this.cancel();
2129
+ this._gen += 1;
2130
+ const gen = this._gen;
2131
+ this._timer = setTimeout(() => {
2132
+ this._timer = void 0;
2133
+ if (gen !== this._gen) return;
2134
+ callback();
2135
+ }, delayMs);
2136
+ }
2137
+ /** Cancel the pending timer (if any). */
2138
+ cancel() {
2139
+ if (this._timer !== void 0) {
2140
+ clearTimeout(this._timer);
2141
+ this._timer = void 0;
2142
+ }
2143
+ }
2144
+ /** Whether a timer is currently pending. */
2145
+ get pending() {
2146
+ return this._timer !== void 0;
2147
+ }
2148
+ };
2149
+
2150
+ // src/extra/utils/ring-buffer.ts
2151
+ var RingBuffer = class {
2152
+ constructor(capacity) {
2153
+ this.capacity = capacity;
2154
+ if (!Number.isInteger(capacity) || capacity <= 0) {
2155
+ throw new Error(`RingBuffer capacity must be a positive integer (got ${capacity})`);
2156
+ }
2157
+ this.buf = new Array(capacity);
2158
+ }
2159
+ buf;
2160
+ head = 0;
2161
+ _size = 0;
2162
+ /** Current number of stored entries. */
2163
+ get size() {
2164
+ return this._size;
2165
+ }
2166
+ /** Configured maximum before drop-oldest eviction fires. */
2167
+ get maxSize() {
2168
+ return this.capacity;
2169
+ }
2170
+ /**
2171
+ * Append an item. If size equals capacity, drops the oldest entry and
2172
+ * advances the head pointer.
2173
+ */
2174
+ push(item) {
2175
+ const idx = (this.head + this._size) % this.capacity;
2176
+ this.buf[idx] = item;
2177
+ if (this._size < this.capacity) this._size++;
2178
+ else this.head = (this.head + 1) % this.capacity;
2179
+ }
2180
+ /** Remove and return the oldest entry; `undefined` when empty. */
2181
+ shift() {
2182
+ if (this._size === 0) return void 0;
2183
+ const item = this.buf[this.head];
2184
+ this.buf[this.head] = void 0;
2185
+ this.head = (this.head + 1) % this.capacity;
2186
+ this._size--;
2187
+ return item;
2188
+ }
2189
+ /**
2190
+ * O(1) index lookup. Negative indices count from the tail (Python-style).
2191
+ * Returns `undefined` for out-of-range.
2192
+ */
2193
+ at(i) {
2194
+ if (this._size === 0) return void 0;
2195
+ const n = i < 0 ? this._size + i : i;
2196
+ if (n < 0 || n >= this._size) return void 0;
2197
+ return this.buf[(this.head + n) % this.capacity];
2198
+ }
2199
+ /**
2200
+ * Materialize the contents in insertion order (oldest → newest).
2201
+ * Returns a new array each call.
2202
+ */
2203
+ toArray() {
2204
+ const result = new Array(this._size);
2205
+ for (let i = 0; i < this._size; i++) {
2206
+ result[i] = this.buf[(this.head + i) % this.capacity];
2207
+ }
2208
+ return result;
2209
+ }
2210
+ /** Reset to empty. Storage slots are released so held refs can GC. */
2211
+ clear() {
2212
+ for (let i = 0; i < this._size; i++) {
2213
+ this.buf[(this.head + i) % this.capacity] = void 0;
2214
+ }
2215
+ this.head = 0;
2216
+ this._size = 0;
2217
+ }
2218
+ };
2219
+
2220
+ // src/graph/explain.ts
2221
+ function explainPath(described, from, to, opts = {}) {
2222
+ const fromExists = from in described.nodes;
2223
+ const toExists = to in described.nodes;
2224
+ if (!fromExists) return makeFailure(from, to, "no-such-from");
2225
+ if (!toExists) return makeFailure(from, to, "no-such-to");
2226
+ const maxDepth = opts.maxDepth;
2227
+ if (maxDepth != null && (!Number.isInteger(maxDepth) || maxDepth < 0)) {
2228
+ throw new Error(`explainPath: maxDepth must be an integer >= 0`);
2229
+ }
2230
+ if (from === to) {
2231
+ if (opts.findCycle === true) {
2232
+ const cycle = findShortestCycle(described, from, opts);
2233
+ if (cycle != null) return cycle;
2234
+ }
2235
+ const step = buildStep(from, described.nodes[from], 0, opts);
2236
+ return makeSuccess(from, to, [step]);
2237
+ }
2238
+ if (maxDepth === 0) return makeFailure(from, to, "no-path");
2239
+ const result = bfsShortestPath(described, from, to, maxDepth);
2240
+ if (!result.found) {
2241
+ return makeFailure(from, to, result.truncated ? "max-depth-exceeded" : "no-path");
2242
+ }
2243
+ return makeSuccess(from, to, materializeSteps(described, result.pathOrder, opts));
2244
+ }
2245
+ function bfsShortestPath(described, from, to, maxDepth) {
2246
+ const pred = /* @__PURE__ */ new Map();
2247
+ const queue = [{ path: to, depth: 0 }];
2248
+ const visited = /* @__PURE__ */ new Set([to]);
2249
+ let head = 0;
2250
+ let truncated = false;
2251
+ while (head < queue.length) {
2252
+ const cur = queue[head++];
2253
+ if (cur.path === from) break;
2254
+ if (maxDepth != null && cur.depth >= maxDepth) {
2255
+ const node3 = described.nodes[cur.path];
2256
+ if (node3?.deps && node3.deps.length > 0) truncated = true;
2257
+ continue;
2258
+ }
2259
+ const node2 = described.nodes[cur.path];
2260
+ if (node2 == null) continue;
2261
+ const deps = node2.deps ?? [];
2262
+ const slots = /* @__PURE__ */ new Map();
2263
+ for (let i = 0; i < deps.length; i++) {
2264
+ const dep = deps[i];
2265
+ if (!dep) continue;
2266
+ let arr = slots.get(dep);
2267
+ if (arr == null) {
2268
+ arr = [];
2269
+ slots.set(dep, arr);
2270
+ }
2271
+ arr.push(i);
2272
+ }
2273
+ for (const [dep, indices] of slots) {
2274
+ if (visited.has(dep)) continue;
2275
+ visited.add(dep);
2276
+ pred.set(dep, { from: cur.path, depIndices: indices });
2277
+ queue.push({ path: dep, depth: cur.depth + 1 });
2278
+ }
2279
+ }
2280
+ if (!pred.has(from)) {
2281
+ return { found: false, pathOrder: [], truncated };
2282
+ }
2283
+ const pathOrder = [{ path: from }];
2284
+ let cursor = from;
2285
+ while (cursor !== to) {
2286
+ const p = pred.get(cursor);
2287
+ if (p == null) return { found: false, pathOrder: [], truncated: false };
2288
+ pathOrder[pathOrder.length - 1].depIndices = p.depIndices;
2289
+ pathOrder.push({ path: p.from });
2290
+ cursor = p.from;
2291
+ }
2292
+ return { found: true, pathOrder, truncated: false };
2293
+ }
2294
+ function findShortestCycle(described, start, opts) {
2295
+ const startNode = described.nodes[start];
2296
+ if (startNode == null) return null;
2297
+ const startDeps = startNode.deps ?? [];
2298
+ const selfSlots = [];
2299
+ for (let i = 0; i < startDeps.length; i++) if (startDeps[i] === start) selfSlots.push(i);
2300
+ if (selfSlots.length > 0) {
2301
+ const step0 = buildStep(start, startNode, 0, opts);
2302
+ step0.dep_index = selfSlots[0];
2303
+ const step1 = buildStep(start, startNode, 1, opts);
2304
+ return makeSuccess(start, start, [step0, step1]);
2305
+ }
2306
+ let best = null;
2307
+ for (let i = 0; i < startDeps.length; i++) {
2308
+ const dep = startDeps[i];
2309
+ if (!dep || dep === start) continue;
2310
+ const sub = bfsShortestPath(described, dep, start, opts.maxDepth);
2311
+ if (!sub.found) continue;
2312
+ if (best == null || sub.pathOrder.length < best.pathOrder.length) {
2313
+ best = sub;
2314
+ best = {
2315
+ found: true,
2316
+ pathOrder: [{ path: start, depIndices: [i] }, ...sub.pathOrder],
2317
+ truncated: false
2318
+ };
2319
+ }
2320
+ }
2321
+ if (best == null) return null;
2322
+ return makeSuccess(start, start, materializeSteps(described, best.pathOrder, opts));
2323
+ }
2324
+ function materializeSteps(described, pathOrder, opts) {
2325
+ return pathOrder.map((entry, i) => {
2326
+ const node2 = described.nodes[entry.path];
2327
+ const step = buildStep(entry.path, node2, i, opts);
2328
+ if (entry.depIndices != null && entry.depIndices.length > 0) {
2329
+ step.dep_index = entry.depIndices[0];
2330
+ if (entry.depIndices.length > 1) step.dep_indices = [...entry.depIndices];
2331
+ }
2332
+ return step;
2333
+ });
2334
+ }
2335
+ function buildStep(path, node2, hop, opts) {
2336
+ const step = {
2337
+ path,
2338
+ type: node2.type,
2339
+ hop
2340
+ };
2341
+ if (node2.status !== void 0) step.status = node2.status;
2342
+ if ("value" in node2) step.value = node2.value;
2343
+ if (node2.v != null) step.v = node2.v;
2344
+ const annotation = opts.annotations?.get(path) ?? node2.reason;
2345
+ if (annotation != null) step.reason = annotation;
2346
+ const lastMutation = opts.lastMutations?.get(path) ?? node2.lastMutation;
2347
+ if (lastMutation != null) step.lastMutation = lastMutation;
2348
+ return step;
2349
+ }
2350
+ function makeSuccess(from, to, steps) {
2351
+ return finalize(from, to, true, "ok", steps);
2352
+ }
2353
+ function makeFailure(from, to, reason) {
2354
+ return finalize(from, to, false, reason, []);
2355
+ }
2356
+ function finalize(from, to, found, reason, steps) {
2357
+ const text = renderChain(from, to, found, reason, steps);
2358
+ return {
2359
+ from,
2360
+ to,
2361
+ found,
2362
+ reason,
2363
+ steps,
2364
+ text,
2365
+ toJSON() {
2366
+ return { from, to, found, reason, steps };
2367
+ }
2368
+ };
2369
+ }
2370
+ function renderChain(from, to, found, reason, steps) {
2371
+ if (!found) {
2372
+ switch (reason) {
2373
+ case "no-such-from":
2374
+ return `explainPath: no node named "${from}"`;
2375
+ case "no-such-to":
2376
+ return `explainPath: no node named "${to}"`;
2377
+ case "max-depth-exceeded":
2378
+ return `explainPath: no path from "${from}" to "${to}" within maxDepth`;
2379
+ default:
2380
+ return `explainPath: no path from "${from}" to "${to}"`;
2381
+ }
2382
+ }
2383
+ const lines = [`Causal path: ${from} \u2192 ${to} (${steps.length} step(s))`];
2384
+ for (const step of steps) {
2385
+ const arrow = step.hop === 0 ? "\xB7" : "\u2193";
2386
+ const head = ` ${arrow} ${step.path} (${step.type}${step.status ? `/${step.status}` : ""})`;
2387
+ lines.push(head);
2388
+ if ("value" in step) {
2389
+ lines.push(` value: ${formatValue(step.value)}`);
2390
+ }
2391
+ if (step.reason != null) {
2392
+ lines.push(` reason: ${step.reason}`);
2393
+ }
2394
+ if (step.lastMutation != null) {
2395
+ const a = step.lastMutation.actor;
2396
+ lines.push(` actor: ${a.type}${a.id ? `:${a.id}` : ""}`);
2397
+ }
2398
+ }
2399
+ return lines.join("\n");
2400
+ }
2401
+ function formatValue(v) {
2402
+ if (v === void 0) return "<sentinel>";
2403
+ if (v === null) return "null";
2404
+ if (typeof v === "string") return JSON.stringify(v);
2405
+ if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") return String(v);
2406
+ try {
2407
+ const s = JSON.stringify(v);
2408
+ return s.length > 80 ? `${s.slice(0, 77)}...` : s;
2409
+ } catch {
2410
+ return String(v);
2411
+ }
2412
+ }
2413
+
2414
+ // src/extra/utils/sizeof.ts
2415
+ var OVERHEAD = {
2416
+ object: 56,
2417
+ array: 64,
2418
+ string: 40,
2419
+ // header; content added separately
2420
+ number: 8,
2421
+ boolean: 4,
2422
+ null: 0,
2423
+ undefined: 0,
2424
+ symbol: 40,
2425
+ bigint: 16,
2426
+ // base; scales with digit count (see `_bigintSize`)
2427
+ function: 120,
2428
+ map: 72,
2429
+ set: 72,
2430
+ mapEntry: 40,
2431
+ setEntry: 24,
2432
+ date: 24,
2433
+ regexp: 48,
2434
+ error: 64,
2435
+ url: 80,
2436
+ promise: 48,
2437
+ weakmap: 40,
2438
+ weakset: 40
2439
+ };
2440
+ var SIZEOF_SYMBOL = /* @__PURE__ */ Symbol.for("sizeof");
2441
+ function sizeof(value) {
2442
+ const seen = /* @__PURE__ */ new WeakSet();
2443
+ const seenBuffers = /* @__PURE__ */ new WeakSet();
2444
+ const stack = [value];
2445
+ let total = 0;
2446
+ while (stack.length > 0) {
2447
+ const v = stack.pop();
2448
+ total += _shallowSize(v, seen, seenBuffers, stack);
2449
+ }
2450
+ return total;
2451
+ }
2452
+ function _shallowSize(value, seen, seenBuffers, stack) {
2453
+ if (value === null || value === void 0) return 0;
2454
+ const t = typeof value;
2455
+ switch (t) {
2456
+ case "number":
2457
+ return OVERHEAD.number;
2458
+ case "boolean":
2459
+ return OVERHEAD.boolean;
2460
+ case "string":
2461
+ return OVERHEAD.string + value.length * 2;
2462
+ case "bigint":
2463
+ return OVERHEAD.bigint + _bigintSize(value);
2464
+ case "symbol":
2465
+ return OVERHEAD.symbol;
2466
+ case "function":
2467
+ if (seen.has(value)) return 0;
2468
+ seen.add(value);
2469
+ return OVERHEAD.function;
2470
+ case "undefined":
2471
+ return 0;
2472
+ }
2473
+ const obj = value;
2474
+ if (seen.has(obj)) return 0;
2475
+ seen.add(obj);
2476
+ const hook = obj[SIZEOF_SYMBOL];
2477
+ if (typeof hook === "function") {
2478
+ try {
2479
+ const reported = hook.call(obj);
2480
+ if (typeof reported === "number" && Number.isFinite(reported)) return reported;
2481
+ } catch {
2482
+ }
2483
+ }
2484
+ if (obj instanceof Date) return OVERHEAD.date;
2485
+ if (obj instanceof RegExp) return OVERHEAD.regexp + obj.source.length * 2;
2486
+ if (obj instanceof Error) {
2487
+ const m = obj.message ? obj.message.length * 2 : 0;
2488
+ const s = obj.stack ? obj.stack.length * 2 : 0;
2489
+ return OVERHEAD.error + m + s;
2490
+ }
2491
+ if (typeof URL !== "undefined" && obj instanceof URL) {
2492
+ return OVERHEAD.url + obj.href.length * 2;
2493
+ }
2494
+ if (typeof Promise !== "undefined" && obj instanceof Promise) {
2495
+ return OVERHEAD.promise;
2496
+ }
2497
+ if (obj instanceof WeakMap) return OVERHEAD.weakmap;
2498
+ if (obj instanceof WeakSet) return OVERHEAD.weakset;
2499
+ if (obj instanceof Map) {
2500
+ let size2 = OVERHEAD.map;
2501
+ for (const [k, v] of obj) {
2502
+ size2 += OVERHEAD.mapEntry;
2503
+ stack.push(k);
2504
+ stack.push(v);
2505
+ }
2506
+ return size2;
2507
+ }
2508
+ if (obj instanceof Set) {
2509
+ let size2 = OVERHEAD.set;
2510
+ for (const v of obj) {
2511
+ size2 += OVERHEAD.setEntry;
2512
+ stack.push(v);
2513
+ }
2514
+ return size2;
2515
+ }
2516
+ if (Array.isArray(obj)) {
2517
+ const size2 = OVERHEAD.array + obj.length * 8;
2518
+ for (const item of obj) stack.push(item);
2519
+ return size2;
2520
+ }
2521
+ if (obj instanceof ArrayBuffer) {
2522
+ if (seenBuffers.has(obj)) return 0;
2523
+ seenBuffers.add(obj);
2524
+ return obj.byteLength;
2525
+ }
2526
+ if (ArrayBuffer.isView(obj)) {
2527
+ const view = obj;
2528
+ if (seenBuffers.has(view.buffer)) return 48;
2529
+ seenBuffers.add(view.buffer);
2530
+ return view.buffer.byteLength + 48;
2531
+ }
2532
+ let size = OVERHEAD.object;
2533
+ const keys = Object.keys(obj);
2534
+ for (const key of keys) {
2535
+ size += OVERHEAD.string + key.length * 2;
2536
+ try {
2537
+ stack.push(obj[key]);
2538
+ } catch {
2539
+ }
2540
+ }
2541
+ return size;
2542
+ }
2543
+ function _bigintSize(n) {
2544
+ const abs = n < 0n ? -n : n;
2545
+ if (abs === 0n) return 0;
2546
+ const bits = abs.toString(2).length;
2547
+ return Math.ceil(bits / 32) * 8;
2548
+ }
2549
+
2550
+ // src/graph/profile.ts
2551
+ function graphProfile(graph, opts) {
2552
+ const topN = opts?.topN ?? 10;
2553
+ const desc = graph.describe({ detail: "standard" });
2554
+ const targets = [];
2555
+ const collector = graph._collectObserveTargets;
2556
+ if (typeof collector === "function") {
2557
+ collector.call(graph, "", targets);
2558
+ }
2559
+ const pathToNode = /* @__PURE__ */ new Map();
2560
+ for (const [p, n] of targets) pathToNode.set(p, n);
2561
+ const profiles = [];
2562
+ for (const [path, nodeDesc] of Object.entries(desc.nodes)) {
2563
+ const nd = pathToNode.get(path);
2564
+ const impl = nd instanceof NodeImpl ? nd : null;
2565
+ const valueSizeBytes = impl ? sizeof(impl.cache) : 0;
2566
+ const subscriberCount = impl ? impl._sinkCount : 0;
2567
+ const depCount = nodeDesc.deps?.length ?? 0;
2568
+ const isOrphanEffect = nodeDesc.type === "effect" && subscriberCount === 0;
2569
+ const orphanKind = subscriberCount === 0 ? nodeDesc.type === "effect" ? "orphan-effect" : nodeDesc.type === "derived" ? "idle-derived" : nodeDesc.type === "producer" ? "idle-producer" : null : null;
2570
+ profiles.push({
2571
+ path,
2572
+ type: nodeDesc.type,
2573
+ status: nodeDesc.status ?? "unknown",
2574
+ valueSizeBytes,
2575
+ subscriberCount,
2576
+ depCount,
2577
+ isOrphanEffect,
2578
+ orphanKind
2579
+ });
2580
+ }
2581
+ const totalValueSizeBytes = profiles.reduce((sum, p) => sum + p.valueSizeBytes, 0);
2582
+ const topBy = (key, cmp) => [...profiles].sort(cmp ?? ((a, b) => b[key] - a[key])).slice(0, topN);
2583
+ const orphans = profiles.filter((p) => p.orphanKind != null);
2584
+ const orphanEffects = profiles.filter((p) => p.isOrphanEffect);
2585
+ return {
2586
+ nodeCount: profiles.length,
2587
+ edgeCount: desc.edges.length,
2588
+ subgraphCount: desc.subgraphs.length,
2589
+ nodes: profiles,
2590
+ totalValueSizeBytes,
2591
+ hotspots: {
2592
+ byValueSize: topBy("valueSizeBytes"),
2593
+ bySubscriberCount: topBy("subscriberCount"),
2594
+ byDepCount: topBy("depCount")
2595
+ },
2596
+ orphans,
2597
+ orphanEffects
2598
+ };
2599
+ }
2600
+
2601
+ // src/graph/graph.ts
2602
+ var PATH_SEP = "::";
2603
+ var GRAPH_META_SEGMENT = "__meta__";
2604
+ var SNAPSHOT_VERSION = 1;
2605
+ function drainDisposers(set, graphName) {
2606
+ const cap = Math.max(16, set.size * 4);
2607
+ let iterations = 0;
2608
+ while (set.size > 0) {
2609
+ if (iterations++ >= cap) {
2610
+ console.error(
2611
+ `[Graph "${graphName}".destroy] disposer drain exceeded cap (${cap}); ${set.size} disposer(s) discarded`
2612
+ );
2613
+ set.clear();
2614
+ return;
2615
+ }
2616
+ const it = set.values().next();
2617
+ if (it.done) return;
2618
+ const dispose = it.value;
2619
+ set.delete(dispose);
2620
+ try {
2621
+ dispose();
2622
+ } catch (err) {
2623
+ console.error(`[Graph "${graphName}".destroy] disposer threw:`, err);
2624
+ }
2625
+ }
2626
+ }
2627
+ function computeVersionFingerprint(nodes) {
2628
+ const parts = [];
2629
+ for (const path of Object.keys(nodes).sort()) {
2630
+ const v = nodes[path].v;
2631
+ if (v != null) parts.push(`${path} ${v.id} ${v.version}`);
2632
+ }
2633
+ return parts.join("\n");
2634
+ }
2635
+ function parseSnapshotEnvelope(data) {
2636
+ if (data.version !== SNAPSHOT_VERSION) {
2637
+ throw new Error(
2638
+ `unsupported snapshot version ${String(data.version)} (expected ${SNAPSHOT_VERSION})`
2639
+ );
2640
+ }
2641
+ for (const key of ["name", "nodes", "edges", "subgraphs"]) {
2642
+ if (!(key in data)) {
2643
+ throw new Error(`snapshot missing required key "${key}"`);
2644
+ }
2645
+ }
2646
+ if (typeof data.name !== "string") {
2647
+ throw new TypeError(`snapshot 'name' must be a string`);
2648
+ }
2649
+ if (typeof data.nodes !== "object" || data.nodes === null || Array.isArray(data.nodes)) {
2650
+ throw new TypeError(`snapshot 'nodes' must be an object`);
2651
+ }
2652
+ if (!Array.isArray(data.edges)) {
2653
+ throw new TypeError(`snapshot 'edges' must be an array`);
2654
+ }
2655
+ if (!Array.isArray(data.subgraphs)) {
2656
+ throw new TypeError(`snapshot 'subgraphs' must be an array`);
2657
+ }
2658
+ }
2659
+ function deepEqual(a, b) {
2660
+ const seen = /* @__PURE__ */ new WeakMap();
2661
+ const walk = (x, y) => {
2662
+ if (Object.is(x, y)) return true;
2663
+ if (x == null || y == null || typeof x !== "object" || typeof y !== "object") return false;
2664
+ let seenRhs = seen.get(x);
2665
+ if (seenRhs == null) {
2666
+ seenRhs = /* @__PURE__ */ new WeakSet();
2667
+ seen.set(x, seenRhs);
2668
+ }
2669
+ if (seenRhs.has(y)) return true;
2670
+ seenRhs.add(y);
2671
+ const ctorA = x.constructor;
2672
+ const ctorB = y.constructor;
2673
+ if (ctorA !== ctorB) return false;
2674
+ if (x instanceof Date) return x.getTime() === y.getTime();
2675
+ if (x instanceof RegExp)
2676
+ return x.source === y.source && x.flags === y.flags;
2677
+ if (Array.isArray(x)) {
2678
+ const arrB = y;
2679
+ if (x.length !== arrB.length) return false;
2680
+ for (let i = 0; i < x.length; i++) {
2681
+ if (!walk(x[i], arrB[i])) return false;
2682
+ }
2683
+ return true;
2684
+ }
2685
+ if (x instanceof Map) {
2686
+ const mB = y;
2687
+ if (x.size !== mB.size) return false;
2688
+ for (const [k, v] of x) {
2689
+ if (!mB.has(k) || !walk(v, mB.get(k))) return false;
2690
+ }
2691
+ return true;
2692
+ }
2693
+ if (x instanceof Set) {
2694
+ const sB = y;
2695
+ if (x.size !== sB.size) return false;
2696
+ for (const v of x) {
2697
+ let found = false;
2698
+ for (const w of sB) {
2699
+ if (walk(v, w)) {
2700
+ found = true;
2701
+ break;
2702
+ }
2703
+ }
2704
+ if (!found) return false;
2705
+ }
2706
+ return true;
2707
+ }
2708
+ if (ArrayBuffer.isView(x)) {
2709
+ const taA = x;
2710
+ const taB = y;
2711
+ if (taA.length !== taB.length) return false;
2712
+ for (let i = 0; i < taA.length; i++) if (taA[i] !== taB[i]) return false;
2713
+ return true;
2714
+ }
2715
+ const keysA = Object.keys(x);
2716
+ const keysB = Object.keys(y);
2717
+ if (keysA.length !== keysB.length) return false;
2718
+ const setB = new Set(keysB);
2719
+ for (const k of keysA) {
2720
+ if (!setB.has(k)) return false;
2721
+ if (!walk(x[k], y[k])) return false;
2722
+ }
2723
+ return true;
2724
+ };
2725
+ return walk(a, b);
2726
+ }
2727
+ function sortJsonValue(value) {
2728
+ if (value === null || typeof value !== "object") {
2729
+ return value;
2730
+ }
2731
+ if (Array.isArray(value)) {
2732
+ return value.map(sortJsonValue);
2733
+ }
2734
+ const obj = value;
2735
+ const keys = Object.keys(obj).sort();
2736
+ const out = {};
2737
+ for (const k of keys) {
2738
+ out[k] = sortJsonValue(obj[k]);
2739
+ }
2740
+ return out;
2741
+ }
2742
+ function escapeMermaidLabel(value) {
2743
+ return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
2744
+ }
2745
+ function escapeD2Label(value) {
2746
+ return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
2747
+ }
2748
+ function d2DirectionFromGraphDirection(direction) {
2749
+ if (direction === "TD") return "down";
2750
+ if (direction === "BT") return "up";
2751
+ if (direction === "RL") return "left";
2752
+ return "right";
2753
+ }
2754
+ function collectDiagramArrows(described) {
2755
+ const seen = /* @__PURE__ */ new Set();
2756
+ const arrows = [];
2757
+ function add(from, to) {
2758
+ const key = `${from}\0${to}`;
2759
+ if (seen.has(key)) return;
2760
+ seen.add(key);
2761
+ arrows.push([from, to]);
2762
+ }
2763
+ for (const [path, info] of Object.entries(described.nodes)) {
2764
+ const deps = info.deps;
2765
+ if (deps) {
2766
+ for (const dep of deps) add(dep, path);
2767
+ }
2768
+ }
2769
+ for (const edge of described.edges) add(edge.from, edge.to);
2770
+ return arrows;
2771
+ }
2772
+ function normalizeDiagramDirection(direction) {
2773
+ if (direction === void 0) return "LR";
2774
+ if (direction === "TD" || direction === "LR" || direction === "BT" || direction === "RL") {
2775
+ return direction;
2776
+ }
2777
+ throw new Error(
2778
+ `invalid diagram direction ${String(direction)}; expected one of: TD, LR, BT, RL`
2779
+ );
2780
+ }
2781
+ function renderDescribeAsJson(d, options) {
2782
+ const includeEdges = options.includeEdges ?? true;
2783
+ const includeSubgraphs = options.includeSubgraphs ?? true;
2784
+ const { expand: _expand, ...rest } = d;
2785
+ const payload = {
2786
+ ...rest,
2787
+ edges: includeEdges ? d.edges : [],
2788
+ subgraphs: includeSubgraphs ? d.subgraphs : []
2789
+ };
2790
+ const text = JSON.stringify(sortJsonValue(payload), null, options.indent ?? 2);
2791
+ options.logger?.(text);
2792
+ return text;
2793
+ }
2794
+ function renderDescribeAsPretty(d, options) {
2795
+ const includeEdges = options.includeEdges ?? true;
2796
+ const includeSubgraphs = options.includeSubgraphs ?? true;
2797
+ const lines = [];
2798
+ lines.push(`Graph ${d.name}`);
2799
+ lines.push("Nodes:");
2800
+ for (const path of Object.keys(d.nodes).sort()) {
2801
+ const n = d.nodes[path];
2802
+ lines.push(`- ${path} (${n.type}/${n.status}): ${describeData(n.value)}`);
2803
+ }
2804
+ if (includeEdges) {
2805
+ lines.push("Edges:");
2806
+ for (const edge of d.edges) {
2807
+ lines.push(`- ${edge.from} -> ${edge.to}`);
2808
+ }
2809
+ }
2810
+ if (includeSubgraphs) {
2811
+ lines.push("Subgraphs:");
2812
+ for (const sg of d.subgraphs) {
2813
+ lines.push(`- ${sg}`);
2814
+ }
2815
+ }
2816
+ const text = lines.join("\n");
2817
+ options.logger?.(text);
2818
+ return text;
2819
+ }
2820
+ function renderDescribeAsMermaid(d, options) {
2821
+ const direction = normalizeDiagramDirection(options.direction);
2822
+ const paths = Object.keys(d.nodes).sort();
2823
+ const ids = /* @__PURE__ */ new Map();
2824
+ for (let i = 0; i < paths.length; i += 1) ids.set(paths[i], `n${i}`);
2825
+ const lines = [`flowchart ${direction}`];
2826
+ for (const path of paths) {
2827
+ const id = ids.get(path);
2828
+ lines.push(` ${id}["${escapeMermaidLabel(path)}"]`);
2829
+ }
2830
+ for (const [from, to] of collectDiagramArrows(d)) {
2831
+ const fromId = ids.get(from);
2832
+ const toId = ids.get(to);
2833
+ if (!fromId || !toId) continue;
2834
+ lines.push(` ${fromId} --> ${toId}`);
2835
+ }
2836
+ return lines.join("\n");
2837
+ }
2838
+ function renderDescribeAsD2(d, options) {
2839
+ const direction = normalizeDiagramDirection(options.direction);
2840
+ const paths = Object.keys(d.nodes).sort();
2841
+ const ids = /* @__PURE__ */ new Map();
2842
+ for (let i = 0; i < paths.length; i += 1) ids.set(paths[i], `n${i}`);
2843
+ const lines = [`direction: ${d2DirectionFromGraphDirection(direction)}`];
2844
+ for (const path of paths) {
2845
+ const id = ids.get(path);
2846
+ lines.push(`${id}: "${escapeD2Label(path)}"`);
2847
+ }
2848
+ for (const [from, to] of collectDiagramArrows(d)) {
2849
+ const fromId = ids.get(from);
2850
+ const toId = ids.get(to);
2851
+ if (!fromId || !toId) continue;
2852
+ lines.push(`${fromId} -> ${toId}`);
2853
+ }
2854
+ return lines.join("\n");
2855
+ }
2856
+ function escapeRegexLiteral(value) {
2857
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2858
+ }
2859
+ function globToRegex(pattern) {
2860
+ let re = "^";
2861
+ for (let i = 0; i < pattern.length; i += 1) {
2862
+ const ch = pattern[i];
2863
+ if (ch === "*") {
2864
+ re += ".*";
2865
+ continue;
2866
+ }
2867
+ if (ch === "?") {
2868
+ re += ".";
2869
+ continue;
2870
+ }
2871
+ if (ch === "[") {
2872
+ const end = pattern.indexOf("]", i + 1);
2873
+ if (end <= i + 1) {
2874
+ re += "\\[";
2875
+ continue;
2876
+ }
2877
+ let cls = pattern.slice(i + 1, end);
2878
+ if (cls.startsWith("!")) cls = `^${cls.slice(1)}`;
2879
+ cls = cls.replace(/\\/g, "\\\\");
2880
+ re += `[${cls}]`;
2881
+ i = end;
2882
+ continue;
2883
+ }
2884
+ re += escapeRegexLiteral(ch);
2885
+ }
2886
+ re += "$";
2887
+ return new RegExp(re);
2888
+ }
2889
+ var OBSERVE_ANSI_THEME = {
2890
+ data: "\x1B[32m",
2891
+ dirty: "\x1B[33m",
2892
+ resolved: "\x1B[36m",
2893
+ invalidate: "\x1B[93m",
2894
+ pause: "\x1B[90m",
2895
+ resume: "\x1B[96m",
2896
+ complete: "\x1B[34m",
2897
+ error: "\x1B[31m",
2898
+ teardown: "\x1B[91m",
2899
+ derived: "\x1B[35m",
2900
+ path: "\x1B[90m",
2901
+ reset: "\x1B[0m"
2902
+ };
2903
+ var OBSERVE_NO_COLOR_THEME = {
2904
+ data: "",
2905
+ dirty: "",
2906
+ resolved: "",
2907
+ invalidate: "",
2908
+ pause: "",
2909
+ resume: "",
2910
+ complete: "",
2911
+ error: "",
2912
+ teardown: "",
2913
+ derived: "",
2914
+ path: "",
2915
+ reset: ""
2916
+ };
2917
+ function describeData(value) {
2918
+ if (typeof value === "string") return JSON.stringify(value);
2919
+ if (typeof value === "number" || typeof value === "boolean" || value == null)
2920
+ return String(value);
2921
+ try {
2922
+ return JSON.stringify(value);
2923
+ } catch {
2924
+ return "[unserializable]";
2925
+ }
2926
+ }
2927
+ function resolveObserveTheme(theme) {
2928
+ if (theme === "none") return OBSERVE_NO_COLOR_THEME;
2929
+ if (theme === "ansi" || theme == null) return OBSERVE_ANSI_THEME;
2930
+ return {
2931
+ data: theme.data ?? "",
2932
+ dirty: theme.dirty ?? "",
2933
+ resolved: theme.resolved ?? "",
2934
+ invalidate: theme.invalidate ?? "",
2935
+ pause: theme.pause ?? "",
2936
+ resume: theme.resume ?? "",
2937
+ complete: theme.complete ?? "",
2938
+ error: theme.error ?? "",
2939
+ teardown: theme.teardown ?? "",
2940
+ derived: theme.derived ?? "",
2941
+ path: theme.path ?? "",
2942
+ reset: theme.reset ?? ""
2943
+ };
2944
+ }
2945
+ function resolveObserveDetail(opts) {
2946
+ if (opts == null) return {};
2947
+ const detail = opts.detail;
2948
+ if (detail === "full") {
2949
+ return {
2950
+ ...opts,
2951
+ structured: opts.structured ?? true,
2952
+ timeline: opts.timeline ?? true,
2953
+ causal: opts.causal ?? true,
2954
+ derived: opts.derived ?? true
2955
+ };
2956
+ }
2957
+ if (detail === "minimal") {
2958
+ return { ...opts, structured: opts.structured ?? true };
2959
+ }
2960
+ return opts;
2961
+ }
2962
+ function assertNoControlChars(name, graphName, label) {
2963
+ for (let i = 0; i < name.length; i++) {
2964
+ const c = name.charCodeAt(i);
2965
+ if (c < 32 || c === 127) {
2966
+ throw new Error(
2967
+ `Graph "${graphName}": ${label} "${name}" must not contain control character (U+${c.toString(16).padStart(4, "0").toUpperCase()} at index ${i})`
2968
+ );
2969
+ }
2970
+ }
2971
+ }
2972
+ function assertRegisterableName(name, graphName, label) {
2973
+ if (name === "") {
2974
+ throw new Error(`Graph "${graphName}": ${label} name must be non-empty`);
2975
+ }
2976
+ if (name.includes(PATH_SEP)) {
2977
+ throw new Error(
2978
+ `Graph "${graphName}": ${label} "${name}" must not contain '${PATH_SEP}' (path separator)`
2979
+ );
2980
+ }
2981
+ if (name === GRAPH_META_SEGMENT) {
2982
+ throw new Error(
2983
+ `Graph "${graphName}": ${label} name "${GRAPH_META_SEGMENT}" is reserved for meta companion paths`
2984
+ );
2985
+ }
2986
+ assertNoControlChars(name, graphName, label);
2987
+ }
2988
+ function splitPath(path, graphName) {
2989
+ if (path === "") {
2990
+ throw new Error(`Graph "${graphName}": resolve path must be non-empty`);
2991
+ }
2992
+ const segments = path.split(PATH_SEP);
2993
+ for (const s of segments) {
2994
+ if (s === "") {
2995
+ throw new Error(`Graph "${graphName}": resolve path has empty segment`);
2996
+ }
2997
+ }
2998
+ return segments;
2999
+ }
3000
+ function filterMetaMessages(messages, config) {
3001
+ let anyFiltered = false;
3002
+ for (const m of messages) {
3003
+ if (!config.isMetaPassthrough(m[0])) {
3004
+ anyFiltered = true;
3005
+ break;
3006
+ }
3007
+ }
3008
+ if (!anyFiltered) return messages;
3009
+ const kept = messages.filter((m) => config.isMetaPassthrough(m[0]));
3010
+ return kept;
3011
+ }
3012
+ function teardownMountedGraph(root) {
3013
+ for (const child of root._mounts.values()) {
3014
+ teardownMountedGraph(child);
3015
+ }
3016
+ for (const n of root._nodes.values()) {
3017
+ try {
3018
+ n.down([[TEARDOWN]], { internal: true });
3019
+ } catch {
3020
+ }
3021
+ }
3022
+ }
3023
+ var Graph = class _Graph {
3024
+ name;
3025
+ opts;
3026
+ /** Protocol config bound to this graph (defaults to `defaultConfig`). */
3027
+ config;
3028
+ /** @internal — exposed for {@link teardownMountedGraph} and cross-graph helpers. */
3029
+ _nodes = /* @__PURE__ */ new Map();
3030
+ /**
3031
+ * @internal Reverse lookup for duplicate-instance detection in
3032
+ * {@link Graph.add} — O(1) replacement for an O(n) scan of `_nodes`.
3033
+ * Weak so nodes can be GC'd after `remove()` even if a caller keeps the
3034
+ * map alive via some unusual pattern.
3035
+ */
3036
+ _nodeToName = /* @__PURE__ */ new WeakMap();
3037
+ /** @internal — exposed for {@link teardownMountedGraph}. */
3038
+ _mounts = /* @__PURE__ */ new Map();
3039
+ /**
3040
+ * @internal Parent graph if this instance is mounted. `undefined` when
3041
+ * this is the root or when the graph has been unmounted. Used for
3042
+ * reparenting rejection + O(depth) ancestor walks.
3043
+ */
3044
+ _parent = void 0;
3045
+ _storageDisposers = /* @__PURE__ */ new Set();
3046
+ _disposers = /* @__PURE__ */ new Set();
3047
+ /**
3048
+ * @internal Lazy `TopologyEvent` producer. Created on first `.topology`
3049
+ * access. Zero cost until something subscribes — producer fn only runs when
3050
+ * the first sink attaches, registering one handler into
3051
+ * {@link Graph._topologyEmitters}.
3052
+ */
3053
+ _topology;
3054
+ /**
3055
+ * @internal Active emit handlers for the topology producer. Each entry is
3056
+ * the closure registered by the producer fn on activation; cleared on
3057
+ * deactivation. `_emitTopology` broadcasts through every entry (there is at
3058
+ * most one per activation cycle of the producer).
3059
+ */
3060
+ _topologyEmitters = /* @__PURE__ */ new Set();
3061
+ /**
3062
+ * @param name - Non-empty graph id (must not contain `::` and must not
3063
+ * equal the reserved meta segment `__meta__`).
3064
+ * @param opts - See {@link GraphOptions}. Stored frozen on the instance.
3065
+ */
3066
+ constructor(name, opts) {
3067
+ if (name === "") {
3068
+ throw new Error("Graph name must be non-empty");
3069
+ }
3070
+ if (name.includes(PATH_SEP)) {
3071
+ throw new Error(`Graph name must not contain '${PATH_SEP}' (got "${name}")`);
3072
+ }
3073
+ if (name === GRAPH_META_SEGMENT) {
3074
+ throw new Error(`Graph name "${GRAPH_META_SEGMENT}" is reserved for meta companion paths`);
3075
+ }
3076
+ this.name = name;
3077
+ this.opts = Object.freeze({ ...opts ?? {} });
3078
+ this.config = opts?.config ?? defaultConfig;
3079
+ this._traceRing = new RingBuffer(opts?.traceCapacity ?? 1e3);
3080
+ if (opts?.versioning != null) {
3081
+ this.setVersioning(opts.versioning);
3082
+ }
3083
+ }
3084
+ /**
3085
+ * Walk ancestors up through `_parent`. Returns the chain starting at this
3086
+ * instance, ending at the root (a graph with no parent). O(depth).
3087
+ *
3088
+ * @param includeSelf - Include `this` in the chain (default `true`).
3089
+ */
3090
+ ancestors(includeSelf = true) {
3091
+ const out = [];
3092
+ let p = includeSelf ? this : this._parent;
3093
+ while (p != null) {
3094
+ out.push(p);
3095
+ p = p._parent;
3096
+ }
3097
+ return out;
3098
+ }
3099
+ // ——————————————————————————————————————————————————————————————
3100
+ // Topology companion (structural-change event stream)
3101
+ // ——————————————————————————————————————————————————————————————
3102
+ /**
3103
+ * Reactive stream of structural changes to this graph's own registry
3104
+ * (add / mount / remove). Value mutations live on `observe()`; this
3105
+ * companion only fires when the topology shape changes.
3106
+ *
3107
+ * Lazy: the underlying node is created on first access and activates when
3108
+ * something subscribes. No emission replay — late subscribers do not
3109
+ * receive historical events and should snapshot via {@link Graph.describe}
3110
+ * before listening for incremental changes. Events that fire while the
3111
+ * producer has zero subscribers are dropped (no retention).
3112
+ *
3113
+ * Own-graph only: a parent's `topology` does NOT emit for structural
3114
+ * changes inside a mounted child. Transitive consumers subscribe to each
3115
+ * child's topology separately (recurse through `topology`'s own "added"
3116
+ * events with `nodeKind: "mount"` to discover new children).
3117
+ *
3118
+ * See {@link TopologyEvent} for payload shape.
3119
+ *
3120
+ * @category observability
3121
+ */
3122
+ get topology() {
3123
+ if (this._topology == null) {
3124
+ this._topology = producer(
3125
+ (actions) => {
3126
+ const handler = (event) => {
3127
+ actions.emit(event);
3128
+ };
3129
+ this._topologyEmitters.add(handler);
3130
+ return () => {
3131
+ this._topologyEmitters.delete(handler);
3132
+ };
3133
+ },
3134
+ { name: `${this.name}_topology` }
3135
+ );
3136
+ }
3137
+ return this._topology;
3138
+ }
3139
+ /**
3140
+ * @internal Fire a {@link TopologyEvent} to every active subscriber of
3141
+ * `this.topology`. No-op when the topology node has never been accessed or
3142
+ * currently has no sinks — zero cost for graphs nobody observes.
3143
+ */
3144
+ _emitTopology(event) {
3145
+ if (this._topology == null || this._topologyEmitters.size === 0) return;
3146
+ for (const h of this._topologyEmitters) h(event);
3147
+ }
3148
+ // ——————————————————————————————————————————————————————————————
3149
+ // Node registry
3150
+ // ——————————————————————————————————————————————————————————————
3151
+ /**
3152
+ * Registers a node under a local name. Fails if the name is already used,
3153
+ * reserved by a mount, the same node instance is already registered, or
3154
+ * the node is torn down.
3155
+ *
3156
+ * Returns the registered node so callers can chain:
3157
+ * `const counter = g.add("counter", state(0))`.
3158
+ *
3159
+ * @param name - Local key (no `::`).
3160
+ * @param node - Node instance to own.
3161
+ */
3162
+ add(name, node2) {
3163
+ assertRegisterableName(name, this.name, "add");
3164
+ if (this._mounts.has(name)) {
3165
+ throw new Error(`Graph "${this.name}": name "${name}" is already a mount point`);
3166
+ }
3167
+ if (this._nodes.has(name)) {
3168
+ throw new Error(`Graph "${this.name}": node "${name}" already exists`);
3169
+ }
3170
+ const existingName = this._nodeToName.get(node2);
3171
+ if (existingName !== void 0) {
3172
+ throw new Error(
3173
+ `Graph "${this.name}": node instance already registered as "${existingName}"`
3174
+ );
3175
+ }
3176
+ this._nodes.set(name, node2);
3177
+ this._nodeToName.set(node2, name);
3178
+ this._emitTopology({ kind: "added", name, nodeKind: "node" });
3179
+ return node2;
3180
+ }
3181
+ /**
3182
+ * Bulk-apply a minimum versioning level to every currently-registered node
3183
+ * in this graph (roadmap §6.0). `_applyVersioning` is monotonic — nodes
3184
+ * already at a higher level are untouched. The method refuses to run
3185
+ * mid-wave; invoke at setup time before any external subscribers attach.
3186
+ *
3187
+ * **Not** a default-for-future-adds mechanism — that's what
3188
+ * `config.defaultVersioning` is for. Nodes added after this call do NOT
3189
+ * automatically inherit `level`; register new nodes with their own
3190
+ * `opts.versioning` or set `config.defaultVersioning` before construction.
3191
+ *
3192
+ * **Scope:** local only. Does not propagate to mounted subgraphs.
3193
+ *
3194
+ * @param level - `0` for V0, `1` for V1, or `undefined` to no-op.
3195
+ */
3196
+ setVersioning(level) {
3197
+ if (level == null) return;
3198
+ for (const node2 of this._nodes.values()) {
3199
+ if (node2 instanceof NodeImpl) {
3200
+ node2._applyVersioning(level);
3201
+ }
3202
+ }
3203
+ }
3204
+ /**
3205
+ * Unregisters a node or unmounts a subgraph and sends `[[TEARDOWN]]` to the
3206
+ * removed node or recursively through the mounted subtree (§3.2).
3207
+ *
3208
+ * @param name - Local mount or node name.
3209
+ * @returns Audit record of what was removed: `{kind, nodes, mounts}`.
3210
+ * `kind: "node"` → `nodes: [name]`, `mounts: []`. `kind: "mount"` →
3211
+ * `nodes` lists every primary node torn down across the subtree (sorted
3212
+ * qualified paths relative to the unmounted subgraph) and `mounts` lists
3213
+ * the mounted subgraphs in depth-first order including `name` itself.
3214
+ */
3215
+ remove(name) {
3216
+ assertRegisterableName(name, this.name, "remove");
3217
+ const child = this._mounts.get(name);
3218
+ if (child) {
3219
+ const audit2 = { kind: "mount", nodes: [], mounts: [] };
3220
+ const targets = [];
3221
+ child._collectObserveTargets("", targets);
3222
+ for (const [p, n] of targets) {
3223
+ if (!p.includes(`${PATH_SEP}${GRAPH_META_SEGMENT}${PATH_SEP}`)) {
3224
+ audit2.nodes.push(p);
3225
+ }
3226
+ void n;
3227
+ }
3228
+ audit2.nodes.sort();
3229
+ audit2.mounts.push(name);
3230
+ audit2.mounts.push(...child._collectSubgraphs(`${name}${PATH_SEP}`));
3231
+ this._mounts.delete(name);
3232
+ child._parent = void 0;
3233
+ teardownMountedGraph(child);
3234
+ this._emitTopology({ kind: "removed", name, nodeKind: "mount", audit: audit2 });
3235
+ return audit2;
3236
+ }
3237
+ const node2 = this._nodes.get(name);
3238
+ if (!node2) {
3239
+ throw new Error(`Graph "${this.name}": unknown node or mount "${name}"`);
3240
+ }
3241
+ this._nodes.delete(name);
3242
+ this._nodeToName.delete(node2);
3243
+ node2.down([[TEARDOWN]], { internal: true });
3244
+ const audit = { kind: "node", nodes: [name], mounts: [] };
3245
+ this._emitTopology({ kind: "removed", name, nodeKind: "node", audit });
3246
+ return audit;
3247
+ }
3248
+ /**
3249
+ * Bulk remove — invokes {@link Graph.remove} for every local name matching
3250
+ * `filter`. Audit records merge into a single result. Mounted subgraphs
3251
+ * are included via `filter` receiving the mount name; internal subtree
3252
+ * entries are not walked directly (use describe + scan for tree-level
3253
+ * queries).
3254
+ *
3255
+ * @param filter - Predicate or glob. Glob strings support `*` within a
3256
+ * segment and `**` across segments (same grammar as `restore({only})`).
3257
+ * @returns Combined audit of all nodes + mounts removed.
3258
+ */
3259
+ removeAll(filter) {
3260
+ const match = typeof filter === "function" ? filter : (() => {
3261
+ const re = globToRegex(filter);
3262
+ return (n) => re.test(n);
3263
+ })();
3264
+ const audit = { kind: "mount", nodes: [], mounts: [] };
3265
+ const localNames = [...this._nodes.keys(), ...this._mounts.keys()].filter((n) => match(n));
3266
+ for (const name of localNames) {
3267
+ const sub = this.remove(name);
3268
+ audit.nodes.push(...sub.nodes);
3269
+ audit.mounts.push(...sub.mounts);
3270
+ }
3271
+ audit.nodes.sort();
3272
+ audit.mounts.sort();
3273
+ return audit;
3274
+ }
3275
+ /**
3276
+ * Iterable over locally-registered `[localName, Node]` pairs (sorted).
3277
+ * Does not recurse into mounts.
3278
+ */
3279
+ [Symbol.iterator]() {
3280
+ const sorted = [...this._nodes.keys()].sort();
3281
+ const nodes = this._nodes;
3282
+ let i = 0;
3283
+ return {
3284
+ [Symbol.iterator]() {
3285
+ return this;
3286
+ },
3287
+ next() {
3288
+ if (i >= sorted.length) return { value: void 0, done: true };
3289
+ const name = sorted[i++];
3290
+ return { value: [name, nodes.get(name)], done: false };
3291
+ }
3292
+ };
3293
+ }
3294
+ /**
3295
+ * Returns a node by local name or `::` qualified path.
3296
+ * Local names are looked up directly; paths with `::` delegate to {@link resolve}.
3297
+ *
3298
+ * @param name - Local name or qualified path.
3299
+ */
3300
+ node(name) {
3301
+ if (name === "") {
3302
+ throw new Error(`Graph "${this.name}": node name must be non-empty`);
3303
+ }
3304
+ if (name.includes(PATH_SEP)) {
3305
+ return this.resolve(name);
3306
+ }
3307
+ const n = this._nodes.get(name);
3308
+ if (!n) {
3309
+ throw new Error(`Graph "${this.name}": unknown node "${name}"`);
3310
+ }
3311
+ return n;
3312
+ }
3313
+ /**
3314
+ * Reads `graph.node(name).get()` — accepts `::` qualified paths (§3.2).
3315
+ *
3316
+ * @param name - Local name or qualified path.
3317
+ * @returns Cached value or `undefined`.
3318
+ */
3319
+ get(name) {
3320
+ return this.node(name).cache;
3321
+ }
3322
+ /**
3323
+ * Shorthand for `graph.node(name).down([[DATA, value]], { actor })` — accepts `::` qualified paths (§3.2).
3324
+ *
3325
+ * @param name - Local name or qualified path.
3326
+ * @param value - Next `DATA` payload.
3327
+ * @param options - Optional `actor` and `internal` guard bypass.
3328
+ */
3329
+ set(name, value, options) {
3330
+ const internal = options?.internal === true;
3331
+ this.node(name).down([[DATA, value]], {
3332
+ actor: options?.actor,
3333
+ internal,
3334
+ delivery: "write"
3335
+ });
3336
+ }
3337
+ /**
3338
+ * Atomic multi-node DATA write. Wraps every {@link Graph.set} call in a
3339
+ * single `batch(...)` so downstream dependents see one coalesced wave
3340
+ * instead of N cascading ones.
3341
+ *
3342
+ * @param entries - `{name → value}` map or `[name, value]` pairs.
3343
+ * @param options - Passed to each underlying `set` call (same `actor` + `internal` semantics).
3344
+ */
3345
+ setAll(entries, options) {
3346
+ const iter = Symbol.iterator in entries ? entries : Object.entries(entries);
3347
+ batch(() => {
3348
+ for (const [name, value] of iter) this.set(name, value, options);
3349
+ });
3350
+ }
3351
+ /**
3352
+ * Emit a single `[[INVALIDATE]]` (tier 1) on a node. Thin wrapper over
3353
+ * `node.down([[INVALIDATE]], …)` matching the {@link Graph.set} ergonomics.
3354
+ */
3355
+ invalidate(name, options) {
3356
+ const internal = options?.internal === true;
3357
+ this.node(name).down([[INVALIDATE]], {
3358
+ actor: options?.actor,
3359
+ internal,
3360
+ delivery: "write"
3361
+ });
3362
+ }
3363
+ /**
3364
+ * Emit a single `[[ERROR, err]]` (tier 4) on a node.
3365
+ */
3366
+ error(name, err, options) {
3367
+ const internal = options?.internal === true;
3368
+ this.node(name).down([[ERROR, err]], {
3369
+ actor: options?.actor,
3370
+ internal,
3371
+ delivery: "write"
3372
+ });
3373
+ }
3374
+ /**
3375
+ * Emit a single `[[COMPLETE]]` (tier 4) on a node, declaring the stream
3376
+ * cleanly finished. Distinct from {@link Graph.remove} (which emits
3377
+ * TEARDOWN and unregisters the node).
3378
+ */
3379
+ complete(name, options) {
3380
+ const internal = options?.internal === true;
3381
+ this.node(name).down([[COMPLETE]], {
3382
+ actor: options?.actor,
3383
+ internal,
3384
+ delivery: "write"
3385
+ });
3386
+ }
3387
+ // ——————————————————————————————————————————————————————————————
3388
+ // Edges (derived on-demand from node `_deps`)
3389
+ // ——————————————————————————————————————————————————————————————
3390
+ /**
3391
+ * Returns the full edge list for this graph tree, derived on demand from
3392
+ * each registered node's `_deps` (no stored registry). Local-only
3393
+ * (non-recursive) by default to match the historical `edges()` surface;
3394
+ * pass `{recursive: true}` to include mounted subgraphs with qualified
3395
+ * paths relative to this graph.
3396
+ *
3397
+ * Use {@link Graph.describe} for full-tree snapshots with edges already
3398
+ * qualified and paired with node metadata.
3399
+ */
3400
+ edges(opts) {
3401
+ const recursive = opts?.recursive === true;
3402
+ const nodeToLocal = /* @__PURE__ */ new Map();
3403
+ if (!recursive) {
3404
+ for (const [localName, n] of this._nodes) nodeToLocal.set(n, localName);
3405
+ const result2 = [];
3406
+ for (const [localName, n] of this._nodes) {
3407
+ if (!(n instanceof NodeImpl)) continue;
3408
+ for (const dep of n._deps) {
3409
+ const from = nodeToLocal.get(dep.node);
3410
+ if (from != null) result2.push([from, localName]);
3411
+ }
3412
+ }
3413
+ result2.sort(
3414
+ (a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0
3415
+ );
3416
+ return result2;
3417
+ }
3418
+ const targets = [];
3419
+ this._collectObserveTargets("", targets);
3420
+ const nodeToPath = /* @__PURE__ */ new Map();
3421
+ for (const [p, n] of targets) nodeToPath.set(n, p);
3422
+ const result = [];
3423
+ for (const [path, n] of targets) {
3424
+ if (!(n instanceof NodeImpl)) continue;
3425
+ for (const dep of n._deps) {
3426
+ const from = nodeToPath.get(dep.node);
3427
+ if (from != null) result.push([from, path]);
3428
+ }
3429
+ }
3430
+ result.sort(
3431
+ (a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0
3432
+ );
3433
+ return result;
3434
+ }
3435
+ // ——————————————————————————————————————————————————————————————
3436
+ // Composition
3437
+ // ——————————————————————————————————————————————————————————————
3438
+ /**
3439
+ * Embed a child graph at a local mount name (§3.4). Child nodes are reachable via
3440
+ * {@link Graph.resolve} using `::` delimited paths (§3.5). Lifecycle
3441
+ * {@link Graph.signal} visits mounted subgraphs recursively.
3442
+ *
3443
+ * Rejects: same name as existing node or mount, self-mount, mount cycles,
3444
+ * and the same child graph instance mounted twice on one parent.
3445
+ *
3446
+ * @param name - Local mount point.
3447
+ * @param child - Nested `Graph` instance.
3448
+ * @returns The mounted `child`, for chaining.
3449
+ */
3450
+ mount(name, child) {
3451
+ assertRegisterableName(name, this.name, "mount");
3452
+ if (this._nodes.has(name)) {
3453
+ throw new Error(
3454
+ `Graph "${this.name}": cannot mount at "${name}" \u2014 node with that name exists`
3455
+ );
3456
+ }
3457
+ if (this._mounts.has(name)) {
3458
+ throw new Error(`Graph "${this.name}": mount "${name}" already exists`);
3459
+ }
3460
+ if (child === this) {
3461
+ throw new Error(`Graph "${this.name}": cannot mount a graph into itself`);
3462
+ }
3463
+ if (child._parent != null) {
3464
+ throw new Error(
3465
+ `Graph "${this.name}": this child graph is already mounted on "${child._parent.name}"`
3466
+ );
3467
+ }
3468
+ for (let p = this; p != null; p = p._parent) {
3469
+ if (p === child) {
3470
+ throw new Error(`Graph "${this.name}": mount("${name}", \u2026) would create a mount cycle`);
3471
+ }
3472
+ }
3473
+ this._mounts.set(name, child);
3474
+ child._parent = this;
3475
+ this._emitTopology({ kind: "added", name, nodeKind: "mount" });
3476
+ return child;
3477
+ }
3478
+ /**
3479
+ * Look up a node by qualified path (§3.5). Segments are separated by `::`.
3480
+ *
3481
+ * If the first segment equals this graph's {@link Graph.name}, it is stripped
3482
+ * (so `root.resolve("app::a")` works when `root.name === "app"`). The strip
3483
+ * is applied **recursively** when descending into mounted children, so
3484
+ * `child.resolve("child::x")` also works when `child.name === "child"`.
3485
+ *
3486
+ * @param path - Qualified `::` path or local name.
3487
+ * @returns The resolved `Node`.
3488
+ */
3489
+ resolve(path) {
3490
+ const segments = splitPath(path, this.name);
3491
+ return this._resolveFromSegments(segments);
3492
+ }
3493
+ /**
3494
+ * Non-throwing {@link Graph.resolve}. Returns `undefined` instead of
3495
+ * throwing when the path does not resolve to a node.
3496
+ */
3497
+ tryResolve(path) {
3498
+ try {
3499
+ return this.resolve(path);
3500
+ } catch {
3501
+ return void 0;
3502
+ }
3503
+ }
3504
+ _resolveFromSegments(segments) {
3505
+ let seg = segments;
3506
+ if (seg[0] === this.name) {
3507
+ seg = seg.slice(1);
3508
+ if (seg.length === 0) {
3509
+ throw new Error(`Graph "${this.name}": resolve path ends at graph name only`);
3510
+ }
3511
+ }
3512
+ const head = seg[0];
3513
+ const rest = seg.slice(1);
3514
+ if (rest.length === 0) {
3515
+ const n = this._nodes.get(head);
3516
+ if (n) return n;
3517
+ if (this._mounts.has(head)) {
3518
+ throw new Error(
3519
+ `Graph "${this.name}": path ends at subgraph "${head}" \u2014 not a node (GRAPHREFLY-SPEC \xA73.5)`
3520
+ );
3521
+ }
3522
+ throw new Error(`Graph "${this.name}": unknown name "${head}"`);
3523
+ }
3524
+ const localN = this._nodes.get(head);
3525
+ if (localN && rest.length > 0 && rest[0] === GRAPH_META_SEGMENT) {
3526
+ return this._resolveMetaChainFromNode(localN, rest, seg.join(PATH_SEP));
3527
+ }
3528
+ const child = this._mounts.get(head);
3529
+ if (!child) {
3530
+ if (this._nodes.has(head)) {
3531
+ throw new Error(
3532
+ `Graph "${this.name}": "${head}" is a node; trailing path "${rest.join(PATH_SEP)}" is invalid`
3533
+ );
3534
+ }
3535
+ throw new Error(`Graph "${this.name}": unknown mount or node "${head}"`);
3536
+ }
3537
+ return child.resolve(rest.join(PATH_SEP));
3538
+ }
3539
+ /**
3540
+ * Resolve `::__meta__::key` segments from a registered primary node (possibly chained).
3541
+ */
3542
+ _resolveMetaChainFromNode(n, parts, fullPath) {
3543
+ let current = n;
3544
+ let i = 0;
3545
+ const p = [...parts];
3546
+ while (i < p.length) {
3547
+ if (p[i] !== GRAPH_META_SEGMENT) {
3548
+ throw new Error(
3549
+ `Graph "${this.name}": expected ${GRAPH_META_SEGMENT} segment in meta path "${fullPath}"`
3550
+ );
3551
+ }
3552
+ if (i + 1 >= p.length) {
3553
+ throw new Error(
3554
+ `Graph "${this.name}": meta path requires a key after ${GRAPH_META_SEGMENT} in "${fullPath}"`
3555
+ );
3556
+ }
3557
+ const key = p[i + 1];
3558
+ const next = current.meta[key];
3559
+ if (!next) {
3560
+ throw new Error(`Graph "${this.name}": unknown meta "${key}" in path "${fullPath}"`);
3561
+ }
3562
+ current = next;
3563
+ i += 2;
3564
+ }
3565
+ return current;
3566
+ }
3567
+ /**
3568
+ * Deliver a message batch to every registered node in this graph and, recursively,
3569
+ * in mounted child graphs (§3.7). Recurses into mounts first, then delivers to
3570
+ * local nodes (sorted by name). Each {@link Node} receives at most one delivery
3571
+ * per call (deduped by reference).
3572
+ *
3573
+ * **Primary-vs-meta filter asymmetry (intentional):** primary nodes receive the
3574
+ * unfiltered `messages` batch — that's the canonical data-plane flow. Companion
3575
+ * `meta` nodes receive a filtered subset keyed by the per-type `metaPassthrough`
3576
+ * flag on {@link GraphReFlyConfig}. Built-in defaults: PAUSE / RESUME / DATA /
3577
+ * RESOLVED pass through to meta; INVALIDATE / COMPLETE / ERROR / TEARDOWN do
3578
+ * not.
3579
+ *
3580
+ * **Where lifecycle terminals reach meta:**
3581
+ * - **TEARDOWN** — primary's `_emit` cascades to meta children directly (see
3582
+ * `core/node.ts` "Meta TEARDOWN fan-out" block) so meta is torn down with
3583
+ * its primary regardless of the signal-level filter.
3584
+ * - **COMPLETE / ERROR / INVALIDATE** — scoped to primaries on the broadcast
3585
+ * path. Meta companions are an attribution side-channel, not a lifecycle
3586
+ * participant; address meta directly via `meta.down(...)` if you need to
3587
+ * forward these. Audit confirmed 2026-04-17: no current meta consumer
3588
+ * relies on broadcast COMPLETE/ERROR/INVALIDATE delivery.
3589
+ *
3590
+ * @param messages - Batch to deliver to every registered node (and mounts, recursively).
3591
+ * @param options - Optional `actor` / `internal` for transport.
3592
+ */
3593
+ signal(messages, options) {
3594
+ if (options?.internal !== true) {
3595
+ for (const m of messages) {
3596
+ const tier = this.config.messageTier(m[0]);
3597
+ if (tier === 3) {
3598
+ throw new Error(
3599
+ `Graph "${this.name}": Graph.signal() rejects tier-3 messages (DATA / RESOLVED). Broadcast is for control-plane tiers (START / DIRTY / INVALIDATE / PAUSE / RESUME / COMPLETE / ERROR / TEARDOWN). For per-node value writes, use Graph.set or graph.node(name).down(...).`
3600
+ );
3601
+ }
3602
+ }
3603
+ }
3604
+ const errors = [];
3605
+ this._signalDeliver(messages, options ?? {}, /* @__PURE__ */ new Set(), errors);
3606
+ if (errors.length > 0) throw errors[0];
3607
+ }
3608
+ _signalDeliver(messages, opts, vis, errors) {
3609
+ for (const sub of this._mounts.values()) {
3610
+ sub._signalDeliver(messages, opts, vis, errors);
3611
+ }
3612
+ const internal = opts.internal === true;
3613
+ const downOpts = internal ? { internal: true } : { actor: opts.actor, delivery: "signal" };
3614
+ const metaMessages = filterMetaMessages(messages, this.config);
3615
+ for (const localName of [...this._nodes.keys()].sort()) {
3616
+ const n = this._nodes.get(localName);
3617
+ if (vis.has(n)) continue;
3618
+ vis.add(n);
3619
+ try {
3620
+ n.down(messages, downOpts);
3621
+ } catch (err) {
3622
+ if (err instanceof GuardDenied) throw err;
3623
+ errors.push(err);
3624
+ }
3625
+ if (metaMessages.length === 0) continue;
3626
+ this._signalMetaSubtree(n, metaMessages, vis, downOpts, errors);
3627
+ }
3628
+ }
3629
+ _signalMetaSubtree(root, messages, vis, downOpts, errors) {
3630
+ for (const mk of Object.keys(root.meta).sort()) {
3631
+ const mnode = root.meta[mk];
3632
+ if (vis.has(mnode)) continue;
3633
+ vis.add(mnode);
3634
+ try {
3635
+ mnode.down(messages, downOpts);
3636
+ } catch (err) {
3637
+ if (err instanceof GuardDenied) throw err;
3638
+ errors.push(err);
3639
+ }
3640
+ this._signalMetaSubtree(mnode, messages, vis, downOpts, errors);
3641
+ }
3642
+ }
3643
+ describe(options) {
3644
+ const actor = options?.actor;
3645
+ const filter = options?.filter;
3646
+ const includeFields = resolveDescribeFields(options?.detail, options?.fields);
3647
+ const isSpec = options?.format === "spec";
3648
+ const effectiveFields = isSpec ? resolveDescribeFields("minimal") : includeFields;
3649
+ const targets = [];
3650
+ this._collectObserveTargets("", targets);
3651
+ const nodeToPath = /* @__PURE__ */ new Map();
3652
+ for (const [p, n] of targets) {
3653
+ nodeToPath.set(n, p);
3654
+ }
3655
+ const nodes = {};
3656
+ for (const [p, n] of targets) {
3657
+ if (actor != null && !n.allowsObserve(actor)) continue;
3658
+ const raw = describeNode(n, effectiveFields);
3659
+ const deps = n instanceof NodeImpl ? n._deps.map((d) => nodeToPath.get(d.node) ?? d.node.name ?? "") : [];
3660
+ const { name: _name, ...rest } = raw;
3661
+ const entry = { ...rest, deps };
3662
+ if (!isSpec) {
3663
+ const reason = this._annotations.get(p);
3664
+ if (reason != null) entry.reason = reason;
3665
+ }
3666
+ if (filter != null) {
3667
+ if (typeof filter === "function") {
3668
+ const fn = filter;
3669
+ const pass = fn.length >= 2 ? fn(p, entry) : fn(entry);
3670
+ if (!pass) continue;
3671
+ } else {
3672
+ let match = true;
3673
+ for (const [fk, fv] of Object.entries(filter)) {
3674
+ const normalizedKey = fk === "deps_includes" ? "depsIncludes" : fk === "meta_has" ? "metaHas" : fk;
3675
+ if (normalizedKey === "depsIncludes") {
3676
+ if (!entry.deps.includes(String(fv))) {
3677
+ match = false;
3678
+ break;
3679
+ }
3680
+ continue;
3681
+ }
3682
+ if (normalizedKey === "metaHas") {
3683
+ if (!Object.hasOwn(entry.meta ?? {}, String(fv))) {
3684
+ match = false;
3685
+ break;
3686
+ }
3687
+ continue;
3688
+ }
3689
+ if (entry[normalizedKey] !== fv) {
3690
+ match = false;
3691
+ break;
3692
+ }
3693
+ }
3694
+ if (!match) continue;
3695
+ }
3696
+ }
3697
+ nodes[p] = entry;
3698
+ }
3699
+ const nodeKeys = new Set(Object.keys(nodes));
3700
+ let edges = this.edges({ recursive: true }).map(
3701
+ ([from, to]) => ({ from, to })
3702
+ );
3703
+ if (actor != null || filter != null) {
3704
+ edges = edges.filter((e) => nodeKeys.has(e.from) && nodeKeys.has(e.to));
3705
+ }
3706
+ const allSubgraphs = this._collectSubgraphs("");
3707
+ const subgraphs = actor != null || filter != null ? allSubgraphs.filter((sg) => {
3708
+ const prefix = `${sg}${PATH_SEP}`;
3709
+ return [...nodeKeys].some((k) => k === sg || k.startsWith(prefix));
3710
+ }) : allSubgraphs;
3711
+ const graph = this;
3712
+ const baseOpts = options;
3713
+ const struct = {
3714
+ name: this.name,
3715
+ nodes,
3716
+ edges,
3717
+ subgraphs,
3718
+ expand(detailOrFields) {
3719
+ const merged = { ...baseOpts, format: void 0 };
3720
+ if (Array.isArray(detailOrFields)) {
3721
+ merged.fields = detailOrFields;
3722
+ merged.detail = void 0;
3723
+ } else {
3724
+ merged.detail = detailOrFields;
3725
+ merged.fields = void 0;
3726
+ }
3727
+ return graph.describe(merged);
3728
+ }
3729
+ };
3730
+ const opts = options ?? {};
3731
+ const fmt = opts.format;
3732
+ if (fmt === "json") return renderDescribeAsJson(struct, opts);
3733
+ if (fmt === "pretty") return renderDescribeAsPretty(struct, opts);
3734
+ if (fmt === "mermaid") return renderDescribeAsMermaid(struct, opts);
3735
+ if (fmt === "d2") return renderDescribeAsD2(struct, opts);
3736
+ return struct;
3737
+ }
3738
+ _collectSubgraphs(prefix) {
3739
+ const out = [];
3740
+ for (const m of [...this._mounts.keys()].sort()) {
3741
+ const q = prefix === "" ? m : `${prefix}${m}`;
3742
+ out.push(q);
3743
+ out.push(...this._mounts.get(m)._collectSubgraphs(`${q}${PATH_SEP}`));
3744
+ }
3745
+ return out;
3746
+ }
3747
+ /**
3748
+ * Snapshot-based resource profile: per-node stats, orphan effect detection,
3749
+ * memory hotspots. Zero runtime overhead — walks nodes on demand.
3750
+ *
3751
+ * @param opts - Optional `topN` for hotspot limit (default 10).
3752
+ * @returns Aggregate profile with per-node details, hotspots, and orphan effects.
3753
+ */
3754
+ resourceProfile(opts) {
3755
+ return graphProfile(this, opts);
3756
+ }
3757
+ reachable(from, direction, opts = {}) {
3758
+ if (opts.withDetail === true) {
3759
+ return reachable(this.describe(), from, direction, {
3760
+ ...opts,
3761
+ withDetail: true
3762
+ });
3763
+ }
3764
+ return reachable(this.describe(), from, direction, opts);
3765
+ }
3766
+ /**
3767
+ * Causal walkback: shortest dep-chain from `from` to `to`, enriched with
3768
+ * each node's value, status, last-mutation actor, and reasoning annotation
3769
+ * from {@link Graph.trace}. Wraps {@link explainPath} (roadmap §9.2).
3770
+ *
3771
+ * @param from - Upstream node (the cause).
3772
+ * @param to - Downstream node (the effect).
3773
+ * @param opts - Optional `maxDepth` and `findCycle`. When `findCycle:true`
3774
+ * and `from === to`, returns the shortest cycle through other nodes
3775
+ * (useful for diagnosing feedback loops, COMPOSITION-GUIDE §7).
3776
+ * Annotations and lastMutations are collected automatically from the
3777
+ * live graph.
3778
+ */
3779
+ explain(from, to, opts) {
3780
+ const described = this.describe({ detail: "full" });
3781
+ const annotations = new Map(this._annotations);
3782
+ const lastMutations = /* @__PURE__ */ new Map();
3783
+ for (const [path, n] of Object.entries(described.nodes)) {
3784
+ if (n.lastMutation != null) lastMutations.set(path, n.lastMutation);
3785
+ }
3786
+ return explainPath(described, from, to, {
3787
+ ...opts?.maxDepth != null ? { maxDepth: opts.maxDepth } : {},
3788
+ ...opts?.findCycle === true ? { findCycle: true } : {},
3789
+ annotations,
3790
+ lastMutations
3791
+ });
3792
+ }
3793
+ /**
3794
+ * @internal Collect all qualified paths in this graph tree matching a
3795
+ * glob pattern. Used by scoped autoCheckpoint subscription.
3796
+ */
3797
+ _pathsMatching(glob) {
3798
+ const re = globToRegex(glob);
3799
+ const targets = [];
3800
+ this._collectObserveTargets("", targets);
3801
+ return targets.map(([p]) => p).filter((p) => re.test(p));
3802
+ }
3803
+ _collectObserveTargets(prefix, out) {
3804
+ for (const m of [...this._mounts.keys()].sort()) {
3805
+ const p2 = prefix === "" ? m : `${prefix}${PATH_SEP}${m}`;
3806
+ this._mounts.get(m)._collectObserveTargets(p2, out);
3807
+ }
3808
+ for (const loc of [...this._nodes.keys()].sort()) {
3809
+ const n = this._nodes.get(loc);
3810
+ const p = prefix === "" ? loc : `${prefix}${PATH_SEP}${loc}`;
3811
+ out.push([p, n]);
3812
+ this._appendMetaObserveTargets(p, n, out);
3813
+ }
3814
+ }
3815
+ _appendMetaObserveTargets(basePath, n, out) {
3816
+ for (const mk of Object.keys(n.meta).sort()) {
3817
+ const m = n.meta[mk];
3818
+ const mp = `${basePath}${PATH_SEP}${GRAPH_META_SEGMENT}${PATH_SEP}${mk}`;
3819
+ out.push([mp, m]);
3820
+ this._appendMetaObserveTargets(mp, m, out);
3821
+ }
3822
+ }
3823
+ observe(pathOrOpts, options) {
3824
+ const isPath = typeof pathOrOpts === "string";
3825
+ const rawOpts = isPath ? options : pathOrOpts;
3826
+ const resolved = resolveObserveDetail(rawOpts);
3827
+ const wantsStructured = resolved.structured === true || resolved.timeline === true || resolved.causal === true || resolved.derived === true || resolved.detail === "minimal" || resolved.detail === "full" || resolved.format != null;
3828
+ const actor = resolved.actor;
3829
+ if (isPath) {
3830
+ const path = pathOrOpts;
3831
+ const target = this.resolve(path);
3832
+ if (actor != null && !target.allowsObserve(actor)) {
3833
+ throw new GuardDenied({ actor, action: "observe", nodeName: path });
3834
+ }
3835
+ if (wantsStructured) return this._buildStructuredObserver([[path, target]], resolved, "one");
3836
+ return {
3837
+ subscribe(sink) {
3838
+ return target.subscribe(sink);
3839
+ },
3840
+ up(messages) {
3841
+ try {
3842
+ target.up?.(messages);
3843
+ } catch (err) {
3844
+ if (err instanceof GuardDenied) return;
3845
+ throw err;
3846
+ }
3847
+ }
3848
+ };
3849
+ }
3850
+ const collected = [];
3851
+ this._collectObserveTargets("", collected);
3852
+ collected.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
3853
+ const picked = actor == null ? collected : collected.filter(([, nd]) => nd.allowsObserve(actor));
3854
+ if (wantsStructured) return this._buildStructuredObserver(picked, resolved, "all");
3855
+ return {
3856
+ subscribe: (sink) => {
3857
+ const unsubs = picked.map(
3858
+ ([p, nd]) => nd.subscribe((msgs) => {
3859
+ sink(p, msgs);
3860
+ })
3861
+ );
3862
+ return () => {
3863
+ for (const u of unsubs) u();
3864
+ };
3865
+ },
3866
+ up: (upPath, messages) => {
3867
+ try {
3868
+ const nd = this.resolve(upPath);
3869
+ nd.up?.(messages);
3870
+ } catch (err) {
3871
+ if (err instanceof GuardDenied) return;
3872
+ throw err;
3873
+ }
3874
+ }
3875
+ };
3876
+ }
3877
+ /** Dispatch helper — builds a unified observer + its expand closure. */
3878
+ _buildStructuredObserver(targets, options, mode) {
3879
+ const firstPath = mode === "one" ? targets[0]?.[0] : void 0;
3880
+ const expand = (merged) => {
3881
+ if (mode === "one" && firstPath != null) {
3882
+ const target = this.resolve(firstPath);
3883
+ return this._buildStructuredObserver([[firstPath, target]], merged, "one");
3884
+ }
3885
+ const collected = [];
3886
+ this._collectObserveTargets("", collected);
3887
+ collected.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
3888
+ const actor = merged.actor;
3889
+ const picked = actor == null ? collected : collected.filter(([, nd]) => nd.allowsObserve(actor));
3890
+ return this._buildStructuredObserver(picked, merged, "all");
3891
+ };
3892
+ return this._createObserveResult(targets, options, expand);
3893
+ }
3894
+ /**
3895
+ * Unified observer builder — replaces the four ex-creators
3896
+ * (`_createObserveResult` / `...ForAll` / `_createFallback…`). Accepts a
3897
+ * list of `[path, node]` targets (single-element for one-node observe,
3898
+ * N-element for all-nodes). Inspector hooks attach per-target when
3899
+ * `causal`/`derived` requested AND `config.inspectorEnabled`; otherwise
3900
+ * those fields gracefully drop.
3901
+ *
3902
+ * Events flow through a `recordEvent()` helper so the format logger,
3903
+ * ring-buffer, and async-iterable hooks all share one push path.
3904
+ */
3905
+ _createObserveResult(targets, options, expand) {
3906
+ const timeline = options.timeline === true;
3907
+ const causal = options.causal === true;
3908
+ const derived = options.derived === true;
3909
+ const minimal = options.detail === "minimal";
3910
+ const inspectorOn = this.config.inspectorEnabled;
3911
+ const wantInspector = (causal || derived) && inspectorOn;
3912
+ const maxEvents = options.maxEvents;
3913
+ const ring = maxEvents != null && maxEvents > 0 ? new RingBuffer(maxEvents) : null;
3914
+ const events = [];
3915
+ const listeners = /* @__PURE__ */ new Set();
3916
+ const values = {};
3917
+ const nodeErrored = /* @__PURE__ */ new Set();
3918
+ let dirtyCount = 0;
3919
+ let resolvedCount = 0;
3920
+ let invalidateCount = 0;
3921
+ let pauseCount = 0;
3922
+ let resumeCount = 0;
3923
+ let teardownCount = 0;
3924
+ let anyCompletedCleanly = false;
3925
+ let anyErrored = false;
3926
+ let batchSeq = 0;
3927
+ const lastTriggerDepIndex = /* @__PURE__ */ new Map();
3928
+ const lastRunDepValues = /* @__PURE__ */ new Map();
3929
+ const lastRunDepBatches = /* @__PURE__ */ new Map();
3930
+ const recordEvent = (event) => {
3931
+ if (ring) ring.push(event);
3932
+ else events.push(event);
3933
+ for (const listener of listeners) listener(event);
3934
+ };
3935
+ const baseMeta = () => timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
3936
+ const attachInspector = (target, path) => {
3937
+ if (!wantInspector || !(target instanceof NodeImpl)) return void 0;
3938
+ return target._setInspectorHook((ev) => {
3939
+ if (ev.kind === "dep_message") {
3940
+ lastTriggerDepIndex.set(target, ev.depIndex);
3941
+ } else if (ev.kind === "run") {
3942
+ const effective = ev.batchData.map(
3943
+ (b, i) => b != null && b.length > 0 ? b.at(-1) : ev.prevData[i]
3944
+ );
3945
+ lastRunDepValues.set(target, effective);
3946
+ const batches = ev.batchData.map(
3947
+ (b) => b != null ? [...b] : void 0
3948
+ );
3949
+ lastRunDepBatches.set(target, batches);
3950
+ if (derived) {
3951
+ recordEvent({
3952
+ type: "derived",
3953
+ path,
3954
+ dep_values: effective,
3955
+ dep_batches: batches,
3956
+ ...baseMeta()
3957
+ });
3958
+ }
3959
+ }
3960
+ });
3961
+ };
3962
+ const buildCausal = (target) => {
3963
+ const idx = lastTriggerDepIndex.get(target);
3964
+ const depValues = lastRunDepValues.get(target);
3965
+ if (!causal || depValues == null) return {};
3966
+ const triggerDep = idx != null && idx >= 0 && target instanceof NodeImpl ? target._deps[idx] : void 0;
3967
+ const triggerNode = triggerDep?.node;
3968
+ const tv = triggerNode?.v;
3969
+ const depBatches = lastRunDepBatches.get(target);
3970
+ return {
3971
+ trigger_dep_index: idx,
3972
+ trigger_dep_name: triggerNode?.name,
3973
+ ...tv != null ? { trigger_version: { id: tv.id, version: tv.version } } : {},
3974
+ dep_values: [...depValues],
3975
+ ...depBatches != null ? { dep_batches: depBatches } : {}
3976
+ };
3977
+ };
3978
+ const inspectorDetaches = [];
3979
+ const unsubs = [];
3980
+ for (const [path, target] of targets) {
3981
+ const detach = attachInspector(target, path);
3982
+ if (detach) inspectorDetaches.push(detach);
3983
+ unsubs.push(
3984
+ target.subscribe((msgs) => {
3985
+ batchSeq++;
3986
+ for (const m of msgs) {
3987
+ const t = m[0];
3988
+ const base = baseMeta();
3989
+ if (t === DATA) {
3990
+ values[path] = m[1];
3991
+ recordEvent({
3992
+ type: "data",
3993
+ path,
3994
+ data: m[1],
3995
+ ...base,
3996
+ ...buildCausal(target)
3997
+ });
3998
+ } else if (minimal) {
3999
+ if (t === DIRTY) dirtyCount++;
4000
+ else if (t === RESOLVED) resolvedCount++;
4001
+ else if (t === INVALIDATE) invalidateCount++;
4002
+ else if (t === PAUSE) pauseCount++;
4003
+ else if (t === RESUME) resumeCount++;
4004
+ else if (t === TEARDOWN) teardownCount++;
4005
+ else if (t === COMPLETE && !nodeErrored.has(path)) anyCompletedCleanly = true;
4006
+ else if (t === ERROR) {
4007
+ anyErrored = true;
4008
+ nodeErrored.add(path);
4009
+ }
4010
+ } else if (t === DIRTY) {
4011
+ dirtyCount++;
4012
+ recordEvent({ type: "dirty", path, ...base });
4013
+ } else if (t === RESOLVED) {
4014
+ resolvedCount++;
4015
+ recordEvent({
4016
+ type: "resolved",
4017
+ path,
4018
+ ...base,
4019
+ ...buildCausal(target)
4020
+ });
4021
+ } else if (t === INVALIDATE) {
4022
+ invalidateCount++;
4023
+ recordEvent({ type: "invalidate", path, ...base });
4024
+ } else if (t === PAUSE) {
4025
+ pauseCount++;
4026
+ recordEvent({ type: "pause", path, lockId: m[1], ...base });
4027
+ } else if (t === RESUME) {
4028
+ resumeCount++;
4029
+ recordEvent({ type: "resume", path, lockId: m[1], ...base });
4030
+ } else if (t === COMPLETE) {
4031
+ if (!nodeErrored.has(path)) anyCompletedCleanly = true;
4032
+ recordEvent({ type: "complete", path, ...base });
4033
+ } else if (t === ERROR) {
4034
+ anyErrored = true;
4035
+ nodeErrored.add(path);
4036
+ recordEvent({
4037
+ type: "error",
4038
+ path,
4039
+ data: m[1],
4040
+ ...base
4041
+ });
4042
+ } else if (t === TEARDOWN) {
4043
+ teardownCount++;
4044
+ recordEvent({ type: "teardown", path, ...base });
4045
+ }
4046
+ }
4047
+ })
4048
+ );
4049
+ }
4050
+ let disposed = false;
4051
+ const dispose = () => {
4052
+ if (disposed) return;
4053
+ disposed = true;
4054
+ for (const u of unsubs) u();
4055
+ for (const d of inspectorDetaches) d();
4056
+ for (const resolve of asyncResolvers) resolve({ value: void 0, done: true });
4057
+ asyncResolvers.length = 0;
4058
+ };
4059
+ const asyncQueue = [];
4060
+ const asyncResolvers = [];
4061
+ listeners.add((ev) => {
4062
+ const resolve = asyncResolvers.shift();
4063
+ if (resolve) resolve({ value: ev, done: false });
4064
+ else asyncQueue.push(ev);
4065
+ });
4066
+ const result = {
4067
+ get values() {
4068
+ return values;
4069
+ },
4070
+ get dirtyCount() {
4071
+ return dirtyCount;
4072
+ },
4073
+ get resolvedCount() {
4074
+ return resolvedCount;
4075
+ },
4076
+ get invalidateCount() {
4077
+ return invalidateCount;
4078
+ },
4079
+ get pauseCount() {
4080
+ return pauseCount;
4081
+ },
4082
+ get resumeCount() {
4083
+ return resumeCount;
4084
+ },
4085
+ get teardownCount() {
4086
+ return teardownCount;
4087
+ },
4088
+ get events() {
4089
+ return ring ? ring.toArray() : [...events];
4090
+ },
4091
+ get anyCompletedCleanly() {
4092
+ return anyCompletedCleanly;
4093
+ },
4094
+ get anyErrored() {
4095
+ return anyErrored;
4096
+ },
4097
+ get completedWithoutErrors() {
4098
+ return anyCompletedCleanly && !anyErrored;
4099
+ },
4100
+ onEvent(listener) {
4101
+ listeners.add(listener);
4102
+ return () => listeners.delete(listener);
4103
+ },
4104
+ dispose,
4105
+ expand(extra) {
4106
+ dispose();
4107
+ const merged = { ...options };
4108
+ if (typeof extra === "string") {
4109
+ merged.detail = extra;
4110
+ } else {
4111
+ Object.assign(merged, extra);
4112
+ }
4113
+ return expand(resolveObserveDetail(merged));
4114
+ },
4115
+ [Symbol.asyncIterator]() {
4116
+ return {
4117
+ next() {
4118
+ if (asyncQueue.length > 0) {
4119
+ return Promise.resolve({ value: asyncQueue.shift(), done: false });
4120
+ }
4121
+ if (disposed) return Promise.resolve({ value: void 0, done: true });
4122
+ return new Promise((resolve) => asyncResolvers.push(resolve));
4123
+ },
4124
+ return() {
4125
+ dispose();
4126
+ return Promise.resolve({ value: void 0, done: true });
4127
+ }
4128
+ };
4129
+ }
4130
+ };
4131
+ if (options.format != null) this._attachFormatLogger(result, options);
4132
+ return result;
4133
+ }
4134
+ /**
4135
+ * Attach format-rendering logger to an ObserveResult by subscribing to its
4136
+ * event stream (no monkey-patching). Renders each event per `format` and
4137
+ * `theme`, filtered by `includeTypes` / `excludeTypes`.
4138
+ */
4139
+ _attachFormatLogger(result, options) {
4140
+ const format = options.format;
4141
+ if (format == null) return;
4142
+ const logger = options.logger ?? ((line) => console.log(line));
4143
+ const include = options.includeTypes ? new Set(options.includeTypes) : null;
4144
+ const exclude = options.excludeTypes ? new Set(options.excludeTypes) : null;
4145
+ const shouldLog = include == null && exclude == null ? () => true : (type) => (include == null || include.has(type)) && (exclude == null || !exclude.has(type));
4146
+ const theme = resolveObserveTheme(options.theme);
4147
+ const renderEvent = (event) => {
4148
+ if (format === "json") {
4149
+ try {
4150
+ return JSON.stringify(event);
4151
+ } catch {
4152
+ return JSON.stringify({
4153
+ type: event.type,
4154
+ path: event.path,
4155
+ data: "[unserializable]"
4156
+ });
4157
+ }
4158
+ }
4159
+ const color = theme[event.type] ?? "";
4160
+ const pathPart = event.path ? `${theme.path}${event.path}${theme.reset} ` : "";
4161
+ const isDataBearing = event.type === "data" || event.type === "error";
4162
+ const isLockBearing = event.type === "pause" || event.type === "resume";
4163
+ const dataPart = isDataBearing ? ` ${describeData(event.data)}` : isLockBearing ? ` ${describeData(event.lockId)}` : "";
4164
+ const causal = event.type === "data" || event.type === "resolved" || event.type === "derived" ? event : void 0;
4165
+ const triggerPart = causal?.trigger_dep_name != null ? ` <- ${causal.trigger_dep_name}` : causal?.trigger_dep_index != null ? ` <- #${causal.trigger_dep_index}` : "";
4166
+ const batchPart = event.in_batch ? " [batch]" : "";
4167
+ return `${pathPart}${color}${event.type.toUpperCase()}${theme.reset}${dataPart}${triggerPart}${batchPart}`;
4168
+ };
4169
+ result.onEvent((event) => {
4170
+ if (shouldLog(event.type)) logger(renderEvent(event), event);
4171
+ });
4172
+ }
4173
+ // `dumpGraph` is folded into `describe({format: "pretty" | "json"})` (Unit 12).
4174
+ // `toMermaid` / `toD2` are folded into `describe({format: "mermaid" | "d2"})` (Unit 20).
4175
+ // ——————————————————————————————————————————————————————————————
4176
+ // Lifecycle & persistence (§3.7–§3.8)
4177
+ // ——————————————————————————————————————————————————————————————
4178
+ /**
4179
+ * Register a cleanup function to be called on {@link Graph.destroy}.
4180
+ *
4181
+ * Factories use this to attach teardown logic for internal nodes, keepalive
4182
+ * subscriptions, or other resources that are not registered on the graph and
4183
+ * would otherwise leak on repeated create/destroy cycles.
4184
+ *
4185
+ * Returns a removal function — call it to unregister the disposer early.
4186
+ */
4187
+ addDisposer(fn) {
4188
+ this._disposers.add(fn);
4189
+ return () => {
4190
+ this._disposers.delete(fn);
4191
+ };
4192
+ }
4193
+ /**
4194
+ * Drains disposers (registered via {@link addDisposer}), then sends `[[TEARDOWN]]` to all
4195
+ * nodes and clears registries on this graph and every mounted subgraph (§3.7).
4196
+ * The instance is left empty and may be reused with {@link Graph.add}.
4197
+ */
4198
+ destroy() {
4199
+ drainDisposers(this._disposers, this.name);
4200
+ this.signal([[TEARDOWN]], { internal: true });
4201
+ drainDisposers(this._storageDisposers, this.name);
4202
+ for (const child of [...this._mounts.values()]) {
4203
+ child._parent = void 0;
4204
+ child._destroyClearOnly();
4205
+ }
4206
+ this._mounts.clear();
4207
+ this._nodes.clear();
4208
+ this._parent = void 0;
4209
+ }
4210
+ /** Clear structure after parent already signaled TEARDOWN through this subtree. */
4211
+ _destroyClearOnly() {
4212
+ for (const child of [...this._mounts.values()]) {
4213
+ child._parent = void 0;
4214
+ child._destroyClearOnly();
4215
+ }
4216
+ this._mounts.clear();
4217
+ this._nodes.clear();
4218
+ this._parent = void 0;
4219
+ }
4220
+ snapshot(opts) {
4221
+ const { expand: _, ...d } = this.describe({ detail: "full" });
4222
+ const sortedNodes = {};
4223
+ for (const key of Object.keys(d.nodes).sort()) {
4224
+ const { lastMutation: _lm, guard: _g, ...node2 } = d.nodes[key];
4225
+ sortedNodes[key] = node2;
4226
+ }
4227
+ const sortedSubgraphs = [...d.subgraphs].sort();
4228
+ const snap = {
4229
+ ...d,
4230
+ version: 1,
4231
+ nodes: sortedNodes,
4232
+ subgraphs: sortedSubgraphs
4233
+ };
4234
+ if (opts?.format == null) return snap;
4235
+ if (opts.format === "json-string") return JSON.stringify(snap);
4236
+ if (opts.format === "bytes") {
4237
+ if (opts.codec == null) {
4238
+ throw new Error("snapshot({format: 'bytes'}) requires a `codec` name");
4239
+ }
4240
+ const codec = this.config.lookupCodec(opts.codec);
4241
+ if (codec == null) {
4242
+ throw new Error(
4243
+ `snapshot: codec "${opts.codec}" is not registered on this graph's config. Call config.registerCodec(...) before creating nodes.`
4244
+ );
4245
+ }
4246
+ return encodeEnvelope(codec, codec.encode(snap));
4247
+ }
4248
+ throw new Error(`snapshot: unknown format "${String(opts.format)}"`);
4249
+ }
4250
+ /**
4251
+ * Auto-dispatch a byte buffer produced by {@link Graph.snapshot} with
4252
+ * `{format: "bytes", codec: name}`. Reads the v1 envelope, resolves the
4253
+ * named codec on `config` (defaults to `defaultConfig`), and returns the
4254
+ * decoded snapshot. Combine with {@link Graph.fromSnapshot} to rehydrate
4255
+ * a full graph topology from bytes.
4256
+ *
4257
+ * @throws If the envelope is malformed or the named codec isn't
4258
+ * registered on the target config.
4259
+ */
4260
+ static decode(bytes, opts) {
4261
+ const cfg = opts?.config ?? defaultConfig;
4262
+ const { codec, codecVersion, payload } = decodeEnvelope(bytes, cfg);
4263
+ return codec.decode(payload, codecVersion);
4264
+ }
4265
+ /**
4266
+ * Apply persisted values onto an existing graph whose topology matches the snapshot
4267
+ * (§3.8). Only {@link DescribeNodeOutput.type} `state` entries with a `value` field
4268
+ * are written by default; `derived` / `operator` / `effect` are always skipped so
4269
+ * deps drive recomputation. `producer` entries are skipped unless `includeProducers`
4270
+ * is set (producers recompute on activation, so restoring is usually a no-op
4271
+ * overwritten on the next wave — opt in for audit / forensic round-trip use cases).
4272
+ * Unknown paths are ignored.
4273
+ *
4274
+ * @param data - Snapshot envelope with matching `name` and node slices.
4275
+ * @throws If `data.name` does not equal {@link Graph.name}.
4276
+ */
4277
+ restore(data, options) {
4278
+ parseSnapshotEnvelope(data);
4279
+ if (data.name !== this.name) {
4280
+ throw new Error(
4281
+ `Graph "${this.name}": restore snapshot name "${data.name}" does not match this graph`
4282
+ );
4283
+ }
4284
+ const onlyPatterns = options?.only == null ? null : (Array.isArray(options.only) ? options.only : [options.only]).map((p) => globToRegex(p));
4285
+ const includeProducers = options?.includeProducers === true;
4286
+ for (const path of Object.keys(data.nodes).sort()) {
4287
+ if (onlyPatterns !== null && !onlyPatterns.some((re) => re.test(path))) continue;
4288
+ const slice = data.nodes[path];
4289
+ if (slice === void 0) continue;
4290
+ if (!("value" in slice) || slice.value === void 0) {
4291
+ if ("value" in slice && slice.value === void 0) {
4292
+ options?.onError?.(
4293
+ path,
4294
+ new Error(
4295
+ `restore: slice.value is undefined for "${path}" (undefined is the global SENTINEL; not valid DATA)`
4296
+ )
4297
+ );
4298
+ }
4299
+ continue;
4300
+ }
4301
+ if (slice.type === "derived" || slice.type === "effect") {
4302
+ continue;
4303
+ }
4304
+ if (slice.type === "producer" && !includeProducers) {
4305
+ continue;
4306
+ }
4307
+ if (slice.v != null) {
4308
+ const live = this.tryResolve(path);
4309
+ const lv = live?.v;
4310
+ if (lv != null && lv.id === slice.v.id && lv.version === slice.v.version) {
4311
+ continue;
4312
+ }
4313
+ }
4314
+ try {
4315
+ this.set(path, slice.value);
4316
+ } catch (err) {
4317
+ options?.onError?.(path, err);
4318
+ }
4319
+ }
4320
+ }
4321
+ /**
4322
+ * Creates a graph named from the snapshot, optionally runs `build` to register nodes
4323
+ * and mounts, then {@link Graph.restore} values (§3.8).
4324
+ *
4325
+ * @param data - Snapshot envelope (`version` checked).
4326
+ * @param opts - Either a legacy `build(g)` callback, or an options object:
4327
+ * - `build?` — topology constructor; skips auto-hydration when present.
4328
+ * - `factories?` — map from glob pattern to {@link GraphNodeFactory},
4329
+ * used by auto-hydration to reconstruct non-state nodes. Per-call (no
4330
+ * process-global registry). First matching pattern wins.
4331
+ * @returns Hydrated `Graph` instance.
4332
+ */
4333
+ static fromSnapshot(data, opts) {
4334
+ parseSnapshotEnvelope(data);
4335
+ const build = typeof opts === "function" ? opts : opts?.build;
4336
+ const factoryMap = typeof opts === "function" ? void 0 : opts?.factories;
4337
+ const g = new _Graph(data.name);
4338
+ if (build) {
4339
+ build(g);
4340
+ g.restore(data);
4341
+ return g;
4342
+ }
4343
+ for (const mount of [...data.subgraphs].sort((a, b) => {
4344
+ const da = a.split(PATH_SEP).length;
4345
+ const db = b.split(PATH_SEP).length;
4346
+ if (da !== db) return da - db;
4347
+ if (a < b) return -1;
4348
+ if (a > b) return 1;
4349
+ return 0;
4350
+ })) {
4351
+ const parts = mount.split(PATH_SEP);
4352
+ let target = g;
4353
+ for (const seg of parts) {
4354
+ if (!target._mounts.has(seg)) {
4355
+ target.mount(seg, new _Graph(seg));
4356
+ }
4357
+ target = target._mounts.get(seg);
4358
+ }
4359
+ }
4360
+ const factories = factoryMap ? Object.entries(factoryMap).map(([pattern, factory]) => ({
4361
+ re: globToRegex(pattern),
4362
+ factory
4363
+ })) : [];
4364
+ const factoryForPath = (path) => {
4365
+ for (const entry of factories) {
4366
+ if (entry.re.test(path)) return entry.factory;
4367
+ }
4368
+ return void 0;
4369
+ };
4370
+ const ownerForPath = (path) => {
4371
+ const segments = path.split(PATH_SEP);
4372
+ const local = segments.pop();
4373
+ if (local == null || local.length === 0) {
4374
+ throw new Error(`invalid snapshot path "${path}"`);
4375
+ }
4376
+ let owner = g;
4377
+ for (const seg of segments) {
4378
+ const next = owner._mounts.get(seg);
4379
+ if (!next) throw new Error(`unknown mount "${seg}" in path "${path}"`);
4380
+ owner = next;
4381
+ }
4382
+ return [owner, local];
4383
+ };
4384
+ const primaryEntries = Object.entries(data.nodes).filter(([path]) => !path.includes(`${PATH_SEP}${GRAPH_META_SEGMENT}${PATH_SEP}`)).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
4385
+ const pending = new Map(primaryEntries);
4386
+ const created = /* @__PURE__ */ new Map();
4387
+ let progressed = true;
4388
+ while (pending.size > 0 && progressed) {
4389
+ progressed = false;
4390
+ for (const [path, slice] of [...pending.entries()]) {
4391
+ const deps = slice?.deps ?? [];
4392
+ if (!deps.every((dep) => created.has(dep))) continue;
4393
+ const [owner, localName] = ownerForPath(path);
4394
+ const meta = { ...slice?.meta ?? {} };
4395
+ const factory = factoryForPath(path);
4396
+ let node2;
4397
+ if (slice?.type === "state") {
4398
+ node2 = state(slice.value, { meta });
4399
+ } else {
4400
+ if (factory == null) continue;
4401
+ node2 = factory(localName, {
4402
+ path,
4403
+ type: slice.type,
4404
+ value: slice.value,
4405
+ meta,
4406
+ deps,
4407
+ resolvedDeps: deps.map((dep) => created.get(dep))
4408
+ });
4409
+ }
4410
+ owner.add(localName, node2);
4411
+ created.set(path, node2);
4412
+ pending.delete(path);
4413
+ progressed = true;
4414
+ }
4415
+ }
4416
+ if (pending.size > 0) {
4417
+ const unresolved = [...pending.keys()].sort().join(", ");
4418
+ throw new Error(
4419
+ `Graph.fromSnapshot could not reconstruct nodes without build callback: ${unresolved}. Pass matching factories via fromSnapshot(data, { factories: { pattern: factoryFn } }).`
4420
+ );
4421
+ }
4422
+ g.restore(data);
4423
+ return g;
4424
+ }
4425
+ /**
4426
+ * ECMAScript `JSON.stringify` hook — returns the same object as
4427
+ * {@link Graph.snapshot}. Makes `JSON.stringify(graph)` "just work"
4428
+ * without double-encoding.
4429
+ */
4430
+ toJSON() {
4431
+ return this.snapshot();
4432
+ }
4433
+ /**
4434
+ * Unified persistence surface (§3.8). Cascades snapshot records through
4435
+ * one or more {@link StorageTier}s, each with its own `debounceMs` /
4436
+ * `compactEvery` cadence and independent diff baseline.
4437
+ *
4438
+ * Subscription gates on {@link messageTier} ≥ 3 (DATA/RESOLVED/terminal),
4439
+ * never on tier-0/1/2 control waves (START/DIRTY/INVALIDATE/PAUSE/RESUME)
4440
+ * or tier-5 TEARDOWN (graceful shutdown is the caller's responsibility).
4441
+ *
4442
+ * Per-tier cadence lets the hot tier stay sync while cold tiers absorb
4443
+ * async writes without blocking the hot path. Each tier holds its own
4444
+ * `{lastSnapshot, lastVersionFingerprint}` so cold-tier diff baselines
4445
+ * aren't polluted by hot-tier flushes. Tiers with `debounceMs === 0`
4446
+ * share a single snapshot computation per observe event; debounced tiers
4447
+ * compute their own snapshot when their timer fires.
4448
+ */
4449
+ attachStorage(tiers, options = {}) {
4450
+ const states = tiers.map((tier) => ({
4451
+ tier,
4452
+ debounceMs: Math.max(0, tier.debounceMs ?? 0),
4453
+ compactEvery: Math.max(1, tier.compactEvery ?? 10),
4454
+ timer: void 0,
4455
+ seq: 0,
4456
+ lastSnapshot: void 0,
4457
+ lastFingerprint: "",
4458
+ disposed: false,
4459
+ savePending: void 0
4460
+ }));
4461
+ if (options.autoRestore === true) {
4462
+ void this._cascadeRestore(tiers, options.onError);
4463
+ }
4464
+ const runFlush = (s, snapshot) => {
4465
+ if (s.disposed) return;
4466
+ const fingerprint = computeVersionFingerprint(snapshot.nodes);
4467
+ if (s.lastSnapshot != null && fingerprint !== "" && fingerprint === s.lastFingerprint) {
4468
+ return;
4469
+ }
4470
+ const nextSeq = s.seq + 1;
4471
+ const timestamp_ns = wallClockNs();
4472
+ const isFirst = s.lastSnapshot == null;
4473
+ const shouldCompact = isFirst || nextSeq % s.compactEvery === 0;
4474
+ const record = shouldCompact ? {
4475
+ mode: "full",
4476
+ snapshot,
4477
+ seq: nextSeq,
4478
+ timestamp_ns,
4479
+ format_version: SNAPSHOT_VERSION
4480
+ } : {
4481
+ mode: "diff",
4482
+ diff: diffForWAL(s.lastSnapshot, snapshot),
4483
+ seq: nextSeq,
4484
+ timestamp_ns,
4485
+ format_version: SNAPSHOT_VERSION
4486
+ };
4487
+ if (s.tier.filter && !s.tier.filter(this.name, record)) {
4488
+ return;
4489
+ }
4490
+ let result;
4491
+ try {
4492
+ result = s.tier.save(this.name, record);
4493
+ } catch (error) {
4494
+ options.onError?.(error, s.tier);
4495
+ return;
4496
+ }
4497
+ if (result && typeof result.then === "function") {
4498
+ const prev = s.savePending ?? Promise.resolve();
4499
+ const chained = prev.then(
4500
+ () => result,
4501
+ // Previous rejection already surfaced; don't block this save.
4502
+ () => result
4503
+ );
4504
+ const final = chained.then(
4505
+ () => {
4506
+ if (s.disposed) return;
4507
+ s.seq = nextSeq;
4508
+ s.lastSnapshot = snapshot;
4509
+ s.lastFingerprint = fingerprint;
4510
+ },
4511
+ (err) => {
4512
+ options.onError?.(err, s.tier);
4513
+ }
4514
+ );
4515
+ s.savePending = final.finally(() => {
4516
+ if (s.savePending === final) s.savePending = void 0;
4517
+ });
4518
+ } else {
4519
+ s.seq = nextSeq;
4520
+ s.lastSnapshot = snapshot;
4521
+ s.lastFingerprint = fingerprint;
4522
+ }
4523
+ };
4524
+ const flushTier = (s, snapshot) => {
4525
+ try {
4526
+ runFlush(s, snapshot);
4527
+ } catch (error) {
4528
+ options.onError?.(error, s.tier);
4529
+ }
4530
+ };
4531
+ const onEvent = (path, messages) => {
4532
+ const triggeredByTier = messages.some((m) => {
4533
+ const tier = this.config.messageTier(m[0]);
4534
+ return tier >= 3 && tier < 5;
4535
+ });
4536
+ if (!triggeredByTier) return;
4537
+ if (options.filter) {
4538
+ const nd = this.tryResolve(path);
4539
+ if (nd == null) return;
4540
+ const described = describeNode(nd, resolveDescribeFields("standard"));
4541
+ if (!options.filter(path, described)) return;
4542
+ }
4543
+ let sharedSnapshot;
4544
+ const getSnapshot = () => {
4545
+ if (sharedSnapshot == null) sharedSnapshot = this.snapshot();
4546
+ return sharedSnapshot;
4547
+ };
4548
+ for (const s of states) {
4549
+ if (s.disposed) continue;
4550
+ if (s.debounceMs === 0) {
4551
+ flushTier(s, getSnapshot());
4552
+ } else {
4553
+ if (s.timer == null) s.timer = new ResettableTimer();
4554
+ s.timer.start(s.debounceMs, () => {
4555
+ if (s.disposed) return;
4556
+ flushTier(s, this.snapshot());
4557
+ });
4558
+ }
4559
+ }
4560
+ };
4561
+ let off;
4562
+ if (options.paths != null) {
4563
+ const paths = typeof options.paths === "string" ? this._pathsMatching(options.paths) : options.paths;
4564
+ const unsubs = paths.map((p) => {
4565
+ const nd = this.tryResolve(p);
4566
+ if (nd == null) return () => {
4567
+ };
4568
+ return nd.subscribe((msgs) => onEvent(p, msgs));
4569
+ });
4570
+ off = () => {
4571
+ for (const u of unsubs) u();
4572
+ };
4573
+ } else {
4574
+ off = this.observe().subscribe((path, messages) => onEvent(path, messages));
4575
+ }
4576
+ const dispose = () => {
4577
+ off();
4578
+ for (const s of states) {
4579
+ s.disposed = true;
4580
+ s.timer?.cancel();
4581
+ }
4582
+ this._storageDisposers.delete(dispose);
4583
+ };
4584
+ this._storageDisposers.add(dispose);
4585
+ return { dispose };
4586
+ }
4587
+ /**
4588
+ * Try tiers in order (hottest first); apply the first record that hits
4589
+ * via {@link Graph.restore}. Returns `true` if any tier produced a
4590
+ * restorable snapshot, `false` if all missed.
4591
+ *
4592
+ * Resilience: a tier that returns data which cannot be restored (load
4593
+ * throws, shape unrecognized, or `restore()` itself throws) does not abort
4594
+ * the cascade — the error is routed through `onError` (if supplied) and
4595
+ * the next colder tier is tried. This mirrors how a multi-tier cache
4596
+ * falls through on a corrupt hot entry.
4597
+ *
4598
+ * Note: `restore()` mutates state incrementally. If a restore throws
4599
+ * partway through, the graph may hold a mixed state (some slices from
4600
+ * the bad tier, some pre-existing). A subsequent successful tier's
4601
+ * `restore()` overwrites the overlapping slices.
4602
+ *
4603
+ * Internal helper shared by {@link Graph.attachStorage}'s `autoRestore`
4604
+ * option and the static {@link Graph.fromStorage} factory.
4605
+ */
4606
+ async _cascadeRestore(tiers, onError) {
4607
+ for (const tier of tiers) {
4608
+ let raw;
4609
+ try {
4610
+ raw = await tier.load(this.name);
4611
+ } catch (err) {
4612
+ onError?.(err, tier);
4613
+ continue;
4614
+ }
4615
+ if (raw == null) continue;
4616
+ if (typeof raw !== "object" || Array.isArray(raw)) continue;
4617
+ const record = raw;
4618
+ try {
4619
+ if (record.mode === "full" && record.snapshot != null) {
4620
+ this.restore(record.snapshot);
4621
+ return true;
4622
+ }
4623
+ if (record.version === SNAPSHOT_VERSION && record.nodes != null) {
4624
+ this.restore(record);
4625
+ return true;
4626
+ }
4627
+ } catch (err) {
4628
+ onError?.(err, tier);
4629
+ }
4630
+ }
4631
+ return false;
4632
+ }
4633
+ /**
4634
+ * Construct a fresh {@link Graph} pre-hydrated from the first tier that
4635
+ * hits. Delegates topology reconstruction to {@link Graph.fromSnapshot}
4636
+ * on `"full"` records and direct {@link Graph.restore} on bare snapshots.
4637
+ *
4638
+ * Always asynchronous — awaits `tier.load()` for async tier support even
4639
+ * when all tiers are sync. Callers that know they only pass sync tiers
4640
+ * can safely `await` immediately.
4641
+ *
4642
+ * @throws If no tier holds a restorable record matching `name` *and* no
4643
+ * `factories` override is provided for dynamic nodes.
4644
+ */
4645
+ static async fromStorage(name, tiers, opts) {
4646
+ for (const tier of tiers) {
4647
+ let raw;
4648
+ try {
4649
+ raw = await tier.load(name);
4650
+ } catch (err) {
4651
+ opts?.onError?.(err, tier);
4652
+ continue;
4653
+ }
4654
+ if (raw == null) continue;
4655
+ if (typeof raw !== "object" || Array.isArray(raw)) continue;
4656
+ const record = raw;
4657
+ const snapshot = record.mode === "full" && record.snapshot != null ? record.snapshot : record.version === SNAPSHOT_VERSION && record.nodes != null ? record : void 0;
4658
+ if (snapshot == null) continue;
4659
+ try {
4660
+ return _Graph.fromSnapshot(snapshot, opts);
4661
+ } catch (err) {
4662
+ opts?.onError?.(err, tier);
4663
+ }
4664
+ }
4665
+ throw new Error(
4666
+ `Graph.fromStorage: no tier held a restorable record for "${name}" across ${tiers.length} tier(s)`
4667
+ );
4668
+ }
4669
+ // ——————————————————————————————————————————————————————————————
4670
+ // Inspector (roadmap 3.3) — reasoning trace, overhead gating
4671
+ // ——————————————————————————————————————————————————————————————
4672
+ // Inspector gating lives on `this.config.inspectorEnabled` (see
4673
+ // `core/config.ts`). Default: `true` outside `NODE_ENV === "production"`.
4674
+ _annotations = /* @__PURE__ */ new Map();
4675
+ _traceRing;
4676
+ trace(path, reason, opts) {
4677
+ if (path != null && reason != null) {
4678
+ if (!this.config.inspectorEnabled) return;
4679
+ if (this.tryResolve(path) == null) return;
4680
+ this._annotations.set(path, reason);
4681
+ const entry = {
4682
+ path,
4683
+ reason,
4684
+ timestamp_ns: monotonicNs(),
4685
+ ...opts?.actor != null ? { actor: opts.actor } : {}
4686
+ };
4687
+ this._traceRing.push(entry);
4688
+ return;
4689
+ }
4690
+ if (!this.config.inspectorEnabled) return [];
4691
+ return this._traceRing.toArray();
4692
+ }
4693
+ /**
4694
+ * Latest reason annotation attached to `path` via {@link Graph.trace},
4695
+ * or `undefined` if none. `describe()` surfaces this via the `reason`
4696
+ * field on each node entry (when present).
4697
+ */
4698
+ annotation(path) {
4699
+ return this._annotations.get(path);
4700
+ }
4701
+ /**
4702
+ * Clear all reasoning-trace state (both the per-path annotations map and
4703
+ * the ring buffer). Useful for long-running processes that want periodic
4704
+ * resets, or tests that need a clean slate.
4705
+ */
4706
+ clearTrace() {
4707
+ this._annotations.clear();
4708
+ this._traceRing.clear();
4709
+ }
4710
+ /**
4711
+ * Remove trace entries matching `predicate`. Returns the number of
4712
+ * entries removed. Does not touch the per-path annotations map — call
4713
+ * {@link Graph.clearTrace} for a full reset.
4714
+ */
4715
+ pruneTrace(predicate) {
4716
+ const kept = this._traceRing.toArray().filter((e) => !predicate(e));
4717
+ const removed = this._traceRing.size - kept.length;
4718
+ this._traceRing.clear();
4719
+ for (const e of kept) this._traceRing.push(e);
4720
+ return removed;
4721
+ }
4722
+ /**
4723
+ * Computes structural + value diff between two {@link Graph.describe} snapshots.
4724
+ *
4725
+ * @param a - Earlier describe output.
4726
+ * @param b - Later describe output.
4727
+ * @returns Added/removed nodes, changed fields, and edge deltas.
4728
+ */
4729
+ static diff(a, b) {
4730
+ const aKeys = new Set(Object.keys(a.nodes));
4731
+ const bKeys = new Set(Object.keys(b.nodes));
4732
+ const nodesAdded = [...bKeys].filter((k) => !aKeys.has(k)).sort();
4733
+ const nodesRemoved = [...aKeys].filter((k) => !bKeys.has(k)).sort();
4734
+ const nodesChanged = [];
4735
+ const versionChanges = [];
4736
+ for (const key of aKeys) {
4737
+ if (!bKeys.has(key)) continue;
4738
+ const na = a.nodes[key];
4739
+ const nb = b.nodes[key];
4740
+ const av = na.v;
4741
+ const bv = nb.v;
4742
+ if (av != null && bv != null && av.id === bv.id && av.version !== bv.version) {
4743
+ versionChanges.push({
4744
+ path: key,
4745
+ id: av.id,
4746
+ from: av.version,
4747
+ to: bv.version
4748
+ });
4749
+ }
4750
+ const versionMatches = av != null && bv != null && av.id === bv.id && av.version === bv.version;
4751
+ for (const field of ["type", "status", "sentinel"]) {
4752
+ const va = na[field];
4753
+ const vb = nb[field];
4754
+ if (va !== vb) {
4755
+ nodesChanged.push({ path: key, field, from: va, to: vb });
4756
+ }
4757
+ }
4758
+ if (versionMatches) continue;
4759
+ for (const field of ["value", "meta"]) {
4760
+ const va = na[field];
4761
+ const vb = nb[field];
4762
+ if (!deepEqual(va, vb)) {
4763
+ nodesChanged.push({ path: key, field, from: va, to: vb });
4764
+ }
4765
+ }
4766
+ }
4767
+ const edgeKey = (e) => `${e.from} ${e.to}`;
4768
+ const aEdges = new Set(a.edges.map(edgeKey));
4769
+ const bEdges = new Set(b.edges.map(edgeKey));
4770
+ const edgesAdded = b.edges.filter((e) => !aEdges.has(edgeKey(e)));
4771
+ const edgesRemoved = a.edges.filter((e) => !bEdges.has(edgeKey(e)));
4772
+ const aSubgraphs = new Set(a.subgraphs);
4773
+ const bSubgraphs = new Set(b.subgraphs);
4774
+ const subgraphsAdded = [...bSubgraphs].filter((s) => !aSubgraphs.has(s)).sort();
4775
+ const subgraphsRemoved = [...aSubgraphs].filter((s) => !bSubgraphs.has(s)).sort();
4776
+ return {
4777
+ nodesAdded,
4778
+ nodesRemoved,
4779
+ nodesChanged,
4780
+ versionChanges,
4781
+ edgesAdded,
4782
+ edgesRemoved,
4783
+ subgraphsAdded,
4784
+ subgraphsRemoved
4785
+ };
4786
+ }
4787
+ };
4788
+ function diffForWAL(a, b) {
4789
+ const base = Graph.diff(a, b);
4790
+ const nodesAddedFull = {};
4791
+ for (const path of base.nodesAdded) {
4792
+ const slice = b.nodes[path];
4793
+ if (slice != null) nodesAddedFull[path] = slice;
4794
+ }
4795
+ return { ...base, nodesAddedFull };
4796
+ }
4797
+ function reachable(described, from, direction, options = {}) {
4798
+ const empty = { paths: [], depths: /* @__PURE__ */ new Map(), truncated: false };
4799
+ if (!from) return options.withDetail ? empty : [];
4800
+ if (!options.both && direction !== "upstream" && direction !== "downstream") {
4801
+ throw new Error(`reachable: direction must be "upstream" or "downstream"`);
4802
+ }
4803
+ const maxDepth = options.maxDepth;
4804
+ if (maxDepth != null && (!Number.isInteger(maxDepth) || maxDepth < 0)) {
4805
+ throw new Error(`reachable: maxDepth must be an integer >= 0`);
4806
+ }
4807
+ if (maxDepth === 0) return options.withDetail ? empty : [];
4808
+ const depsByPath = /* @__PURE__ */ new Map();
4809
+ const reverseDeps = /* @__PURE__ */ new Map();
4810
+ const incomingEdges = /* @__PURE__ */ new Map();
4811
+ const outgoingEdges = /* @__PURE__ */ new Map();
4812
+ const universe = /* @__PURE__ */ new Set();
4813
+ for (const [path, node2] of Object.entries(described.nodes)) {
4814
+ if (!path) continue;
4815
+ universe.add(path);
4816
+ const deps = node2.deps ?? [];
4817
+ depsByPath.set(path, deps);
4818
+ for (const dep of deps) {
4819
+ if (!dep) continue;
4820
+ universe.add(dep);
4821
+ if (!reverseDeps.has(dep)) reverseDeps.set(dep, /* @__PURE__ */ new Set());
4822
+ reverseDeps.get(dep).add(path);
4823
+ }
4824
+ }
4825
+ for (const edge of described.edges) {
4826
+ if (edge == null || typeof edge !== "object") continue;
4827
+ const from2 = typeof edge.from === "string" ? edge.from : "";
4828
+ const to = typeof edge.to === "string" ? edge.to : "";
4829
+ if (!from2 || !to) continue;
4830
+ universe.add(from2);
4831
+ universe.add(to);
4832
+ if (!outgoingEdges.has(from2)) outgoingEdges.set(from2, /* @__PURE__ */ new Set());
4833
+ outgoingEdges.get(from2).add(to);
4834
+ if (!incomingEdges.has(to)) incomingEdges.set(to, /* @__PURE__ */ new Set());
4835
+ incomingEdges.get(to).add(from2);
4836
+ }
4837
+ if (!universe.has(from)) return options.withDetail ? empty : [];
4838
+ const doBoth = options.both === true;
4839
+ const visit = (path) => {
4840
+ if (doBoth) {
4841
+ const up = depsByPath.get(path) ?? [];
4842
+ const upEdges = incomingEdges.get(path);
4843
+ const down2 = reverseDeps.get(path);
4844
+ const downEdges2 = outgoingEdges.get(path);
4845
+ const acc2 = [...up];
4846
+ if (upEdges) acc2.push(...upEdges);
4847
+ if (down2) acc2.push(...down2);
4848
+ if (downEdges2) acc2.push(...downEdges2);
4849
+ return acc2;
4850
+ }
4851
+ if (direction === "upstream") {
4852
+ const up = depsByPath.get(path) ?? [];
4853
+ const upEdges = incomingEdges.get(path);
4854
+ if (!upEdges) return up;
4855
+ return [...up, ...upEdges];
4856
+ }
4857
+ const down = reverseDeps.get(path);
4858
+ const downEdges = outgoingEdges.get(path);
4859
+ const acc = down ? [...down] : [];
4860
+ if (downEdges) acc.push(...downEdges);
4861
+ return acc;
4862
+ };
4863
+ const visited = /* @__PURE__ */ new Set([from]);
4864
+ const depths = /* @__PURE__ */ new Map();
4865
+ const queue = [{ path: from, depth: 0 }];
4866
+ let head = 0;
4867
+ let truncated = false;
4868
+ while (head < queue.length) {
4869
+ const next = queue[head++];
4870
+ if (maxDepth != null && next.depth >= maxDepth) {
4871
+ if (visit(next.path).length > 0) truncated = true;
4872
+ continue;
4873
+ }
4874
+ for (const nb of visit(next.path)) {
4875
+ if (!nb || visited.has(nb)) continue;
4876
+ visited.add(nb);
4877
+ depths.set(nb, next.depth + 1);
4878
+ queue.push({ path: nb, depth: next.depth + 1 });
4879
+ }
4880
+ }
4881
+ const paths = [...depths.keys()].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
4882
+ if (options.withDetail) return { paths, depths, truncated };
4883
+ return paths;
4884
+ }
4885
+
4886
+ // src/compat/zustand/index.ts
4887
+ var alwaysDiffer = () => false;
4888
+ function create(initializer) {
4889
+ const g = new Graph("zustand");
4890
+ const s = state(void 0, {
4891
+ name: "state",
4892
+ equals: alwaysDiffer
4893
+ });
4894
+ g.add("state", s);
4895
+ const getState = () => s.cache;
4896
+ const setState = (partial, replace) => {
4897
+ const prev = s.cache;
4898
+ const next = typeof partial === "function" ? partial(prev) : partial;
4899
+ s.emit(replace ? next : { ...prev, ...next });
4900
+ };
4901
+ const api = {
4902
+ getState,
4903
+ setState,
4904
+ getInitialState: () => initialValue,
4905
+ subscribe: (listener) => {
4906
+ let initial = true;
4907
+ let prev = s.cache;
4908
+ return s.subscribe((msgs) => {
4909
+ for (const [t, v] of msgs) {
4910
+ if (t === DATA) {
4911
+ if (initial) {
4912
+ initial = false;
4913
+ continue;
4914
+ }
4915
+ listener(v, prev);
4916
+ prev = v;
4917
+ }
4918
+ }
4919
+ });
4920
+ },
4921
+ destroy: g.destroy.bind(g)
4922
+ };
4923
+ const initialValue = initializer(setState, getState, api);
4924
+ s.emit(initialValue);
4925
+ return Object.assign(g, api);
4926
+ }
4927
+ // Annotate the CommonJS export names for ESM import in node:
4928
+ 0 && (module.exports = {
4929
+ create
4930
+ });
4931
+ //# sourceMappingURL=index.cjs.map