@graphrefly/graphrefly 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +7 -5
  2. package/dist/chunk-44HD4BTA.js +47 -0
  3. package/dist/chunk-44HD4BTA.js.map +1 -0
  4. package/dist/chunk-7TAQJHQV.js +103 -0
  5. package/dist/chunk-7TAQJHQV.js.map +1 -0
  6. package/dist/chunk-BLD3IFYF.js +6827 -0
  7. package/dist/chunk-BLD3IFYF.js.map +1 -0
  8. package/dist/{chunk-ZTCDY5NQ.js → chunk-EQUZ5NLD.js} +34 -45
  9. package/dist/chunk-EQUZ5NLD.js.map +1 -0
  10. package/dist/{chunk-VOQFK7YN.js → chunk-IR3KMOLX.js} +358 -128
  11. package/dist/chunk-IR3KMOLX.js.map +1 -0
  12. package/dist/{chunk-XWBVAO2R.js → chunk-MQBQOFDS.js} +20 -11
  13. package/dist/chunk-MQBQOFDS.js.map +1 -0
  14. package/dist/chunk-NXC35KC5.js +2417 -0
  15. package/dist/chunk-NXC35KC5.js.map +1 -0
  16. package/dist/chunk-QA3RP5NH.js +2234 -0
  17. package/dist/chunk-QA3RP5NH.js.map +1 -0
  18. package/dist/chunk-RHI3GHZW.js +115 -0
  19. package/dist/chunk-RHI3GHZW.js.map +1 -0
  20. package/dist/{chunk-7IGHIFTT.js → chunk-TH6COGOP.js} +15 -26
  21. package/dist/chunk-TH6COGOP.js.map +1 -0
  22. package/dist/compat/nestjs/index.cjs +3366 -2259
  23. package/dist/compat/nestjs/index.cjs.map +1 -1
  24. package/dist/compat/nestjs/index.d.cts +6 -4
  25. package/dist/compat/nestjs/index.d.ts +6 -4
  26. package/dist/compat/nestjs/index.js +8 -8
  27. package/dist/core/index.cjs +1611 -1218
  28. package/dist/core/index.cjs.map +1 -1
  29. package/dist/core/index.d.cts +3 -2
  30. package/dist/core/index.d.ts +3 -2
  31. package/dist/core/index.js +37 -34
  32. package/dist/extra/index.cjs +7387 -6089
  33. package/dist/extra/index.cjs.map +1 -1
  34. package/dist/extra/index.d.cts +4 -4
  35. package/dist/extra/index.d.ts +4 -4
  36. package/dist/extra/index.js +57 -30
  37. package/dist/graph/index.cjs +3107 -2216
  38. package/dist/graph/index.cjs.map +1 -1
  39. package/dist/graph/index.d.cts +5 -3
  40. package/dist/graph/index.d.ts +5 -3
  41. package/dist/graph/index.js +24 -11
  42. package/dist/graph-DFr0diXB.d.ts +1128 -0
  43. package/dist/graph-ab1yPwIB.d.cts +1128 -0
  44. package/dist/{index-8a605sg9.d.ts → index-BHm3Ba5q.d.ts} +2 -2
  45. package/dist/{index-DuN3bhtm.d.ts → index-BbYZma8G.d.ts} +1697 -586
  46. package/dist/{index-SFzE_KTa.d.cts → index-BvWfZCTt.d.cts} +1697 -586
  47. package/dist/index-C9z6rU9P.d.cts +388 -0
  48. package/dist/{index-BjtlNirP.d.cts → index-D36MAQ3f.d.ts} +4 -4
  49. package/dist/{index-VHA43cGP.d.cts → index-DLE1Sp-L.d.cts} +2 -2
  50. package/dist/{index-CgSiUouz.d.ts → index-DrJq9B1T.d.cts} +4 -4
  51. package/dist/index-DsGxLfwL.d.ts +315 -0
  52. package/dist/index-Dy04P4W3.d.cts +315 -0
  53. package/dist/index-HdJx_BjO.d.ts +388 -0
  54. package/dist/index.cjs +9781 -7878
  55. package/dist/index.cjs.map +1 -1
  56. package/dist/index.d.cts +214 -37
  57. package/dist/index.d.ts +214 -37
  58. package/dist/index.js +905 -638
  59. package/dist/index.js.map +1 -1
  60. package/dist/meta--fr9sxRM.d.cts +41 -0
  61. package/dist/meta-n3FoVWML.d.ts +41 -0
  62. package/dist/node-C5UD5MGq.d.cts +1146 -0
  63. package/dist/node-C5UD5MGq.d.ts +1146 -0
  64. package/dist/{observable-DcBwQY7t.d.ts → observable-CQRBtEbq.d.ts} +1 -1
  65. package/dist/{observable-C8Kx_O6k.d.cts → observable-DWydVy5b.d.cts} +1 -1
  66. package/dist/patterns/reactive-layout/index.cjs +3102 -2132
  67. package/dist/patterns/reactive-layout/index.cjs.map +1 -1
  68. package/dist/patterns/reactive-layout/index.d.cts +5 -3
  69. package/dist/patterns/reactive-layout/index.d.ts +5 -3
  70. package/dist/patterns/reactive-layout/index.js +5 -4
  71. package/dist/storage-Bew05Xy6.d.cts +182 -0
  72. package/dist/storage-C9fZfMfM.d.ts +182 -0
  73. package/package.json +1 -1
  74. package/dist/chunk-2UDLYZHT.js +0 -2117
  75. package/dist/chunk-2UDLYZHT.js.map +0 -1
  76. package/dist/chunk-4MQ2J6IG.js +0 -1631
  77. package/dist/chunk-4MQ2J6IG.js.map +0 -1
  78. package/dist/chunk-7IGHIFTT.js.map +0 -1
  79. package/dist/chunk-DOSLSFKL.js +0 -162
  80. package/dist/chunk-DOSLSFKL.js.map +0 -1
  81. package/dist/chunk-ECN37NVS.js +0 -6227
  82. package/dist/chunk-ECN37NVS.js.map +0 -1
  83. package/dist/chunk-G66H6ZRK.js +0 -111
  84. package/dist/chunk-G66H6ZRK.js.map +0 -1
  85. package/dist/chunk-VOQFK7YN.js.map +0 -1
  86. package/dist/chunk-WZ2Z2CRV.js +0 -32
  87. package/dist/chunk-WZ2Z2CRV.js.map +0 -1
  88. package/dist/chunk-XWBVAO2R.js.map +0 -1
  89. package/dist/chunk-ZTCDY5NQ.js.map +0 -1
  90. package/dist/graph-KsTe57nI.d.cts +0 -750
  91. package/dist/graph-mILUUqW8.d.ts +0 -750
  92. package/dist/index-B2SvPEbc.d.ts +0 -257
  93. package/dist/index-BHfg_Ez3.d.ts +0 -629
  94. package/dist/index-Bc_diYYJ.d.cts +0 -629
  95. package/dist/index-UudxGnzc.d.cts +0 -257
  96. package/dist/meta-BnG7XAaE.d.cts +0 -778
  97. package/dist/meta-BnG7XAaE.d.ts +0 -778
@@ -1,2117 +0,0 @@
1
- import {
2
- describeNode,
3
- resolveDescribeFields
4
- } from "./chunk-7IGHIFTT.js";
5
- import {
6
- COMPLETE,
7
- DATA,
8
- DIRTY,
9
- ERROR,
10
- GuardDenied,
11
- INVALIDATE,
12
- NodeImpl,
13
- RESOLVED,
14
- TEARDOWN,
15
- isBatching,
16
- messageTier,
17
- monotonicNs,
18
- state
19
- } from "./chunk-4MQ2J6IG.js";
20
-
21
- // src/graph/sizeof.ts
22
- var OVERHEAD = {
23
- object: 56,
24
- array: 64,
25
- string: 40,
26
- // header; content added separately
27
- number: 8,
28
- boolean: 4,
29
- null: 0,
30
- undefined: 0,
31
- symbol: 40,
32
- bigint: 16,
33
- function: 120,
34
- map: 72,
35
- set: 72,
36
- mapEntry: 40,
37
- setEntry: 24
38
- };
39
- function sizeof(value) {
40
- const seen = /* @__PURE__ */ new WeakSet();
41
- return _sizeof(value, seen);
42
- }
43
- function _sizeof(value, seen) {
44
- if (value == null) return 0;
45
- const t = typeof value;
46
- switch (t) {
47
- case "number":
48
- return OVERHEAD.number;
49
- case "boolean":
50
- return OVERHEAD.boolean;
51
- case "string":
52
- return OVERHEAD.string + value.length * 2;
53
- // UTF-16
54
- case "bigint":
55
- return OVERHEAD.bigint;
56
- case "symbol":
57
- return OVERHEAD.symbol;
58
- case "function":
59
- if (seen.has(value)) return 0;
60
- seen.add(value);
61
- return OVERHEAD.function;
62
- case "undefined":
63
- return 0;
64
- }
65
- const obj = value;
66
- if (seen.has(obj)) return 0;
67
- seen.add(obj);
68
- if (obj instanceof Map) {
69
- let size2 = OVERHEAD.map;
70
- for (const [k, v] of obj) {
71
- size2 += OVERHEAD.mapEntry + _sizeof(k, seen) + _sizeof(v, seen);
72
- }
73
- return size2;
74
- }
75
- if (obj instanceof Set) {
76
- let size2 = OVERHEAD.set;
77
- for (const v of obj) {
78
- size2 += OVERHEAD.setEntry + _sizeof(v, seen);
79
- }
80
- return size2;
81
- }
82
- if (Array.isArray(obj)) {
83
- let size2 = OVERHEAD.array + obj.length * 8;
84
- for (const item of obj) {
85
- size2 += _sizeof(item, seen);
86
- }
87
- return size2;
88
- }
89
- if (obj instanceof ArrayBuffer) return obj.byteLength;
90
- if (ArrayBuffer.isView(obj)) return obj.byteLength;
91
- let size = OVERHEAD.object;
92
- const keys = Object.keys(obj);
93
- for (const key of keys) {
94
- size += OVERHEAD.string + key.length * 2;
95
- size += _sizeof(obj[key], seen);
96
- }
97
- return size;
98
- }
99
-
100
- // src/graph/profile.ts
101
- function graphProfile(graph, opts) {
102
- const topN = opts?.topN ?? 10;
103
- const desc = graph.describe({ detail: "standard" });
104
- const targets = [];
105
- if (typeof graph._collectObserveTargets === "function") {
106
- graph._collectObserveTargets("", targets);
107
- }
108
- const pathToNode = /* @__PURE__ */ new Map();
109
- for (const [p, n] of targets) {
110
- pathToNode.set(p, n);
111
- }
112
- const profiles = [];
113
- for (const [path, nodeDesc] of Object.entries(desc.nodes)) {
114
- const nd = pathToNode.get(path);
115
- const impl = nd instanceof NodeImpl ? nd : null;
116
- const valueSizeBytes = impl ? sizeof(impl.get()) : 0;
117
- const subscriberCount = impl ? impl._sinkCount : 0;
118
- const depCount = nodeDesc.deps?.length ?? 0;
119
- const isOrphanEffect = nodeDesc.type === "effect" && subscriberCount === 0;
120
- profiles.push({
121
- path,
122
- type: nodeDesc.type,
123
- status: nodeDesc.status ?? "unknown",
124
- valueSizeBytes,
125
- subscriberCount,
126
- depCount,
127
- isOrphanEffect
128
- });
129
- }
130
- const totalValueSizeBytes = profiles.reduce((sum, p) => sum + p.valueSizeBytes, 0);
131
- const hotspots = [...profiles].sort((a, b) => b.valueSizeBytes - a.valueSizeBytes).slice(0, topN);
132
- const orphanEffects = profiles.filter((p) => p.isOrphanEffect);
133
- return {
134
- nodeCount: profiles.length,
135
- edgeCount: desc.edges.length,
136
- subgraphCount: desc.subgraphs.length,
137
- nodes: profiles,
138
- totalValueSizeBytes,
139
- hotspots,
140
- orphanEffects
141
- };
142
- }
143
-
144
- // src/graph/graph.ts
145
- var PATH_SEP = "::";
146
- var GRAPH_META_SEGMENT = "__meta__";
147
- var SNAPSHOT_VERSION = 1;
148
- function parseSnapshotEnvelope(data) {
149
- if (data.version !== SNAPSHOT_VERSION) {
150
- throw new Error(
151
- `unsupported snapshot version ${String(data.version)} (expected ${SNAPSHOT_VERSION})`
152
- );
153
- }
154
- for (const key of ["name", "nodes", "edges", "subgraphs"]) {
155
- if (!(key in data)) {
156
- throw new Error(`snapshot missing required key "${key}"`);
157
- }
158
- }
159
- if (typeof data.name !== "string") {
160
- throw new TypeError(`snapshot 'name' must be a string`);
161
- }
162
- if (typeof data.nodes !== "object" || data.nodes === null || Array.isArray(data.nodes)) {
163
- throw new TypeError(`snapshot 'nodes' must be an object`);
164
- }
165
- if (!Array.isArray(data.edges)) {
166
- throw new TypeError(`snapshot 'edges' must be an array`);
167
- }
168
- if (!Array.isArray(data.subgraphs)) {
169
- throw new TypeError(`snapshot 'subgraphs' must be an array`);
170
- }
171
- }
172
- function sortJsonValue(value) {
173
- if (value === null || typeof value !== "object") {
174
- return value;
175
- }
176
- if (Array.isArray(value)) {
177
- return value.map(sortJsonValue);
178
- }
179
- const obj = value;
180
- const keys = Object.keys(obj).sort();
181
- const out = {};
182
- for (const k of keys) {
183
- out[k] = sortJsonValue(obj[k]);
184
- }
185
- return out;
186
- }
187
- function stableJsonStringify(value) {
188
- return `${JSON.stringify(sortJsonValue(value))}
189
- `;
190
- }
191
- function escapeMermaidLabel(value) {
192
- return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
193
- }
194
- function escapeD2Label(value) {
195
- return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
196
- }
197
- function d2DirectionFromGraphDirection(direction) {
198
- if (direction === "TD") return "down";
199
- if (direction === "BT") return "up";
200
- if (direction === "RL") return "left";
201
- return "right";
202
- }
203
- function collectDiagramArrows(described) {
204
- const seen = /* @__PURE__ */ new Set();
205
- const arrows = [];
206
- function add(from, to) {
207
- const key = `${from}\0${to}`;
208
- if (seen.has(key)) return;
209
- seen.add(key);
210
- arrows.push([from, to]);
211
- }
212
- for (const [path, info] of Object.entries(described.nodes)) {
213
- const deps = info.deps;
214
- if (deps) {
215
- for (const dep of deps) add(dep, path);
216
- }
217
- }
218
- for (const edge of described.edges) add(edge.from, edge.to);
219
- return arrows;
220
- }
221
- function normalizeDiagramDirection(direction) {
222
- if (direction === void 0) return "LR";
223
- if (direction === "TD" || direction === "LR" || direction === "BT" || direction === "RL") {
224
- return direction;
225
- }
226
- throw new Error(
227
- `invalid diagram direction ${String(direction)}; expected one of: TD, LR, BT, RL`
228
- );
229
- }
230
- function escapeRegexLiteral(value) {
231
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
232
- }
233
- function globToRegex(pattern) {
234
- let re = "^";
235
- for (let i = 0; i < pattern.length; i += 1) {
236
- const ch = pattern[i];
237
- if (ch === "*") {
238
- re += ".*";
239
- continue;
240
- }
241
- if (ch === "?") {
242
- re += ".";
243
- continue;
244
- }
245
- if (ch === "[") {
246
- const end = pattern.indexOf("]", i + 1);
247
- if (end <= i + 1) {
248
- re += "\\[";
249
- continue;
250
- }
251
- let cls = pattern.slice(i + 1, end);
252
- if (cls.startsWith("!")) cls = `^${cls.slice(1)}`;
253
- cls = cls.replace(/\\/g, "\\\\");
254
- re += `[${cls}]`;
255
- i = end;
256
- continue;
257
- }
258
- re += escapeRegexLiteral(ch);
259
- }
260
- re += "$";
261
- return new RegExp(re);
262
- }
263
- var RingBuffer = class {
264
- constructor(capacity) {
265
- this.capacity = capacity;
266
- this.buf = new Array(capacity);
267
- }
268
- buf;
269
- head = 0;
270
- _size = 0;
271
- get size() {
272
- return this._size;
273
- }
274
- push(item) {
275
- const idx = (this.head + this._size) % this.capacity;
276
- this.buf[idx] = item;
277
- if (this._size < this.capacity) this._size++;
278
- else this.head = (this.head + 1) % this.capacity;
279
- }
280
- toArray() {
281
- const result = [];
282
- for (let i = 0; i < this._size; i++) result.push(this.buf[(this.head + i) % this.capacity]);
283
- return result;
284
- }
285
- };
286
- var OBSERVE_ANSI_THEME = {
287
- data: "\x1B[32m",
288
- dirty: "\x1B[33m",
289
- resolved: "\x1B[36m",
290
- complete: "\x1B[34m",
291
- error: "\x1B[31m",
292
- derived: "\x1B[35m",
293
- path: "\x1B[90m",
294
- reset: "\x1B[0m"
295
- };
296
- var OBSERVE_NO_COLOR_THEME = {
297
- data: "",
298
- dirty: "",
299
- resolved: "",
300
- complete: "",
301
- error: "",
302
- derived: "",
303
- path: "",
304
- reset: ""
305
- };
306
- function describeData(value) {
307
- if (typeof value === "string") return JSON.stringify(value);
308
- if (typeof value === "number" || typeof value === "boolean" || value == null)
309
- return String(value);
310
- try {
311
- return JSON.stringify(value);
312
- } catch {
313
- return "[unserializable]";
314
- }
315
- }
316
- function resolveObserveTheme(theme) {
317
- if (theme === "none") return OBSERVE_NO_COLOR_THEME;
318
- if (theme === "ansi" || theme == null) return OBSERVE_ANSI_THEME;
319
- return {
320
- data: theme.data ?? "",
321
- dirty: theme.dirty ?? "",
322
- resolved: theme.resolved ?? "",
323
- complete: theme.complete ?? "",
324
- error: theme.error ?? "",
325
- derived: theme.derived ?? "",
326
- path: theme.path ?? "",
327
- reset: theme.reset ?? ""
328
- };
329
- }
330
- function resolveObserveDetail(opts) {
331
- if (opts == null) return {};
332
- const detail = opts.detail;
333
- if (detail === "full") {
334
- return {
335
- ...opts,
336
- structured: opts.structured ?? true,
337
- timeline: opts.timeline ?? true,
338
- causal: opts.causal ?? true,
339
- derived: opts.derived ?? true
340
- };
341
- }
342
- if (detail === "minimal") {
343
- return { ...opts, structured: opts.structured ?? true };
344
- }
345
- return opts;
346
- }
347
- function assertLocalName(name, graphName, label) {
348
- if (name === "") {
349
- throw new Error(`Graph "${graphName}": ${label} name must be non-empty`);
350
- }
351
- }
352
- function assertNoPathSep(name, graphName, label) {
353
- if (name.includes(PATH_SEP)) {
354
- throw new Error(
355
- `Graph "${graphName}": ${label} "${name}" must not contain '${PATH_SEP}' (path separator)`
356
- );
357
- }
358
- }
359
- function assertNotReservedMetaSegment(name, graphName, label) {
360
- if (name === GRAPH_META_SEGMENT) {
361
- throw new Error(
362
- `Graph "${graphName}": ${label} name "${GRAPH_META_SEGMENT}" is reserved for meta companion paths`
363
- );
364
- }
365
- }
366
- function assertConnectPathNotMeta(path, graphName) {
367
- if (path.split(PATH_SEP).includes(GRAPH_META_SEGMENT)) {
368
- throw new Error(
369
- `Graph "${graphName}": connect/disconnect endpoints must be registered graph nodes, not meta paths (got "${path}")`
370
- );
371
- }
372
- }
373
- function splitPath(path, graphName) {
374
- if (path === "") {
375
- throw new Error(`Graph "${graphName}": resolve path must be non-empty`);
376
- }
377
- const segments = path.split(PATH_SEP);
378
- for (const s of segments) {
379
- if (s === "") {
380
- throw new Error(`Graph "${graphName}": resolve path has empty segment`);
381
- }
382
- }
383
- return segments;
384
- }
385
- function edgeKey(from, to) {
386
- return `${from} ${to}`;
387
- }
388
- function parseEdgeKey(key) {
389
- const i = key.indexOf(" ");
390
- return [key.slice(0, i), key.slice(i + 1)];
391
- }
392
- var META_FILTERED_TYPES = /* @__PURE__ */ new Set([TEARDOWN, INVALIDATE, COMPLETE, ERROR]);
393
- function filterMetaMessages(messages) {
394
- const kept = messages.filter((m) => !META_FILTERED_TYPES.has(m[0]));
395
- return kept;
396
- }
397
- function teardownMountedGraph(root) {
398
- for (const child of root._mounts.values()) {
399
- teardownMountedGraph(child);
400
- }
401
- for (const n of root._nodes.values()) {
402
- n.down([[TEARDOWN]], { internal: true });
403
- }
404
- }
405
- var Graph = class _Graph {
406
- static _factories = [];
407
- name;
408
- opts;
409
- /** @internal — exposed for {@link teardownMountedGraph} and cross-graph helpers. */
410
- _nodes = /* @__PURE__ */ new Map();
411
- _edges = /* @__PURE__ */ new Set();
412
- /** @internal — exposed for {@link teardownMountedGraph}. */
413
- _mounts = /* @__PURE__ */ new Map();
414
- _autoCheckpointDisposers = /* @__PURE__ */ new Set();
415
- _disposers = /* @__PURE__ */ new Set();
416
- _defaultVersioningLevel;
417
- static registerFactory(pattern, factory) {
418
- if (!pattern) {
419
- throw new Error("Graph.registerFactory requires a non-empty pattern");
420
- }
421
- _Graph.unregisterFactory(pattern);
422
- _Graph._factories.push({ pattern, re: globToRegex(pattern), factory });
423
- }
424
- static unregisterFactory(pattern) {
425
- const i = _Graph._factories.findIndex((entry) => entry.pattern === pattern);
426
- if (i >= 0) _Graph._factories.splice(i, 1);
427
- }
428
- /**
429
- * @param name - Non-empty graph id (must not contain `::`).
430
- * @param opts - Reserved for future hooks; currently unused.
431
- */
432
- constructor(name, opts) {
433
- if (name === "") {
434
- throw new Error("Graph name must be non-empty");
435
- }
436
- if (name.includes(PATH_SEP)) {
437
- throw new Error(`Graph name must not contain '${PATH_SEP}' (got "${name}")`);
438
- }
439
- this.name = name;
440
- this.opts = opts ?? {};
441
- }
442
- static _factoryForPath(path) {
443
- for (let i = _Graph._factories.length - 1; i >= 0; i -= 1) {
444
- const entry = _Graph._factories[i];
445
- if (entry.re.test(path)) return entry.factory;
446
- }
447
- return void 0;
448
- }
449
- static _ownerForPath(root, path) {
450
- const segments = path.split(PATH_SEP);
451
- const local = segments.pop();
452
- if (local == null || local.length === 0) {
453
- throw new Error(`invalid snapshot path "${path}"`);
454
- }
455
- let owner = root;
456
- for (const seg of segments) {
457
- const next = owner._mounts.get(seg);
458
- if (!next) throw new Error(`unknown mount "${seg}" in path "${path}"`);
459
- owner = next;
460
- }
461
- return [owner, local];
462
- }
463
- /**
464
- * Graphs reachable from this instance via nested {@link Graph.mount} (includes `this`).
465
- */
466
- _graphsReachableViaMounts(seen = /* @__PURE__ */ new Set()) {
467
- if (seen.has(this)) return seen;
468
- seen.add(this);
469
- for (const child of this._mounts.values()) {
470
- child._graphsReachableViaMounts(seen);
471
- }
472
- return seen;
473
- }
474
- /**
475
- * Resolve an endpoint: returns `[owningGraph, localName, node]`.
476
- * Accepts both local names and `::` qualified paths.
477
- */
478
- _resolveEndpoint(path) {
479
- if (!path.includes(PATH_SEP)) {
480
- const n = this._nodes.get(path);
481
- if (!n) {
482
- throw new Error(`Graph "${this.name}": unknown node "${path}"`);
483
- }
484
- return [this, path, n];
485
- }
486
- const segments = splitPath(path, this.name);
487
- return this._resolveEndpointFromSegments(segments, path);
488
- }
489
- _resolveEndpointFromSegments(segments, fullPath) {
490
- const head = segments[0];
491
- const rest = segments.slice(1);
492
- if (rest.length === 0) {
493
- const n = this._nodes.get(head);
494
- if (n) return [this, head, n];
495
- throw new Error(`Graph "${this.name}": unknown node "${head}" (from path "${fullPath}")`);
496
- }
497
- const localN = this._nodes.get(head);
498
- if (localN && rest.length > 0 && rest[0] === GRAPH_META_SEGMENT) {
499
- return this._resolveMetaEndpointKeys(localN, head, rest, fullPath);
500
- }
501
- const child = this._mounts.get(head);
502
- if (!child) {
503
- if (this._nodes.has(head)) {
504
- throw new Error(
505
- `Graph "${this.name}": "${head}" is a node; trailing path "${rest.join(PATH_SEP)}" is invalid`
506
- );
507
- }
508
- throw new Error(`Graph "${this.name}": unknown mount or node "${head}"`);
509
- }
510
- return child._resolveEndpointFromSegments(rest, fullPath);
511
- }
512
- // ——————————————————————————————————————————————————————————————
513
- // Node registry
514
- // ——————————————————————————————————————————————————————————————
515
- /**
516
- * Registers a node under a local name. Fails if the name is already used,
517
- * reserved by a mount, or the same node instance is already registered.
518
- *
519
- * @param name - Local key (no `::`).
520
- * @param node - Node instance to own.
521
- */
522
- add(name, node) {
523
- assertLocalName(name, this.name, "add");
524
- assertNoPathSep(name, this.name, "add");
525
- assertNotReservedMetaSegment(name, this.name, "node");
526
- if (this._mounts.has(name)) {
527
- throw new Error(`Graph "${this.name}": name "${name}" is already a mount point`);
528
- }
529
- if (this._nodes.has(name)) {
530
- throw new Error(`Graph "${this.name}": node "${name}" already exists`);
531
- }
532
- for (const [existingName, existing] of this._nodes) {
533
- if (existing === node) {
534
- throw new Error(
535
- `Graph "${this.name}": node instance already registered as "${existingName}"`
536
- );
537
- }
538
- }
539
- this._nodes.set(name, node);
540
- if (node instanceof NodeImpl) {
541
- node._assignRegistryName(name);
542
- if (this._defaultVersioningLevel != null) {
543
- node._applyVersioning(this._defaultVersioningLevel);
544
- }
545
- if (node._deps.length > 0) {
546
- for (const dep of node._deps) {
547
- for (const [depName, depNode] of this._nodes) {
548
- if (depNode === dep) {
549
- this._edges.add(edgeKey(depName, name));
550
- break;
551
- }
552
- }
553
- }
554
- }
555
- for (const [otherName, otherNode] of this._nodes) {
556
- if (otherName === name) continue;
557
- if (otherNode instanceof NodeImpl && otherNode._deps.includes(node)) {
558
- this._edges.add(edgeKey(name, otherName));
559
- }
560
- }
561
- }
562
- }
563
- /**
564
- * Set a default versioning level for all nodes added to this graph (roadmap §6.0).
565
- *
566
- * Nodes already registered are retroactively upgraded. Nodes added later via
567
- * {@link add} will inherit this level unless they already have versioning.
568
- *
569
- * **Scope:** Does not propagate to mounted subgraphs. Call `setVersioning`
570
- * on each child graph separately if needed.
571
- *
572
- * @param level - `0` for V0, `1` for V1, or `undefined` to clear.
573
- */
574
- setVersioning(level) {
575
- this._defaultVersioningLevel = level;
576
- if (level == null) return;
577
- for (const n of this._nodes.values()) {
578
- if (n instanceof NodeImpl) {
579
- n._applyVersioning(level);
580
- }
581
- }
582
- }
583
- /**
584
- * Unregisters a node or unmounts a subgraph, drops incident edges, and sends
585
- * `[[TEARDOWN]]` to the removed node or recursively through the mounted subtree (§3.2).
586
- *
587
- * @param name - Local mount or node name.
588
- */
589
- remove(name) {
590
- assertLocalName(name, this.name, "remove");
591
- assertNoPathSep(name, this.name, "remove");
592
- const child = this._mounts.get(name);
593
- if (child) {
594
- this._mounts.delete(name);
595
- const prefix = `${name}${PATH_SEP}`;
596
- for (const key of [...this._edges]) {
597
- const [from, to] = parseEdgeKey(key);
598
- if (from === name || to === name || from.startsWith(prefix) || to.startsWith(prefix)) {
599
- this._edges.delete(key);
600
- }
601
- }
602
- teardownMountedGraph(child);
603
- return;
604
- }
605
- const node = this._nodes.get(name);
606
- if (!node) {
607
- throw new Error(`Graph "${this.name}": unknown node or mount "${name}"`);
608
- }
609
- this._nodes.delete(name);
610
- for (const key of [...this._edges]) {
611
- const [from, to] = parseEdgeKey(key);
612
- if (from === name || to === name) this._edges.delete(key);
613
- }
614
- node.down([[TEARDOWN]], { internal: true });
615
- }
616
- /**
617
- * Returns a node by local name or `::` qualified path.
618
- * Local names are looked up directly; paths with `::` delegate to {@link resolve}.
619
- *
620
- * @param name - Local name or qualified path.
621
- */
622
- node(name) {
623
- if (name === "") {
624
- throw new Error(`Graph "${this.name}": node name must be non-empty`);
625
- }
626
- if (name.includes(PATH_SEP)) {
627
- return this.resolve(name);
628
- }
629
- const n = this._nodes.get(name);
630
- if (!n) {
631
- throw new Error(`Graph "${this.name}": unknown node "${name}"`);
632
- }
633
- return n;
634
- }
635
- /**
636
- * Reads `graph.node(name).get()` — accepts `::` qualified paths (§3.2).
637
- *
638
- * @param name - Local name or qualified path.
639
- * @returns Cached value or `undefined`.
640
- */
641
- get(name) {
642
- return this.node(name).get();
643
- }
644
- /**
645
- * Shorthand for `graph.node(name).down([[DATA, value]], { actor })` — accepts `::` qualified paths (§3.2).
646
- *
647
- * @param name - Local name or qualified path.
648
- * @param value - Next `DATA` payload.
649
- * @param options - Optional `actor` and `internal` guard bypass.
650
- */
651
- set(name, value, options) {
652
- const internal = options?.internal === true;
653
- this.node(name).down([[DATA, value]], {
654
- actor: options?.actor,
655
- internal,
656
- delivery: "write"
657
- });
658
- }
659
- // ——————————————————————————————————————————————————————————————
660
- // Edges
661
- // ——————————————————————————————————————————————————————————————
662
- /**
663
- * Record a wire from `fromPath` → `toPath` (§3.3). Accepts local names or
664
- * `::` qualified paths. The target must be a {@link NodeImpl} whose `_deps`
665
- * includes the source node (same reference). Idempotent.
666
- *
667
- * Same-owner edges are stored on the owning child graph; cross-subgraph edges
668
- * are stored on this (parent) graph's registry.
669
- *
670
- * @param fromPath - Source endpoint (local or qualified).
671
- * @param toPath - Target endpoint whose deps already include the source node.
672
- */
673
- connect(fromPath, toPath) {
674
- if (!fromPath || !toPath) {
675
- throw new Error(`Graph "${this.name}": connect paths must be non-empty`);
676
- }
677
- assertConnectPathNotMeta(fromPath, this.name);
678
- assertConnectPathNotMeta(toPath, this.name);
679
- const [fromGraph, fromLocal, fromNode] = this._resolveEndpoint(fromPath);
680
- const [toGraph, toLocal, toNode] = this._resolveEndpoint(toPath);
681
- if (fromNode === toNode) {
682
- throw new Error(`Graph "${this.name}": cannot connect a node to itself`);
683
- }
684
- if (!(toNode instanceof NodeImpl)) {
685
- throw new Error(
686
- `Graph "${this.name}": connect(${fromPath}, ${toPath}) requires the target to be a graphrefly NodeImpl so deps can be validated`
687
- );
688
- }
689
- if (!toNode._deps.includes(fromNode)) {
690
- throw new Error(
691
- `Graph "${this.name}": connect(${fromPath}, ${toPath}) \u2014 target must include source in its constructor deps (same node reference)`
692
- );
693
- }
694
- if (fromGraph === toGraph) {
695
- const key = edgeKey(fromLocal, toLocal);
696
- fromGraph._edges.add(key);
697
- } else {
698
- const key = edgeKey(fromPath, toPath);
699
- this._edges.add(key);
700
- }
701
- }
702
- /**
703
- * Remove a registered edge (§3.3). Accepts local names or `::` qualified paths.
704
- *
705
- * **Registry-only (§C resolved):** This drops the edge from the graph's edge
706
- * registry only. It does **not** mutate the target node's constructor-time
707
- * dependency list, bitmasks, or upstream subscriptions. Message flow follows
708
- * constructor-time deps, not the edge registry. For runtime dep rewiring, use
709
- * {@link dynamicNode}.
710
- *
711
- * @param fromPath - Registered edge tail.
712
- * @param toPath - Registered edge head.
713
- */
714
- disconnect(fromPath, toPath) {
715
- if (!fromPath || !toPath) {
716
- throw new Error(`Graph "${this.name}": disconnect paths must be non-empty`);
717
- }
718
- assertConnectPathNotMeta(fromPath, this.name);
719
- assertConnectPathNotMeta(toPath, this.name);
720
- const [fromGraph, fromLocal] = this._resolveEndpoint(fromPath);
721
- const [toGraph, toLocal] = this._resolveEndpoint(toPath);
722
- if (fromGraph === toGraph) {
723
- const key = edgeKey(fromLocal, toLocal);
724
- if (!fromGraph._edges.delete(key)) {
725
- throw new Error(`Graph "${this.name}": no registered edge ${fromPath} \u2192 ${toPath}`);
726
- }
727
- } else {
728
- const key = edgeKey(fromPath, toPath);
729
- if (!this._edges.delete(key)) {
730
- throw new Error(`Graph "${this.name}": no registered edge ${fromPath} \u2192 ${toPath}`);
731
- }
732
- }
733
- }
734
- /**
735
- * Returns registered `[from, to]` edge pairs (read-only snapshot).
736
- *
737
- * @returns Edge pairs recorded on this graph instance’s local `_edges` set.
738
- */
739
- edges() {
740
- const result = [];
741
- for (const key of this._edges) {
742
- result.push(parseEdgeKey(key));
743
- }
744
- return result;
745
- }
746
- // ——————————————————————————————————————————————————————————————
747
- // Composition
748
- // ——————————————————————————————————————————————————————————————
749
- /**
750
- * Embed a child graph at a local mount name (§3.4). Child nodes are reachable via
751
- * {@link Graph.resolve} using `::` delimited paths (§3.5). Lifecycle
752
- * {@link Graph.signal} visits mounted subgraphs recursively.
753
- *
754
- * Rejects: same name as existing node or mount, self-mount, mount cycles,
755
- * and the same child graph instance mounted twice on one parent.
756
- *
757
- * @param name - Local mount point.
758
- * @param child - Nested `Graph` instance.
759
- */
760
- mount(name, child) {
761
- assertLocalName(name, this.name, "mount");
762
- assertNoPathSep(name, this.name, "mount");
763
- assertNotReservedMetaSegment(name, this.name, "mount");
764
- if (this._nodes.has(name)) {
765
- throw new Error(
766
- `Graph "${this.name}": cannot mount at "${name}" \u2014 node with that name exists`
767
- );
768
- }
769
- if (this._mounts.has(name)) {
770
- throw new Error(`Graph "${this.name}": mount "${name}" already exists`);
771
- }
772
- if (child === this) {
773
- throw new Error(`Graph "${this.name}": cannot mount a graph into itself`);
774
- }
775
- for (const existing of this._mounts.values()) {
776
- if (existing === child) {
777
- throw new Error(`Graph "${this.name}": this child graph is already mounted on this graph`);
778
- }
779
- }
780
- if (child._graphsReachableViaMounts().has(this)) {
781
- throw new Error(`Graph "${this.name}": mount("${name}", \u2026) would create a mount cycle`);
782
- }
783
- this._mounts.set(name, child);
784
- }
785
- /**
786
- * Look up a node by qualified path (§3.5). Segments are separated by `::`.
787
- *
788
- * If the first segment equals this graph's {@link Graph.name}, it is stripped
789
- * (so `root.resolve("app::a")` works when `root.name === "app"`).
790
- *
791
- * @param path - Qualified `::` path or local name.
792
- * @returns The resolved `Node`.
793
- */
794
- resolve(path) {
795
- let segments = splitPath(path, this.name);
796
- if (segments[0] === this.name) {
797
- segments = segments.slice(1);
798
- if (segments.length === 0) {
799
- throw new Error(`Graph "${this.name}": resolve path ends at graph name only`);
800
- }
801
- }
802
- return this._resolveFromSegments(segments);
803
- }
804
- _resolveFromSegments(segments) {
805
- const head = segments[0];
806
- const rest = segments.slice(1);
807
- if (rest.length === 0) {
808
- const n = this._nodes.get(head);
809
- if (n) return n;
810
- if (this._mounts.has(head)) {
811
- throw new Error(
812
- `Graph "${this.name}": path ends at subgraph "${head}" \u2014 not a node (GRAPHREFLY-SPEC \xA73.5)`
813
- );
814
- }
815
- throw new Error(`Graph "${this.name}": unknown name "${head}"`);
816
- }
817
- const localN = this._nodes.get(head);
818
- if (localN && rest.length > 0 && rest[0] === GRAPH_META_SEGMENT) {
819
- return this._resolveMetaChainFromNode(localN, rest, segments.join(PATH_SEP));
820
- }
821
- const child = this._mounts.get(head);
822
- if (!child) {
823
- if (this._nodes.has(head)) {
824
- throw new Error(
825
- `Graph "${this.name}": "${head}" is a node; trailing path "${rest.join(PATH_SEP)}" is invalid`
826
- );
827
- }
828
- throw new Error(`Graph "${this.name}": unknown mount or node "${head}"`);
829
- }
830
- return child.resolve(rest.join(PATH_SEP));
831
- }
832
- /**
833
- * Resolve `::__meta__::key` segments from a registered primary node (possibly chained).
834
- */
835
- _resolveMetaChainFromNode(n, parts, fullPath) {
836
- let current = n;
837
- let i = 0;
838
- const p = [...parts];
839
- while (i < p.length) {
840
- if (p[i] !== GRAPH_META_SEGMENT) {
841
- throw new Error(
842
- `Graph "${this.name}": expected ${GRAPH_META_SEGMENT} segment in meta path "${fullPath}"`
843
- );
844
- }
845
- if (i + 1 >= p.length) {
846
- throw new Error(
847
- `Graph "${this.name}": meta path requires a key after ${GRAPH_META_SEGMENT} in "${fullPath}"`
848
- );
849
- }
850
- const key = p[i + 1];
851
- const next = current.meta[key];
852
- if (!next) {
853
- throw new Error(`Graph "${this.name}": unknown meta "${key}" in path "${fullPath}"`);
854
- }
855
- current = next;
856
- i += 2;
857
- }
858
- return current;
859
- }
860
- _resolveMetaEndpointKeys(baseNode, baseLocalKey, parts, fullPath) {
861
- let current = baseNode;
862
- let localKey = baseLocalKey;
863
- let i = 0;
864
- const p = [...parts];
865
- while (i < p.length) {
866
- if (p[i] !== GRAPH_META_SEGMENT) {
867
- throw new Error(
868
- `Graph "${this.name}": expected ${GRAPH_META_SEGMENT} segment in meta path "${fullPath}"`
869
- );
870
- }
871
- if (i + 1 >= p.length) {
872
- throw new Error(
873
- `Graph "${this.name}": meta path requires a key after ${GRAPH_META_SEGMENT} in "${fullPath}"`
874
- );
875
- }
876
- const metaKey = p[i + 1];
877
- const next = current.meta[metaKey];
878
- if (!next) {
879
- throw new Error(
880
- `Graph "${this.name}": unknown meta "${metaKey}" on node (in "${fullPath}")`
881
- );
882
- }
883
- localKey = `${localKey}${PATH_SEP}${GRAPH_META_SEGMENT}${PATH_SEP}${metaKey}`;
884
- current = next;
885
- i += 2;
886
- }
887
- return [this, localKey, current];
888
- }
889
- /**
890
- * Deliver a message batch to every registered node in this graph and, recursively,
891
- * in mounted child graphs (§3.7). Recurses into mounts first, then delivers to
892
- * local nodes (sorted by name). Each {@link Node} receives at most one delivery
893
- * per call (deduped by reference).
894
- *
895
- * Companion `meta` nodes receive the same batch for control-plane types (e.g.
896
- * PAUSE) that the primary does not forward. **TEARDOWN-only** batches skip the
897
- * extra meta pass — the primary’s `down()` already cascades TEARDOWN to meta.
898
- *
899
- * @param messages - Batch to deliver to every registered node (and mounts, recursively).
900
- * @param options - Optional `actor` / `internal` for transport.
901
- */
902
- signal(messages, options) {
903
- this._signalDeliver(messages, options ?? {}, /* @__PURE__ */ new Set());
904
- }
905
- _signalDeliver(messages, opts, vis) {
906
- for (const sub of this._mounts.values()) {
907
- sub._signalDeliver(messages, opts, vis);
908
- }
909
- const internal = opts.internal === true;
910
- const downOpts = internal ? { internal: true } : { actor: opts.actor, delivery: "signal" };
911
- const metaMessages = filterMetaMessages(messages);
912
- for (const localName of [...this._nodes.keys()].sort()) {
913
- const n = this._nodes.get(localName);
914
- if (vis.has(n)) continue;
915
- vis.add(n);
916
- n.down(messages, downOpts);
917
- if (metaMessages.length === 0) continue;
918
- this._signalMetaSubtree(n, metaMessages, vis, downOpts);
919
- }
920
- }
921
- _signalMetaSubtree(root, messages, vis, downOpts) {
922
- for (const mk of Object.keys(root.meta).sort()) {
923
- const mnode = root.meta[mk];
924
- if (vis.has(mnode)) continue;
925
- vis.add(mnode);
926
- mnode.down(messages, downOpts);
927
- this._signalMetaSubtree(mnode, messages, vis, downOpts);
928
- }
929
- }
930
- /**
931
- * Static structure snapshot: qualified node keys, edges, mount names (GRAPHREFLY-SPEC §3.6, Appendix B).
932
- *
933
- * @param options - Optional `actor` for guard-scoped visibility and/or `filter` for selective output.
934
- * @returns JSON-shaped describe payload for this graph tree.
935
- *
936
- * @example
937
- * ```ts
938
- * graph.describe() // full snapshot
939
- * graph.describe({ actor: llm }) // guard-scoped
940
- * graph.describe({ filter: { status: "errored" } }) // only errored nodes
941
- * graph.describe({ filter: (n) => n.type === "state" }) // predicate filter
942
- * ```
943
- */
944
- describe(options) {
945
- const actor = options?.actor;
946
- const filter = options?.filter;
947
- const includeFields = resolveDescribeFields(options?.detail, options?.fields);
948
- const isSpec = options?.format === "spec";
949
- const effectiveFields = isSpec ? resolveDescribeFields("minimal") : includeFields;
950
- const targets = [];
951
- this._collectObserveTargets("", targets);
952
- const nodeToPath = /* @__PURE__ */ new Map();
953
- for (const [p, n] of targets) {
954
- nodeToPath.set(n, p);
955
- }
956
- const nodes = {};
957
- for (const [p, n] of targets) {
958
- if (actor != null && !n.allowsObserve(actor)) continue;
959
- const raw = describeNode(n, effectiveFields);
960
- const deps = n instanceof NodeImpl ? n._deps.map((d) => nodeToPath.get(d) ?? d.name ?? "") : [];
961
- const { name: _name, ...rest } = raw;
962
- const entry = { ...rest, deps };
963
- if (filter != null) {
964
- if (typeof filter === "function") {
965
- const fn = filter;
966
- const pass = fn.length >= 2 ? fn(p, entry) : fn(entry);
967
- if (!pass) continue;
968
- } else {
969
- let match = true;
970
- for (const [fk, fv] of Object.entries(filter)) {
971
- const normalizedKey = fk === "deps_includes" ? "depsIncludes" : fk === "meta_has" ? "metaHas" : fk;
972
- if (normalizedKey === "depsIncludes") {
973
- if (!entry.deps.includes(String(fv))) {
974
- match = false;
975
- break;
976
- }
977
- continue;
978
- }
979
- if (normalizedKey === "metaHas") {
980
- if (!Object.hasOwn(entry.meta ?? {}, String(fv))) {
981
- match = false;
982
- break;
983
- }
984
- continue;
985
- }
986
- if (entry[normalizedKey] !== fv) {
987
- match = false;
988
- break;
989
- }
990
- }
991
- if (!match) continue;
992
- }
993
- }
994
- nodes[p] = entry;
995
- }
996
- const nodeKeys = new Set(Object.keys(nodes));
997
- let edges = this._collectAllEdges("");
998
- if (actor != null || filter != null) {
999
- edges = edges.filter((e) => nodeKeys.has(e.from) && nodeKeys.has(e.to));
1000
- }
1001
- edges.sort((a, b) => {
1002
- if (a.from < b.from) return -1;
1003
- if (a.from > b.from) return 1;
1004
- if (a.to < b.to) return -1;
1005
- if (a.to > b.to) return 1;
1006
- return 0;
1007
- });
1008
- const allSubgraphs = this._collectSubgraphs("");
1009
- const subgraphs = actor != null || filter != null ? allSubgraphs.filter((sg) => {
1010
- const prefix = `${sg}${PATH_SEP}`;
1011
- return [...nodeKeys].some((k) => k === sg || k.startsWith(prefix));
1012
- }) : allSubgraphs;
1013
- const graph = this;
1014
- const baseOpts = options;
1015
- return {
1016
- name: this.name,
1017
- nodes,
1018
- edges,
1019
- subgraphs,
1020
- expand(detailOrFields) {
1021
- const merged = { ...baseOpts, format: void 0 };
1022
- if (Array.isArray(detailOrFields)) {
1023
- merged.fields = detailOrFields;
1024
- merged.detail = void 0;
1025
- } else {
1026
- merged.detail = detailOrFields;
1027
- merged.fields = void 0;
1028
- }
1029
- return graph.describe(merged);
1030
- }
1031
- };
1032
- }
1033
- _collectSubgraphs(prefix) {
1034
- const out = [];
1035
- for (const m of [...this._mounts.keys()].sort()) {
1036
- const q = prefix === "" ? m : `${prefix}${m}`;
1037
- out.push(q);
1038
- out.push(...this._mounts.get(m)._collectSubgraphs(`${q}${PATH_SEP}`));
1039
- }
1040
- return out;
1041
- }
1042
- _collectAllEdges(prefix) {
1043
- const out = [];
1044
- for (const m of [...this._mounts.keys()].sort()) {
1045
- const p2 = prefix === "" ? m : `${prefix}${PATH_SEP}${m}`;
1046
- out.push(...this._mounts.get(m)._collectAllEdges(p2));
1047
- }
1048
- for (const [f, t] of this.edges()) {
1049
- out.push({
1050
- from: this._qualifyEdgeEndpoint(f, prefix),
1051
- to: this._qualifyEdgeEndpoint(t, prefix)
1052
- });
1053
- }
1054
- return out;
1055
- }
1056
- /**
1057
- * Snapshot-based resource profile: per-node stats, orphan effect detection,
1058
- * memory hotspots. Zero runtime overhead — walks nodes on demand.
1059
- *
1060
- * @param opts - Optional `topN` for hotspot limit (default 10).
1061
- * @returns Aggregate profile with per-node details, hotspots, and orphan effects.
1062
- */
1063
- resourceProfile(opts) {
1064
- return graphProfile(this, opts);
1065
- }
1066
- _qualifyEdgeEndpoint(part, prefix) {
1067
- if (part.includes(PATH_SEP)) return part;
1068
- return prefix === "" ? part : `${prefix}${PATH_SEP}${part}`;
1069
- }
1070
- _collectObserveTargets(prefix, out) {
1071
- for (const m of [...this._mounts.keys()].sort()) {
1072
- const p2 = prefix === "" ? m : `${prefix}${PATH_SEP}${m}`;
1073
- this._mounts.get(m)._collectObserveTargets(p2, out);
1074
- }
1075
- for (const loc of [...this._nodes.keys()].sort()) {
1076
- const n = this._nodes.get(loc);
1077
- const p = prefix === "" ? loc : `${prefix}${PATH_SEP}${loc}`;
1078
- out.push([p, n]);
1079
- this._appendMetaObserveTargets(p, n, out);
1080
- }
1081
- }
1082
- _appendMetaObserveTargets(basePath, n, out) {
1083
- for (const mk of Object.keys(n.meta).sort()) {
1084
- const m = n.meta[mk];
1085
- const mp = `${basePath}${PATH_SEP}${GRAPH_META_SEGMENT}${PATH_SEP}${mk}`;
1086
- out.push([mp, m]);
1087
- this._appendMetaObserveTargets(mp, m, out);
1088
- }
1089
- }
1090
- observe(pathOrOpts, options) {
1091
- if (typeof pathOrOpts === "string") {
1092
- const path = pathOrOpts;
1093
- const resolved = resolveObserveDetail(options);
1094
- const actor2 = resolved.actor;
1095
- const target = this.resolve(path);
1096
- if (actor2 != null && !target.allowsObserve(actor2)) {
1097
- throw new GuardDenied({ actor: actor2, action: "observe", nodeName: path });
1098
- }
1099
- const wantsStructured2 = resolved.structured === true || resolved.timeline === true || resolved.causal === true || resolved.derived === true || resolved.detail === "minimal" || resolved.detail === "full" || resolved.format != null;
1100
- if (wantsStructured2) {
1101
- const result = _Graph.inspectorEnabled ? this._createObserveResult(path, target, resolved) : this._createFallbackObserveResult(path, resolved);
1102
- if (resolved.format != null) {
1103
- this._attachFormatLogger(result, resolved);
1104
- }
1105
- return result;
1106
- }
1107
- return {
1108
- subscribe(sink) {
1109
- return target.subscribe(sink);
1110
- },
1111
- up(messages) {
1112
- try {
1113
- target.up?.(messages);
1114
- } catch (err) {
1115
- if (err instanceof GuardDenied) return;
1116
- throw err;
1117
- }
1118
- }
1119
- };
1120
- }
1121
- const opts = resolveObserveDetail(pathOrOpts);
1122
- const actor = opts.actor;
1123
- const wantsStructured = opts.structured === true || opts.timeline === true || opts.causal === true || opts.derived === true || opts.detail === "minimal" || opts.detail === "full" || opts.format != null;
1124
- if (wantsStructured) {
1125
- const result = _Graph.inspectorEnabled ? this._createObserveResultForAll(opts) : this._createFallbackObserveResultForAll(opts);
1126
- if (opts.format != null) {
1127
- this._attachFormatLogger(result, opts);
1128
- }
1129
- return result;
1130
- }
1131
- return {
1132
- subscribe: (sink) => {
1133
- const targets = [];
1134
- this._collectObserveTargets("", targets);
1135
- targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
1136
- const picked = actor == null ? targets : targets.filter(([, nd]) => nd.allowsObserve(actor));
1137
- const unsubs = picked.map(
1138
- ([p, nd]) => nd.subscribe((msgs) => {
1139
- sink(p, msgs);
1140
- })
1141
- );
1142
- return () => {
1143
- for (const u of unsubs) u();
1144
- };
1145
- },
1146
- up: (upPath, messages) => {
1147
- try {
1148
- const nd = this.resolve(upPath);
1149
- nd.up?.(messages);
1150
- } catch (err) {
1151
- if (err instanceof GuardDenied) return;
1152
- throw err;
1153
- }
1154
- }
1155
- };
1156
- }
1157
- _createObserveResult(path, target, options) {
1158
- const timeline = options.timeline === true;
1159
- const causal = options.causal === true;
1160
- const derived = options.derived === true;
1161
- const minimal = options.detail === "minimal";
1162
- const result = {
1163
- values: {},
1164
- dirtyCount: 0,
1165
- resolvedCount: 0,
1166
- events: [],
1167
- anyCompletedCleanly: false,
1168
- anyErrored: false
1169
- };
1170
- let lastTriggerDepIndex;
1171
- let lastRunDepValues;
1172
- let detachInspectorHook;
1173
- let batchSeq = 0;
1174
- if ((causal || derived) && target instanceof NodeImpl) {
1175
- detachInspectorHook = target._setInspectorHook((event) => {
1176
- if (event.kind === "dep_message") {
1177
- lastTriggerDepIndex = event.depIndex;
1178
- return;
1179
- }
1180
- lastRunDepValues = [...event.depValues];
1181
- if (derived) {
1182
- result.events.push({
1183
- type: "derived",
1184
- path,
1185
- dep_values: [...event.depValues],
1186
- ...timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {}
1187
- });
1188
- }
1189
- });
1190
- }
1191
- const unsub = target.subscribe((msgs) => {
1192
- batchSeq++;
1193
- for (const m of msgs) {
1194
- const t = m[0];
1195
- const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
1196
- const withCausal = causal && lastRunDepValues != null ? (() => {
1197
- const triggerDep = lastTriggerDepIndex != null && lastTriggerDepIndex >= 0 && target instanceof NodeImpl ? target._deps[lastTriggerDepIndex] : void 0;
1198
- const tv = triggerDep?.v;
1199
- return {
1200
- trigger_dep_index: lastTriggerDepIndex,
1201
- trigger_dep_name: triggerDep?.name,
1202
- ...tv != null ? { trigger_version: { id: tv.id, version: tv.version } } : {},
1203
- dep_values: [...lastRunDepValues]
1204
- };
1205
- })() : {};
1206
- if (t === DATA) {
1207
- result.values[path] = m[1];
1208
- result.events.push({ type: "data", path, data: m[1], ...base, ...withCausal });
1209
- } else if (minimal) {
1210
- if (t === DIRTY) result.dirtyCount++;
1211
- else if (t === RESOLVED) result.resolvedCount++;
1212
- else if (t === COMPLETE && !result.anyErrored) result.anyCompletedCleanly = true;
1213
- else if (t === ERROR) result.anyErrored = true;
1214
- } else if (t === DIRTY) {
1215
- result.dirtyCount++;
1216
- result.events.push({ type: "dirty", path, ...base });
1217
- } else if (t === RESOLVED) {
1218
- result.resolvedCount++;
1219
- result.events.push({ type: "resolved", path, ...base, ...withCausal });
1220
- } else if (t === COMPLETE) {
1221
- if (!result.anyErrored) result.anyCompletedCleanly = true;
1222
- result.events.push({ type: "complete", path, ...base });
1223
- } else if (t === ERROR) {
1224
- result.anyErrored = true;
1225
- result.events.push({ type: "error", path, data: m[1], ...base });
1226
- }
1227
- }
1228
- });
1229
- const graph = this;
1230
- const basePath = path;
1231
- return {
1232
- get values() {
1233
- return result.values;
1234
- },
1235
- get dirtyCount() {
1236
- return result.dirtyCount;
1237
- },
1238
- get resolvedCount() {
1239
- return result.resolvedCount;
1240
- },
1241
- get events() {
1242
- return result.events;
1243
- },
1244
- get anyCompletedCleanly() {
1245
- return result.anyCompletedCleanly;
1246
- },
1247
- get anyErrored() {
1248
- return result.anyErrored;
1249
- },
1250
- get completedWithoutErrors() {
1251
- return result.anyCompletedCleanly && !result.anyErrored;
1252
- },
1253
- dispose() {
1254
- unsub();
1255
- detachInspectorHook?.();
1256
- },
1257
- expand(extra) {
1258
- unsub();
1259
- detachInspectorHook?.();
1260
- const merged = { ...options };
1261
- if (typeof extra === "string") {
1262
- merged.detail = extra;
1263
- } else {
1264
- Object.assign(merged, extra);
1265
- }
1266
- const resolvedTarget = graph.resolve(basePath);
1267
- const expanded = graph._createObserveResult(
1268
- basePath,
1269
- resolvedTarget,
1270
- resolveObserveDetail(merged)
1271
- );
1272
- if (merged.format != null) {
1273
- graph._attachFormatLogger(expanded, merged);
1274
- }
1275
- return expanded;
1276
- }
1277
- };
1278
- }
1279
- _createObserveResultForAll(options) {
1280
- const timeline = options.timeline === true;
1281
- const minimal = options.detail === "minimal";
1282
- const result = {
1283
- values: {},
1284
- dirtyCount: 0,
1285
- resolvedCount: 0,
1286
- events: [],
1287
- anyCompletedCleanly: false,
1288
- anyErrored: false
1289
- };
1290
- const nodeErrored = /* @__PURE__ */ new Set();
1291
- const actor = options.actor;
1292
- const targets = [];
1293
- this._collectObserveTargets("", targets);
1294
- targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
1295
- const picked = actor == null ? targets : targets.filter(([, nd]) => nd.allowsObserve(actor));
1296
- let batchSeq = 0;
1297
- const unsubs = picked.map(
1298
- ([path, nd]) => nd.subscribe((msgs) => {
1299
- batchSeq++;
1300
- for (const m of msgs) {
1301
- const t = m[0];
1302
- const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
1303
- if (t === DATA) {
1304
- result.values[path] = m[1];
1305
- result.events.push({ type: "data", path, data: m[1], ...base });
1306
- } else if (minimal) {
1307
- if (t === DIRTY) result.dirtyCount++;
1308
- else if (t === RESOLVED) result.resolvedCount++;
1309
- else if (t === COMPLETE && !nodeErrored.has(path)) result.anyCompletedCleanly = true;
1310
- else if (t === ERROR) {
1311
- result.anyErrored = true;
1312
- nodeErrored.add(path);
1313
- }
1314
- } else if (t === DIRTY) {
1315
- result.dirtyCount++;
1316
- result.events.push({ type: "dirty", path, ...base });
1317
- } else if (t === RESOLVED) {
1318
- result.resolvedCount++;
1319
- result.events.push({ type: "resolved", path, ...base });
1320
- } else if (t === COMPLETE) {
1321
- if (!nodeErrored.has(path)) result.anyCompletedCleanly = true;
1322
- result.events.push({ type: "complete", path, ...base });
1323
- } else if (t === ERROR) {
1324
- result.anyErrored = true;
1325
- nodeErrored.add(path);
1326
- result.events.push({ type: "error", path, data: m[1], ...base });
1327
- }
1328
- }
1329
- })
1330
- );
1331
- const graph = this;
1332
- return {
1333
- get values() {
1334
- return result.values;
1335
- },
1336
- get dirtyCount() {
1337
- return result.dirtyCount;
1338
- },
1339
- get resolvedCount() {
1340
- return result.resolvedCount;
1341
- },
1342
- get events() {
1343
- return result.events;
1344
- },
1345
- get anyCompletedCleanly() {
1346
- return result.anyCompletedCleanly;
1347
- },
1348
- get anyErrored() {
1349
- return result.anyErrored;
1350
- },
1351
- get completedWithoutErrors() {
1352
- return result.anyCompletedCleanly && !result.anyErrored;
1353
- },
1354
- dispose() {
1355
- for (const u of unsubs) u();
1356
- },
1357
- expand(extra) {
1358
- for (const u of unsubs) u();
1359
- const merged = { ...options };
1360
- if (typeof extra === "string") {
1361
- merged.detail = extra;
1362
- } else {
1363
- Object.assign(merged, extra);
1364
- }
1365
- const expanded = graph._createObserveResultForAll(resolveObserveDetail(merged));
1366
- if (merged.format != null) {
1367
- graph._attachFormatLogger(expanded, merged);
1368
- }
1369
- return expanded;
1370
- }
1371
- };
1372
- }
1373
- /**
1374
- * Fallback ObserveResult for single-node when inspector is disabled but `format` is requested.
1375
- * Subscribes to raw messages and accumulates events with timeline info.
1376
- */
1377
- _createFallbackObserveResult(path, options) {
1378
- const timeline = options.timeline !== false;
1379
- const acc = {
1380
- values: {},
1381
- dirtyCount: 0,
1382
- resolvedCount: 0,
1383
- events: [],
1384
- anyCompletedCleanly: false,
1385
- anyErrored: false
1386
- };
1387
- const target = this.resolve(path);
1388
- let batchSeq = 0;
1389
- const unsub = target.subscribe((msgs) => {
1390
- batchSeq++;
1391
- for (const m of msgs) {
1392
- const t = m[0];
1393
- const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
1394
- if (t === DATA) {
1395
- acc.values[path] = m[1];
1396
- acc.events.push({ type: "data", path, data: m[1], ...base });
1397
- } else if (t === DIRTY) {
1398
- acc.dirtyCount++;
1399
- acc.events.push({ type: "dirty", path, ...base });
1400
- } else if (t === RESOLVED) {
1401
- acc.resolvedCount++;
1402
- acc.events.push({ type: "resolved", path, ...base });
1403
- } else if (t === COMPLETE) {
1404
- if (!acc.anyErrored) acc.anyCompletedCleanly = true;
1405
- acc.events.push({ type: "complete", path, ...base });
1406
- } else if (t === ERROR) {
1407
- acc.anyErrored = true;
1408
- acc.events.push({ type: "error", path, data: m[1], ...base });
1409
- }
1410
- }
1411
- });
1412
- return {
1413
- get values() {
1414
- return acc.values;
1415
- },
1416
- get dirtyCount() {
1417
- return acc.dirtyCount;
1418
- },
1419
- get resolvedCount() {
1420
- return acc.resolvedCount;
1421
- },
1422
- get events() {
1423
- return acc.events;
1424
- },
1425
- get anyCompletedCleanly() {
1426
- return acc.anyCompletedCleanly;
1427
- },
1428
- get anyErrored() {
1429
- return acc.anyErrored;
1430
- },
1431
- get completedWithoutErrors() {
1432
- return acc.anyCompletedCleanly && !acc.anyErrored;
1433
- },
1434
- dispose() {
1435
- unsub();
1436
- },
1437
- expand() {
1438
- throw new Error("expand() requires inspector mode (Graph.inspectorEnabled = true)");
1439
- }
1440
- };
1441
- }
1442
- /**
1443
- * Fallback ObserveResult for graph-wide when inspector is disabled but `format` is requested.
1444
- */
1445
- _createFallbackObserveResultForAll(options) {
1446
- const timeline = options.timeline !== false;
1447
- const actor = options.actor;
1448
- const acc = {
1449
- values: {},
1450
- dirtyCount: 0,
1451
- resolvedCount: 0,
1452
- events: [],
1453
- anyCompletedCleanly: false,
1454
- anyErrored: false
1455
- };
1456
- const nodeErrored = /* @__PURE__ */ new Set();
1457
- const targets = [];
1458
- this._collectObserveTargets("", targets);
1459
- targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
1460
- const picked = actor == null ? targets : targets.filter(([, nd]) => nd.allowsObserve(actor));
1461
- let batchSeq = 0;
1462
- const unsubs = picked.map(
1463
- ([path, nd]) => nd.subscribe((msgs) => {
1464
- batchSeq++;
1465
- for (const m of msgs) {
1466
- const t = m[0];
1467
- const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
1468
- if (t === DATA) {
1469
- acc.values[path] = m[1];
1470
- acc.events.push({ type: "data", path, data: m[1], ...base });
1471
- } else if (t === DIRTY) {
1472
- acc.dirtyCount++;
1473
- acc.events.push({ type: "dirty", path, ...base });
1474
- } else if (t === RESOLVED) {
1475
- acc.resolvedCount++;
1476
- acc.events.push({ type: "resolved", path, ...base });
1477
- } else if (t === COMPLETE) {
1478
- if (!nodeErrored.has(path)) acc.anyCompletedCleanly = true;
1479
- acc.events.push({ type: "complete", path, ...base });
1480
- } else if (t === ERROR) {
1481
- acc.anyErrored = true;
1482
- nodeErrored.add(path);
1483
- acc.events.push({ type: "error", path, data: m[1], ...base });
1484
- }
1485
- }
1486
- })
1487
- );
1488
- return {
1489
- get values() {
1490
- return acc.values;
1491
- },
1492
- get dirtyCount() {
1493
- return acc.dirtyCount;
1494
- },
1495
- get resolvedCount() {
1496
- return acc.resolvedCount;
1497
- },
1498
- get events() {
1499
- return acc.events;
1500
- },
1501
- get anyCompletedCleanly() {
1502
- return acc.anyCompletedCleanly;
1503
- },
1504
- get anyErrored() {
1505
- return acc.anyErrored;
1506
- },
1507
- get completedWithoutErrors() {
1508
- return acc.anyCompletedCleanly && !acc.anyErrored;
1509
- },
1510
- dispose() {
1511
- for (const u of unsubs) u();
1512
- },
1513
- expand() {
1514
- throw new Error("expand() requires inspector mode (Graph.inspectorEnabled = true)");
1515
- }
1516
- };
1517
- }
1518
- /**
1519
- * Attaches a format logger to an ObserveResult, rendering events as they arrive.
1520
- * Wraps the result's dispose to flush pending events.
1521
- */
1522
- _attachFormatLogger(result, options) {
1523
- const format = options.format;
1524
- const logger = options.logger ?? ((line) => console.log(line));
1525
- const include = options.includeTypes ? new Set(options.includeTypes) : null;
1526
- const exclude = options.excludeTypes ? new Set(options.excludeTypes) : null;
1527
- const theme = resolveObserveTheme(options.theme);
1528
- const shouldLog = (type) => {
1529
- if (include?.has(type) === false) return false;
1530
- if (exclude?.has(type) === true) return false;
1531
- return true;
1532
- };
1533
- const renderEvent = (event) => {
1534
- if (format === "json") {
1535
- try {
1536
- return JSON.stringify(event);
1537
- } catch {
1538
- return JSON.stringify({
1539
- type: event.type,
1540
- path: event.path,
1541
- data: "[unserializable]"
1542
- });
1543
- }
1544
- }
1545
- const color = theme[event.type] ?? "";
1546
- const pathPart = event.path ? `${theme.path}${event.path}${theme.reset} ` : "";
1547
- const dataPart = event.data !== void 0 ? ` ${describeData(event.data)}` : "";
1548
- const triggerPart = event.trigger_dep_name != null ? ` <- ${event.trigger_dep_name}` : event.trigger_dep_index != null ? ` <- #${event.trigger_dep_index}` : "";
1549
- const batchPart = event.in_batch ? " [batch]" : "";
1550
- return `${pathPart}${color}${event.type.toUpperCase()}${theme.reset}${dataPart}${triggerPart}${batchPart}`;
1551
- };
1552
- let cursor = 0;
1553
- const flush = () => {
1554
- const events = result.events;
1555
- while (cursor < events.length) {
1556
- const event = events[cursor++];
1557
- if (shouldLog(event.type)) {
1558
- logger(renderEvent(event), event);
1559
- }
1560
- }
1561
- };
1562
- const origPush = result.events.push;
1563
- result.events.push = function(...items) {
1564
- const ret = origPush.apply(this, items);
1565
- flush();
1566
- return ret;
1567
- };
1568
- const origDispose = result.dispose.bind(result);
1569
- result.dispose = () => {
1570
- origDispose();
1571
- flush();
1572
- };
1573
- }
1574
- /**
1575
- * CLI/debug-friendly graph dump built on {@link Graph.describe}.
1576
- *
1577
- * @param options - Optional actor/filter/format toggles.
1578
- * @returns Rendered graph text.
1579
- */
1580
- dumpGraph(options = {}) {
1581
- const { expand: _, ...described } = this.describe({
1582
- actor: options.actor,
1583
- filter: options.filter,
1584
- detail: "standard"
1585
- });
1586
- const includeEdges = options.includeEdges ?? true;
1587
- const includeSubgraphs = options.includeSubgraphs ?? true;
1588
- if (options.format === "json") {
1589
- const payload = {
1590
- ...described,
1591
- edges: includeEdges ? described.edges : [],
1592
- subgraphs: includeSubgraphs ? described.subgraphs : []
1593
- };
1594
- const text2 = JSON.stringify(sortJsonValue(payload), null, options.indent ?? 2);
1595
- options.logger?.(text2);
1596
- return text2;
1597
- }
1598
- const lines = [];
1599
- lines.push(`Graph ${described.name}`);
1600
- lines.push("Nodes:");
1601
- for (const path of Object.keys(described.nodes).sort()) {
1602
- const n = described.nodes[path];
1603
- lines.push(`- ${path} (${n.type}/${n.status}): ${describeData(n.value)}`);
1604
- }
1605
- if (includeEdges) {
1606
- lines.push("Edges:");
1607
- for (const edge of described.edges) {
1608
- lines.push(`- ${edge.from} -> ${edge.to}`);
1609
- }
1610
- }
1611
- if (includeSubgraphs) {
1612
- lines.push("Subgraphs:");
1613
- for (const sg of described.subgraphs) {
1614
- lines.push(`- ${sg}`);
1615
- }
1616
- }
1617
- const text = lines.join("\n");
1618
- options.logger?.(text);
1619
- return text;
1620
- }
1621
- // ——————————————————————————————————————————————————————————————
1622
- // Lifecycle & persistence (§3.7–§3.8)
1623
- // ——————————————————————————————————————————————————————————————
1624
- /**
1625
- * Register a cleanup function to be called on {@link Graph.destroy}.
1626
- *
1627
- * Factories use this to attach teardown logic for internal nodes, keepalive
1628
- * subscriptions, or other resources that are not registered on the graph and
1629
- * would otherwise leak on repeated create/destroy cycles.
1630
- *
1631
- * Returns a removal function — call it to unregister the disposer early.
1632
- */
1633
- addDisposer(fn) {
1634
- this._disposers.add(fn);
1635
- return () => {
1636
- this._disposers.delete(fn);
1637
- };
1638
- }
1639
- /**
1640
- * Drains disposers (registered via {@link addDisposer}), then sends `[[TEARDOWN]]` to all
1641
- * nodes and clears registries on this graph and every mounted subgraph (§3.7).
1642
- * The instance is left empty and may be reused with {@link Graph.add}.
1643
- */
1644
- destroy() {
1645
- for (const dispose of [...this._disposers]) {
1646
- try {
1647
- dispose();
1648
- } catch {
1649
- }
1650
- }
1651
- this._disposers.clear();
1652
- this.signal([[TEARDOWN]], { internal: true });
1653
- for (const dispose of [...this._autoCheckpointDisposers]) {
1654
- try {
1655
- dispose();
1656
- } catch {
1657
- }
1658
- }
1659
- this._autoCheckpointDisposers.clear();
1660
- for (const child of [...this._mounts.values()]) {
1661
- child._destroyClearOnly();
1662
- }
1663
- this._mounts.clear();
1664
- this._nodes.clear();
1665
- this._edges.clear();
1666
- }
1667
- /** Clear structure after parent already signaled TEARDOWN through this subtree. */
1668
- _destroyClearOnly() {
1669
- for (const child of [...this._mounts.values()]) {
1670
- child._destroyClearOnly();
1671
- }
1672
- this._mounts.clear();
1673
- this._nodes.clear();
1674
- this._edges.clear();
1675
- }
1676
- /**
1677
- * Serializes structure and current values to JSON-shaped data (§3.8). Same information
1678
- * as {@link Graph.describe} plus a `version` field for format evolution.
1679
- *
1680
- * @returns Persistable snapshot with sorted keys.
1681
- */
1682
- snapshot() {
1683
- const { expand: _, ...d } = this.describe({ detail: "full" });
1684
- const sortedNodes = {};
1685
- for (const key of Object.keys(d.nodes).sort()) {
1686
- const { lastMutation: _lm, guard: _g, ...node } = d.nodes[key];
1687
- sortedNodes[key] = node;
1688
- }
1689
- const sortedSubgraphs = [...d.subgraphs].sort();
1690
- return { ...d, version: 1, nodes: sortedNodes, subgraphs: sortedSubgraphs };
1691
- }
1692
- /**
1693
- * Apply persisted values onto an existing graph whose topology matches the snapshot
1694
- * (§3.8). Only {@link DescribeNodeOutput.type} `state` and `producer` entries with a
1695
- * `value` field are written; `derived` / `operator` / `effect` are skipped so deps
1696
- * drive recomputation. Unknown paths are ignored.
1697
- *
1698
- * @param data - Snapshot envelope with matching `name` and node slices.
1699
- * @throws If `data.name` does not equal {@link Graph.name}.
1700
- */
1701
- restore(data, options) {
1702
- parseSnapshotEnvelope(data);
1703
- if (data.name !== this.name) {
1704
- throw new Error(
1705
- `Graph "${this.name}": restore snapshot name "${data.name}" does not match this graph`
1706
- );
1707
- }
1708
- const onlyPatterns = options?.only == null ? null : (Array.isArray(options.only) ? options.only : [options.only]).map((p) => globToRegex(p));
1709
- for (const path of Object.keys(data.nodes).sort()) {
1710
- if (onlyPatterns !== null && !onlyPatterns.some((re) => re.test(path))) continue;
1711
- const slice = data.nodes[path];
1712
- if (slice === void 0 || slice.value === void 0) continue;
1713
- if (slice.type === "derived" || slice.type === "operator" || slice.type === "effect") {
1714
- continue;
1715
- }
1716
- try {
1717
- this.set(path, slice.value);
1718
- } catch {
1719
- }
1720
- }
1721
- }
1722
- /**
1723
- * Creates a graph named from the snapshot, optionally runs `build` to register nodes
1724
- * and mounts, then {@link Graph.restore} values (§3.8).
1725
- *
1726
- * @param data - Snapshot envelope (`version` checked).
1727
- * @param build - Optional callback to construct topology before values are applied.
1728
- * @returns Hydrated `Graph` instance.
1729
- */
1730
- static fromSnapshot(data, build) {
1731
- parseSnapshotEnvelope(data);
1732
- const g = new _Graph(data.name);
1733
- if (build) {
1734
- build(g);
1735
- g.restore(data);
1736
- return g;
1737
- }
1738
- for (const mount of [...data.subgraphs].sort((a, b) => {
1739
- const da = a.split(PATH_SEP).length;
1740
- const db = b.split(PATH_SEP).length;
1741
- if (da !== db) return da - db;
1742
- if (a < b) return -1;
1743
- if (a > b) return 1;
1744
- return 0;
1745
- })) {
1746
- const parts = mount.split(PATH_SEP);
1747
- let target = g;
1748
- for (const seg of parts) {
1749
- if (!target._mounts.has(seg)) {
1750
- target.mount(seg, new _Graph(seg));
1751
- }
1752
- target = target._mounts.get(seg);
1753
- }
1754
- }
1755
- 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);
1756
- const pending = new Map(primaryEntries);
1757
- const created = /* @__PURE__ */ new Map();
1758
- let progressed = true;
1759
- while (pending.size > 0 && progressed) {
1760
- progressed = false;
1761
- for (const [path, slice] of [...pending.entries()]) {
1762
- const deps = slice?.deps ?? [];
1763
- if (!deps.every((dep) => created.has(dep))) continue;
1764
- const [owner, localName] = _Graph._ownerForPath(g, path);
1765
- const meta = { ...slice?.meta ?? {} };
1766
- const factory = _Graph._factoryForPath(path);
1767
- let node;
1768
- if (slice?.type === "state") {
1769
- node = state(slice.value, { meta });
1770
- } else {
1771
- if (factory == null) continue;
1772
- node = factory(localName, {
1773
- path,
1774
- type: slice.type,
1775
- value: slice.value,
1776
- meta,
1777
- deps,
1778
- resolvedDeps: deps.map((dep) => created.get(dep))
1779
- });
1780
- }
1781
- owner.add(localName, node);
1782
- created.set(path, node);
1783
- pending.delete(path);
1784
- progressed = true;
1785
- }
1786
- }
1787
- if (pending.size > 0) {
1788
- const unresolved = [...pending.keys()].sort().join(", ");
1789
- throw new Error(
1790
- `Graph.fromSnapshot could not reconstruct nodes without build callback: ${unresolved}. Register matching factories with Graph.registerFactory(pattern, factory).`
1791
- );
1792
- }
1793
- for (const edge of data.edges) {
1794
- try {
1795
- g.connect(edge.from, edge.to);
1796
- } catch {
1797
- }
1798
- }
1799
- g.restore(data);
1800
- return g;
1801
- }
1802
- /**
1803
- * Plain snapshot object with **recursively sorted object keys** for deterministic serialization (§3.8).
1804
- *
1805
- * @remarks
1806
- * For a single UTF-8 string with a trailing newline (convenient for git), use {@link Graph.toJSONString}.
1807
- *
1808
- * @returns Same object as {@link Graph.snapshot}.
1809
- */
1810
- toObject() {
1811
- return this.snapshot();
1812
- }
1813
- /**
1814
- * ECMAScript `JSON.stringify` hook — delegates to {@link Graph.toObject}.
1815
- *
1816
- * @remarks
1817
- * Must return a plain object (not a string) so `JSON.stringify(graph)` works correctly
1818
- * without double-encoding.
1819
- */
1820
- toJSON() {
1821
- return this.toObject();
1822
- }
1823
- /**
1824
- * Deterministic JSON **text**: `JSON.stringify` of {@link Graph.toObject} plus a trailing newline (§3.8).
1825
- *
1826
- * @returns Stable string suitable for diffs.
1827
- */
1828
- toJSONString() {
1829
- return stableJsonStringify(this.snapshot());
1830
- }
1831
- /**
1832
- * Debounced persistence wired to graph-wide observe stream (spec §3.8 auto-checkpoint).
1833
- *
1834
- * Checkpoint trigger uses {@link messageTier}: only batches containing tier >= 3 messages
1835
- * schedule a save (`DATA`/`RESOLVED`/terminal/destruction), never pure tier-0/1/2 control
1836
- * waves (`START`/`DIRTY`/`INVALIDATE`/`PAUSE`/`RESUME`).
1837
- */
1838
- autoCheckpoint(adapter, options = {}) {
1839
- const debounceMs = Math.max(0, options.debounceMs ?? 500);
1840
- const compactEvery = Math.max(1, options.compactEvery ?? 10);
1841
- let timer;
1842
- let seq = 0;
1843
- let pending = false;
1844
- let lastDescribe;
1845
- const flush = () => {
1846
- timer = void 0;
1847
- if (!pending) return;
1848
- pending = false;
1849
- try {
1850
- const { expand: _expand, ...raw } = this.describe({ detail: "full" });
1851
- const cleanNodes = {};
1852
- for (const [p, n] of Object.entries(raw.nodes)) {
1853
- const { lastMutation: _lm, guard: _g, ...node } = n;
1854
- cleanNodes[p] = node;
1855
- }
1856
- const described = { ...raw, nodes: cleanNodes };
1857
- const snapshot = { ...described, version: SNAPSHOT_VERSION };
1858
- seq += 1;
1859
- const shouldCompact = lastDescribe == null || seq % compactEvery === 0;
1860
- if (shouldCompact) {
1861
- adapter.save(this.name, { mode: "full", snapshot, seq });
1862
- } else {
1863
- const previous = lastDescribe;
1864
- if (previous == null) return;
1865
- adapter.save(this.name, {
1866
- mode: "diff",
1867
- diff: _Graph.diff(previous, described),
1868
- snapshot,
1869
- seq
1870
- });
1871
- }
1872
- lastDescribe = described;
1873
- } catch (error) {
1874
- options.onError?.(error);
1875
- }
1876
- };
1877
- const schedule = () => {
1878
- pending = true;
1879
- if (timer !== void 0) clearTimeout(timer);
1880
- timer = setTimeout(flush, debounceMs);
1881
- };
1882
- const off = this.observe().subscribe((path, messages) => {
1883
- const triggeredByTier = messages.some((m) => messageTier(m[0]) >= 3);
1884
- if (!triggeredByTier) return;
1885
- if (options.filter) {
1886
- const nd = this.resolve(path);
1887
- if (nd == null) return;
1888
- const described = describeNode(nd, resolveDescribeFields("standard"));
1889
- if (!options.filter(path, described)) return;
1890
- }
1891
- schedule();
1892
- });
1893
- const dispose = () => {
1894
- off();
1895
- if (timer !== void 0) {
1896
- clearTimeout(timer);
1897
- timer = void 0;
1898
- }
1899
- this._autoCheckpointDisposers.delete(dispose);
1900
- };
1901
- this._autoCheckpointDisposers.add(dispose);
1902
- return { dispose };
1903
- }
1904
- /**
1905
- * Export the current graph topology as Mermaid flowchart text.
1906
- *
1907
- * Renders qualified node paths and registered edges from {@link Graph.describe}.
1908
- *
1909
- * @param options - Optional diagram direction (`LR` by default).
1910
- * @returns Mermaid flowchart source.
1911
- */
1912
- toMermaid(options) {
1913
- const direction = normalizeDiagramDirection(options?.direction);
1914
- const described = this.describe();
1915
- const paths = Object.keys(described.nodes).sort();
1916
- const ids = /* @__PURE__ */ new Map();
1917
- for (let i = 0; i < paths.length; i += 1) {
1918
- ids.set(paths[i], `n${i}`);
1919
- }
1920
- const lines = [`flowchart ${direction}`];
1921
- for (const path of paths) {
1922
- const id = ids.get(path);
1923
- lines.push(` ${id}["${escapeMermaidLabel(path)}"]`);
1924
- }
1925
- for (const [from, to] of collectDiagramArrows(described)) {
1926
- const fromId = ids.get(from);
1927
- const toId = ids.get(to);
1928
- if (!fromId || !toId) continue;
1929
- lines.push(` ${fromId} --> ${toId}`);
1930
- }
1931
- return lines.join("\n");
1932
- }
1933
- /**
1934
- * Export the current graph topology as D2 diagram text.
1935
- *
1936
- * Renders qualified node paths, constructor deps, and registered edges from {@link Graph.describe}.
1937
- *
1938
- * @param options - Optional diagram direction (`LR` by default).
1939
- * @returns D2 source text.
1940
- */
1941
- toD2(options) {
1942
- const direction = normalizeDiagramDirection(options?.direction);
1943
- const described = this.describe();
1944
- const paths = Object.keys(described.nodes).sort();
1945
- const ids = /* @__PURE__ */ new Map();
1946
- for (let i = 0; i < paths.length; i += 1) {
1947
- ids.set(paths[i], `n${i}`);
1948
- }
1949
- const lines = [`direction: ${d2DirectionFromGraphDirection(direction)}`];
1950
- for (const path of paths) {
1951
- const id = ids.get(path);
1952
- lines.push(`${id}: "${escapeD2Label(path)}"`);
1953
- }
1954
- for (const [from, to] of collectDiagramArrows(described)) {
1955
- const fromId = ids.get(from);
1956
- const toId = ids.get(to);
1957
- if (!fromId || !toId) continue;
1958
- lines.push(`${fromId} -> ${toId}`);
1959
- }
1960
- return lines.join("\n");
1961
- }
1962
- // ——————————————————————————————————————————————————————————————
1963
- // Inspector (roadmap 3.3) — reasoning trace, overhead gating
1964
- // ——————————————————————————————————————————————————————————————
1965
- /**
1966
- * When `false`, structured observation options (`causal`, `timeline`),
1967
- * and `trace()` writes are no-ops. Raw `observe()` always works.
1968
- *
1969
- * Default: `true` outside production (`process.env.NODE_ENV !== "production"`).
1970
- */
1971
- static inspectorEnabled = !(typeof process !== "undefined" && process.env?.NODE_ENV === "production");
1972
- _annotations = /* @__PURE__ */ new Map();
1973
- _traceRing = new RingBuffer(1e3);
1974
- trace(path, reason) {
1975
- if (path != null && reason != null) {
1976
- if (!_Graph.inspectorEnabled) return;
1977
- this.resolve(path);
1978
- this._annotations.set(path, reason);
1979
- this._traceRing.push({ path, reason, timestamp_ns: monotonicNs() });
1980
- return;
1981
- }
1982
- if (!_Graph.inspectorEnabled) return [];
1983
- return this._traceRing.toArray();
1984
- }
1985
- /**
1986
- * Computes structural + value diff between two {@link Graph.describe} snapshots.
1987
- *
1988
- * @param a - Earlier describe output.
1989
- * @param b - Later describe output.
1990
- * @returns Added/removed nodes, changed fields, and edge deltas.
1991
- */
1992
- static diff(a, b) {
1993
- const aKeys = new Set(Object.keys(a.nodes));
1994
- const bKeys = new Set(Object.keys(b.nodes));
1995
- const nodesAdded = [...bKeys].filter((k) => !aKeys.has(k)).sort();
1996
- const nodesRemoved = [...aKeys].filter((k) => !bKeys.has(k)).sort();
1997
- const nodesChanged = [];
1998
- for (const key of aKeys) {
1999
- if (!bKeys.has(key)) continue;
2000
- const na = a.nodes[key];
2001
- const nb = b.nodes[key];
2002
- const av = na.v;
2003
- const bv = nb.v;
2004
- if (av != null && bv != null && av.id === bv.id && av.version === bv.version) {
2005
- for (const field of ["type", "status"]) {
2006
- const va = na[field];
2007
- const vb = nb[field];
2008
- if (va !== vb) {
2009
- nodesChanged.push({ path: key, field, from: va, to: vb });
2010
- }
2011
- }
2012
- continue;
2013
- }
2014
- for (const field of ["type", "status", "value"]) {
2015
- const va = na[field];
2016
- const vb = nb[field];
2017
- if (!Object.is(va, vb) && JSON.stringify(va) !== JSON.stringify(vb)) {
2018
- nodesChanged.push({ path: key, field, from: va, to: vb });
2019
- }
2020
- }
2021
- }
2022
- const edgeKey2 = (e) => `${e.from} ${e.to}`;
2023
- const aEdges = new Set(a.edges.map(edgeKey2));
2024
- const bEdges = new Set(b.edges.map(edgeKey2));
2025
- const edgesAdded = b.edges.filter((e) => !aEdges.has(edgeKey2(e)));
2026
- const edgesRemoved = a.edges.filter((e) => !bEdges.has(edgeKey2(e)));
2027
- const aSubgraphs = new Set(a.subgraphs);
2028
- const bSubgraphs = new Set(b.subgraphs);
2029
- const subgraphsAdded = [...bSubgraphs].filter((s) => !aSubgraphs.has(s)).sort();
2030
- const subgraphsRemoved = [...aSubgraphs].filter((s) => !bSubgraphs.has(s)).sort();
2031
- return {
2032
- nodesAdded,
2033
- nodesRemoved,
2034
- nodesChanged,
2035
- edgesAdded,
2036
- edgesRemoved,
2037
- subgraphsAdded,
2038
- subgraphsRemoved
2039
- };
2040
- }
2041
- };
2042
- function reachable(described, from, direction, options = {}) {
2043
- if (!from) return [];
2044
- if (direction !== "upstream" && direction !== "downstream") {
2045
- throw new Error(`reachable: direction must be "upstream" or "downstream"`);
2046
- }
2047
- const maxDepth = options.maxDepth;
2048
- if (maxDepth != null && (!Number.isInteger(maxDepth) || maxDepth < 0)) {
2049
- throw new Error(`reachable: maxDepth must be an integer >= 0`);
2050
- }
2051
- if (maxDepth === 0) return [];
2052
- const depsByPath = /* @__PURE__ */ new Map();
2053
- const reverseDeps = /* @__PURE__ */ new Map();
2054
- const incomingEdges = /* @__PURE__ */ new Map();
2055
- const outgoingEdges = /* @__PURE__ */ new Map();
2056
- const universe = /* @__PURE__ */ new Set();
2057
- const nodesRaw = described != null && typeof described === "object" && "nodes" in described && typeof described.nodes === "object" && described.nodes !== null && !Array.isArray(described.nodes) ? described.nodes : {};
2058
- const edgesRaw = described != null && typeof described === "object" && "edges" in described && Array.isArray(described.edges) ? described.edges : [];
2059
- for (const [path, node] of Object.entries(nodesRaw)) {
2060
- if (!path) continue;
2061
- universe.add(path);
2062
- const deps = node != null && typeof node === "object" && Array.isArray(node.deps) ? node.deps : [];
2063
- const cleanDeps = deps.filter((d) => typeof d === "string" && d.length > 0);
2064
- depsByPath.set(path, cleanDeps);
2065
- for (const dep of cleanDeps) {
2066
- universe.add(dep);
2067
- if (!reverseDeps.has(dep)) reverseDeps.set(dep, /* @__PURE__ */ new Set());
2068
- reverseDeps.get(dep).add(path);
2069
- }
2070
- }
2071
- for (const edge of edgesRaw) {
2072
- if (edge == null || typeof edge !== "object") continue;
2073
- const edgeFrom = "from" in edge && typeof edge.from === "string" ? edge.from : "";
2074
- const edgeTo = "to" in edge && typeof edge.to === "string" ? edge.to : "";
2075
- if (!edgeFrom || !edgeTo) continue;
2076
- universe.add(edgeFrom);
2077
- universe.add(edgeTo);
2078
- if (!outgoingEdges.has(edgeFrom)) outgoingEdges.set(edgeFrom, /* @__PURE__ */ new Set());
2079
- outgoingEdges.get(edgeFrom).add(edgeTo);
2080
- if (!incomingEdges.has(edgeTo)) incomingEdges.set(edgeTo, /* @__PURE__ */ new Set());
2081
- incomingEdges.get(edgeTo).add(edgeFrom);
2082
- }
2083
- if (!universe.has(from)) return [];
2084
- const neighbors = (path) => {
2085
- if (direction === "upstream") {
2086
- const depNeighbors2 = depsByPath.get(path) ?? [];
2087
- const edgeNeighbors2 = [...incomingEdges.get(path) ?? []];
2088
- return [...depNeighbors2, ...edgeNeighbors2];
2089
- }
2090
- const depNeighbors = [...reverseDeps.get(path) ?? []];
2091
- const edgeNeighbors = [...outgoingEdges.get(path) ?? []];
2092
- return [...depNeighbors, ...edgeNeighbors];
2093
- };
2094
- const visited = /* @__PURE__ */ new Set([from]);
2095
- const out = /* @__PURE__ */ new Set();
2096
- const queue = [{ path: from, depth: 0 }];
2097
- while (queue.length > 0) {
2098
- const next = queue.shift();
2099
- if (maxDepth != null && next.depth >= maxDepth) continue;
2100
- for (const nb of neighbors(next.path)) {
2101
- if (!nb || visited.has(nb)) continue;
2102
- visited.add(nb);
2103
- out.add(nb);
2104
- queue.push({ path: nb, depth: next.depth + 1 });
2105
- }
2106
- }
2107
- return [...out].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
2108
- }
2109
-
2110
- export {
2111
- sizeof,
2112
- graphProfile,
2113
- GRAPH_META_SEGMENT,
2114
- Graph,
2115
- reachable
2116
- };
2117
- //# sourceMappingURL=chunk-2UDLYZHT.js.map