@hediet/linkrpc-infra 0.0.1

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.
@@ -0,0 +1,734 @@
1
+ import { DEFAULT_RPC_TIMEOUT_MS, withRpcTimeout } from "@hediet/linkrpc";
2
+ import { nodeInterface, topologyInterface, trafficInterface } from "@hediet/linkrpc/inspection";
3
+ import { HubDirectoryExplorer } from "@hediet/linkrpc/hub/common";
4
+ //#region src/inspection/nodeInfoClient.ts
5
+ /** Typed convenience client for root and service-scoped node identity. */
6
+ var NodeInfoClient = class {
7
+ _connection;
8
+ constructor(_connection) {
9
+ this._connection = _connection;
10
+ }
11
+ getPeer() {
12
+ return this._connection.get(nodeInterface).getNodeId({});
13
+ }
14
+ getForService(serviceId) {
15
+ return this._connection.service(serviceId).get(nodeInterface).getNodeId({});
16
+ }
17
+ };
18
+ //#endregion
19
+ //#region src/inspection/topologyClient.ts
20
+ /** Snapshot and invalidation-watch client for one inspected service. */
21
+ var TopologyClient = class {
22
+ _connection;
23
+ serviceId;
24
+ _timeoutMs;
25
+ constructor(_connection, serviceId, _timeoutMs = DEFAULT_RPC_TIMEOUT_MS) {
26
+ this._connection = _connection;
27
+ this.serviceId = serviceId;
28
+ this._timeoutMs = _timeoutMs;
29
+ }
30
+ getGraph() {
31
+ const request = this._connection.service(this.serviceId).get(topologyInterface).getGraph({});
32
+ return withRpcTimeout(request, `topology service '${this.serviceId}'`, this._timeoutMs);
33
+ }
34
+ watch(callbacks) {
35
+ const client = this._connection.service(this.serviceId).get(topologyInterface);
36
+ let active = true;
37
+ let dirty = false;
38
+ let refreshing = false;
39
+ let refreshTail = Promise.resolve();
40
+ let readySettled = false;
41
+ let resolveReady;
42
+ let rejectReady;
43
+ const ready = new Promise((resolve, reject) => {
44
+ resolveReady = resolve;
45
+ rejectReady = reject;
46
+ });
47
+ const reportError = (error) => {
48
+ try {
49
+ callbacks.onError?.(error);
50
+ } catch {}
51
+ };
52
+ const requestRefresh = () => {
53
+ if (!active) return;
54
+ dirty = true;
55
+ if (refreshing) return;
56
+ refreshing = true;
57
+ refreshTail = (async () => {
58
+ while (active && dirty) {
59
+ dirty = false;
60
+ try {
61
+ const graph = await withRpcTimeout(client.getGraph({}), `topology service '${this.serviceId}'`, this._timeoutMs);
62
+ if (!active) break;
63
+ try {
64
+ callbacks.onGraph(graph);
65
+ } catch (error) {
66
+ reportError(error);
67
+ }
68
+ if (!readySettled) {
69
+ readySettled = true;
70
+ resolveReady(graph);
71
+ }
72
+ } catch (error) {
73
+ reportError(error);
74
+ if (!readySettled) {
75
+ readySettled = true;
76
+ rejectReady(error);
77
+ }
78
+ }
79
+ }
80
+ })().finally(() => {
81
+ refreshing = false;
82
+ if (active && dirty) requestRefresh();
83
+ });
84
+ };
85
+ const call = client.watchGraph({}, { onMessage: requestRefresh });
86
+ requestRefresh();
87
+ return {
88
+ ready,
89
+ done: (async () => {
90
+ try {
91
+ await call;
92
+ } catch (error) {
93
+ reportError(error);
94
+ throw error;
95
+ } finally {
96
+ active = false;
97
+ }
98
+ await refreshTail;
99
+ })(),
100
+ cancel: async (reason) => {
101
+ active = false;
102
+ if (!readySettled) {
103
+ readySettled = true;
104
+ rejectReady(new Error(reason ?? "Topology watch cancelled"));
105
+ }
106
+ try {
107
+ await call.cancel(reason);
108
+ } finally {
109
+ call.dispose?.(reason);
110
+ }
111
+ }
112
+ };
113
+ }
114
+ };
115
+ //#endregion
116
+ //#region src/inspection/trafficClient.ts
117
+ /** Traffic stream client for the endpoint node hosting one service. */
118
+ var TrafficClient = class {
119
+ _connection;
120
+ serviceId;
121
+ constructor(_connection, serviceId) {
122
+ this._connection = _connection;
123
+ this.serviceId = serviceId;
124
+ }
125
+ watch(options, callbacks) {
126
+ const call = this._connection.service(this.serviceId).get(trafficInterface).watch(withTrafficIgnoreKey(options), { onMessage: (event) => {
127
+ dispatchTrafficEvent(event, callbacks);
128
+ } });
129
+ call.catch((error) => reportTrafficError(callbacks, error));
130
+ return {
131
+ done: call,
132
+ cancel: async (reason) => {
133
+ try {
134
+ await call.cancel(reason);
135
+ await call;
136
+ } finally {
137
+ call.dispose?.(reason);
138
+ }
139
+ }
140
+ };
141
+ }
142
+ watchWithPayloads(options, callbacks) {
143
+ const call = this._connection.service(this.serviceId).get(trafficInterface).watchWithPayloads(withTrafficIgnoreKey(options), { onMessage: (event) => {
144
+ dispatchTrafficEvent(event, callbacks);
145
+ } });
146
+ call.catch((error) => reportTrafficError(callbacks, error));
147
+ return {
148
+ done: call,
149
+ cancel: async (reason) => {
150
+ try {
151
+ await call.cancel(reason);
152
+ await call;
153
+ } finally {
154
+ call.dispose?.(reason);
155
+ }
156
+ }
157
+ };
158
+ }
159
+ };
160
+ function withTrafficIgnoreKey(options) {
161
+ return {
162
+ ...options,
163
+ trafficIgnoreKey: options.trafficIgnoreKey ?? createTrafficIgnoreKey()
164
+ };
165
+ }
166
+ function createTrafficIgnoreKey() {
167
+ if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
168
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
169
+ globalThis.crypto.getRandomValues(bytes);
170
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
171
+ }
172
+ function dispatchTrafficEvent(event, callbacks) {
173
+ try {
174
+ if (event.type === "transit") callbacks.onTransit(event);
175
+ else callbacks.onOverflow?.(event);
176
+ } catch (error) {
177
+ reportTrafficError(callbacks, error);
178
+ }
179
+ }
180
+ function reportTrafficError(callbacks, error) {
181
+ try {
182
+ callbacks.onError?.(error);
183
+ } catch {}
184
+ }
185
+ //#endregion
186
+ //#region src/inspection/topologyGraph.ts
187
+ function mergeTopologyGraphs(inputs) {
188
+ const nodes = /* @__PURE__ */ new Map();
189
+ const links = /* @__PURE__ */ new Map();
190
+ const routes = /* @__PURE__ */ new Map();
191
+ for (const { source, graph } of [...inputs].sort((a, b) => a.source.localeCompare(b.source))) {
192
+ for (const node of graph.nodes) {
193
+ const existing = nodes.get(node.nodeId);
194
+ if (existing === void 0) nodes.set(node.nodeId, {
195
+ value: cloneTopologyNode(node),
196
+ sources: /* @__PURE__ */ new Set([source])
197
+ });
198
+ else {
199
+ existing.sources.add(source);
200
+ if (node.kind === "hub") existing.value.kind = "hub";
201
+ existing.value.label ??= node.label;
202
+ for (const port of node.ports) {
203
+ const current = existing.value.ports.find((candidate) => candidate.portId === port.portId);
204
+ if (current === void 0) existing.value.ports.push({ ...port });
205
+ else current.label ??= port.label;
206
+ }
207
+ const descriptors = existing.value.descriptors ?? [];
208
+ const descriptorKeys = new Set(descriptors.map(descriptorKey));
209
+ for (const descriptor of node.descriptors ?? []) {
210
+ const key = descriptorKey(descriptor);
211
+ if (!descriptorKeys.has(key)) {
212
+ descriptors.push(cloneParticipantDescriptorSource(descriptor));
213
+ descriptorKeys.add(key);
214
+ }
215
+ }
216
+ if (descriptors.length > 0) existing.value.descriptors = descriptors;
217
+ }
218
+ }
219
+ for (const link of graph.links) {
220
+ const isReversed = endpointKey(link.from) > endpointKey(link.to);
221
+ const [from, to] = canonicalEndpoints(link.from, link.to);
222
+ const key = `${endpointKey(from)}\u0000${endpointKey(to)}`;
223
+ const value = {
224
+ ...link,
225
+ from,
226
+ to,
227
+ ...link.transport === void 0 ? {} : { transport: cloneTopologyTransport(link.transport, isReversed) }
228
+ };
229
+ const existing = links.get(key);
230
+ if (existing === void 0) links.set(key, {
231
+ value,
232
+ sources: /* @__PURE__ */ new Set([source])
233
+ });
234
+ else {
235
+ existing.sources.add(source);
236
+ existing.value.transport ??= value.transport;
237
+ }
238
+ }
239
+ for (const route of graph.routes) {
240
+ const key = `${route.serviceId}\u0000${route.nodeId}\u0000${route.portId}\u0000${route.match}`;
241
+ const existing = routes.get(key);
242
+ if (existing === void 0) routes.set(key, {
243
+ value: { ...route },
244
+ sources: /* @__PURE__ */ new Set([source])
245
+ });
246
+ else existing.sources.add(source);
247
+ }
248
+ }
249
+ const sourcedRoutes = [...routes.values()].map(({ value, sources }) => ({
250
+ ...value,
251
+ sources: [...sources].sort()
252
+ }));
253
+ return {
254
+ nodes: [...nodes.values()].map(({ value, sources }) => ({
255
+ ...value,
256
+ ports: [...value.ports].sort((a, b) => a.portId.localeCompare(b.portId)),
257
+ ...value.descriptors === void 0 ? {} : { descriptors: [...value.descriptors].sort((a, b) => descriptorKey(a).localeCompare(descriptorKey(b))) },
258
+ sources: [...sources].sort()
259
+ })).sort((a, b) => a.nodeId.localeCompare(b.nodeId)),
260
+ links: [...links.values()].map(({ value, sources }) => ({
261
+ ...value,
262
+ sources: [...sources].sort()
263
+ })).sort((a, b) => endpointKey(a.from).localeCompare(endpointKey(b.from)) || endpointKey(a.to).localeCompare(endpointKey(b.to))),
264
+ routes: sourcedRoutes.sort((a, b) => a.serviceId.localeCompare(b.serviceId) || a.nodeId.localeCompare(b.nodeId) || a.portId.localeCompare(b.portId) || a.match.localeCompare(b.match)),
265
+ sources: [...inputs].sort((a, b) => a.source.localeCompare(b.source)).map(({ source, graph }) => ({
266
+ serviceId: source,
267
+ observerServiceId: graph.observerServiceId,
268
+ entryNodeId: graph.entryNodeId
269
+ }))
270
+ };
271
+ }
272
+ function cloneTopologyNode(node) {
273
+ return {
274
+ ...node,
275
+ ports: node.ports.map((port) => ({ ...port })),
276
+ ...node.descriptors === void 0 ? {} : { descriptors: node.descriptors.map(cloneParticipantDescriptorSource) }
277
+ };
278
+ }
279
+ function cloneParticipantDescriptorSource(source) {
280
+ return {
281
+ ...source,
282
+ descriptor: {
283
+ ...source.descriptor,
284
+ ...source.descriptor.metadata === void 0 ? {} : { metadata: { ...source.descriptor.metadata } }
285
+ }
286
+ };
287
+ }
288
+ function cloneTopologyTransport(transport, reverse) {
289
+ const { local: reportedLocal, remote: reportedRemote, metadata, ...rest } = transport;
290
+ const local = reverse ? reportedRemote : reportedLocal;
291
+ const remote = reverse ? reportedLocal : reportedRemote;
292
+ return {
293
+ ...rest,
294
+ ...local === void 0 ? {} : { local: { ...local } },
295
+ ...remote === void 0 ? {} : { remote: { ...remote } },
296
+ ...metadata === void 0 ? {} : { metadata: { ...metadata } }
297
+ };
298
+ }
299
+ function descriptorKey(source) {
300
+ const descriptor = source.descriptor;
301
+ const metadata = descriptor.metadata === void 0 ? void 0 : Object.fromEntries(Object.entries(descriptor.metadata).sort(([a], [b]) => a.localeCompare(b)));
302
+ return JSON.stringify({
303
+ source: source.source,
304
+ descriptor: {
305
+ ...descriptor,
306
+ ...metadata === void 0 ? {} : { metadata }
307
+ }
308
+ });
309
+ }
310
+ function canonicalEndpoints(a, b) {
311
+ return endpointKey(a) <= endpointKey(b) ? [a, b] : [b, a];
312
+ }
313
+ function endpointKey(endpoint) {
314
+ return `${endpoint.nodeId}\u0000${endpoint.portId}`;
315
+ }
316
+ //#endregion
317
+ //#region src/inspection/networkInspectionClient.ts
318
+ /**
319
+ * Merges independently observed service graphs. Traffic is deliberately not
320
+ * deduplicated: each source remains identified so consumers can stitch flows.
321
+ */
322
+ var NetworkInspectionClient = class {
323
+ _connection;
324
+ _options;
325
+ _sources = /* @__PURE__ */ new Map();
326
+ _trafficGroups = /* @__PURE__ */ new Set();
327
+ constructor(_connection, _options = {}) {
328
+ this._connection = _connection;
329
+ this._options = _options;
330
+ }
331
+ async addTopologyService(serviceId) {
332
+ const existing = this._sources.get(serviceId);
333
+ if (existing !== void 0) {
334
+ await existing.watch.ready;
335
+ return;
336
+ }
337
+ const client = new TopologyClient(this._connection, serviceId);
338
+ let source;
339
+ const watch = client.watch({
340
+ onGraph: (graph) => {
341
+ source.graph = graph;
342
+ this._options.onGraph?.(this.getGraph());
343
+ },
344
+ onError: (error) => this._options.onError?.(error, serviceId)
345
+ });
346
+ source = {
347
+ client,
348
+ watch,
349
+ graph: void 0
350
+ };
351
+ this._sources.set(serviceId, source);
352
+ for (const group of this._trafficGroups) group.add(serviceId);
353
+ try {
354
+ await watch.ready;
355
+ } catch (error) {
356
+ if (this._sources.get(serviceId) === source) await this.removeTopologyService(serviceId);
357
+ else {
358
+ await source.watch.cancel("source-replaced").catch(() => void 0);
359
+ await source.watch.done.catch(() => void 0);
360
+ }
361
+ throw error;
362
+ }
363
+ }
364
+ async removeTopologyService(serviceId) {
365
+ const source = this._sources.get(serviceId);
366
+ if (source === void 0) return;
367
+ this._sources.delete(serviceId);
368
+ await Promise.all([...this._trafficGroups].map((group) => group.remove(serviceId)));
369
+ await source.watch.cancel("source-removed");
370
+ await source.watch.done.catch(() => void 0);
371
+ this._options.onGraph?.(this.getGraph());
372
+ }
373
+ getGraph() {
374
+ return mergeTopologyGraphs([...this._sources.entries()].flatMap(([source, value]) => value.graph === void 0 ? [] : [{
375
+ source,
376
+ graph: value.graph
377
+ }]));
378
+ }
379
+ watchTraffic(options, callbacks) {
380
+ const group = new NetworkTrafficGroup(this._connection, options, callbacks, () => this._trafficGroups.delete(group));
381
+ this._trafficGroups.add(group);
382
+ for (const serviceId of this._sources.keys()) group.add(serviceId);
383
+ return group;
384
+ }
385
+ async dispose() {
386
+ const failures = [];
387
+ const trafficResults = await Promise.allSettled([...this._trafficGroups].map((group) => group.cancel("disposed")));
388
+ failures.push(...trafficResults.flatMap((result) => result.status === "rejected" ? [result.reason] : []));
389
+ const sourceResults = await Promise.allSettled([...this._sources.keys()].map((serviceId) => this.removeTopologyService(serviceId)));
390
+ failures.push(...sourceResults.flatMap((result) => result.status === "rejected" ? [result.reason] : []));
391
+ if (failures.length !== 0) throw new AggregateError(failures, "Failed to dispose network inspection");
392
+ }
393
+ };
394
+ var NetworkTrafficGroup = class {
395
+ _connection;
396
+ _options;
397
+ _callbacks;
398
+ _onCancel;
399
+ _watches = /* @__PURE__ */ new Map();
400
+ _active = true;
401
+ _resolveDone;
402
+ done;
403
+ constructor(_connection, _options, _callbacks, _onCancel) {
404
+ this._connection = _connection;
405
+ this._options = _options;
406
+ this._callbacks = _callbacks;
407
+ this._onCancel = _onCancel;
408
+ let resolve;
409
+ this.done = new Promise((r) => resolve = r);
410
+ this._resolveDone = resolve;
411
+ }
412
+ add(serviceId) {
413
+ if (!this._active || this._watches.has(serviceId)) return;
414
+ const client = new TrafficClient(this._connection, serviceId);
415
+ const callbacks = {
416
+ onTransit: (transit) => this._callbacks.onTransit(transit, serviceId),
417
+ onOverflow: (overflow) => this._callbacks.onOverflow?.(overflow, serviceId),
418
+ onError: (error) => this._callbacks.onError?.(error, serviceId)
419
+ };
420
+ const watch = "maxPayloadBytes" in this._options ? client.watchWithPayloads(this._options, callbacks) : client.watch(this._options, callbacks);
421
+ this._watches.set(serviceId, watch);
422
+ }
423
+ async remove(serviceId) {
424
+ const watch = this._watches.get(serviceId);
425
+ if (watch === void 0) return;
426
+ this._watches.delete(serviceId);
427
+ await watch.cancel("source-removed").catch(() => void 0);
428
+ await watch.done.catch(() => void 0);
429
+ }
430
+ async cancel(reason) {
431
+ if (!this._active) return this.done;
432
+ this._active = false;
433
+ this._onCancel();
434
+ const watches = [...this._watches.values()];
435
+ this._watches.clear();
436
+ const failures = [];
437
+ try {
438
+ const results = await Promise.allSettled(watches.map(async (watch) => {
439
+ await watch.cancel(reason);
440
+ await watch.done;
441
+ }));
442
+ failures.push(...results.flatMap((result) => result.status === "rejected" ? [result.reason] : []));
443
+ } finally {
444
+ this._resolveDone();
445
+ }
446
+ if (failures.length !== 0) throw new AggregateError(failures, "Failed to cancel network traffic watches");
447
+ }
448
+ };
449
+ //#endregion
450
+ //#region src/inspection/topologyNetworkClient.ts
451
+ /**
452
+ * Queries or watches a merged topology graph from one, many, or dynamically
453
+ * discovered topology providers. Query mode never registers directory or
454
+ * topology watches; progress snapshots describe its finite request fan-out.
455
+ */
456
+ var TopologyNetworkClient = class {
457
+ _connection;
458
+ constructor(_connection) {
459
+ this._connection = _connection;
460
+ }
461
+ async query(options = {}, callbacks = {}) {
462
+ const store = new TopologyNetworkStore();
463
+ const unsubscribe = callbacks.onSnapshot === void 0 ? void 0 : store.subscribe(callbacks.onSnapshot);
464
+ try {
465
+ if (options.sourceServiceIds !== void 0) {
466
+ const serviceIds = uniqueSorted(options.sourceServiceIds);
467
+ store.reconcileSources(serviceIds);
468
+ await Promise.all(serviceIds.map((serviceId) => this._querySource(store, serviceId, options.timeoutMs)));
469
+ } else await this._queryDiscovered(store, options);
470
+ store.setComplete();
471
+ return store.snapshot;
472
+ } finally {
473
+ unsubscribe?.();
474
+ }
475
+ }
476
+ watch(options = {}) {
477
+ return new TopologyNetworkWatchImpl(this._connection, options);
478
+ }
479
+ async _queryDiscovered(store, options) {
480
+ const explorer = new HubDirectoryExplorer(this._connection.channel, {
481
+ interfaceId: topologyInterface.info.id,
482
+ maxDepth: options.maxDepth,
483
+ timeoutMs: options.timeoutMs ?? DEFAULT_RPC_TIMEOUT_MS
484
+ });
485
+ const queries = /* @__PURE__ */ new Map();
486
+ const reconcile = (snapshot) => {
487
+ store.setDirectory(snapshot);
488
+ const serviceIds = topologyServiceIds(snapshot);
489
+ store.reconcileSources(serviceIds);
490
+ for (const serviceId of serviceIds) if (!queries.has(serviceId)) queries.set(serviceId, this._querySource(store, serviceId, options.timeoutMs));
491
+ };
492
+ const unsubscribe = explorer.subscribe((event) => reconcile(event.snapshot));
493
+ try {
494
+ await explorer.explore();
495
+ reconcile(explorer.graphSnapshot);
496
+ } catch (error) {
497
+ store.setDirectoryError(error);
498
+ } finally {
499
+ unsubscribe();
500
+ explorer.dispose();
501
+ }
502
+ await Promise.all(queries.values());
503
+ }
504
+ async _querySource(store, serviceId, timeoutMs = DEFAULT_RPC_TIMEOUT_MS) {
505
+ try {
506
+ const graph = await new TopologyClient(this._connection, serviceId, timeoutMs).getGraph();
507
+ if (store.hasSource(serviceId)) store.setSourceGraph(serviceId, graph);
508
+ } catch (error) {
509
+ if (store.hasSource(serviceId)) store.setSourceError(serviceId, error);
510
+ }
511
+ }
512
+ };
513
+ var TopologyNetworkWatchImpl = class {
514
+ _connection;
515
+ _options;
516
+ _store = new TopologyNetworkStore();
517
+ _sourceWatches = /* @__PURE__ */ new Map();
518
+ _initialSourceReady = /* @__PURE__ */ new Map();
519
+ _explorer;
520
+ _stopDirectoryWatch;
521
+ _cancelled = false;
522
+ _resolveDone;
523
+ _resolveReady;
524
+ _readySettled = false;
525
+ ready = new Promise((resolve) => {
526
+ this._resolveReady = resolve;
527
+ });
528
+ done = new Promise((resolve) => {
529
+ this._resolveDone = resolve;
530
+ });
531
+ constructor(_connection, _options) {
532
+ this._connection = _connection;
533
+ this._options = _options;
534
+ this._explorer = _options.sourceServiceIds === void 0 ? new HubDirectoryExplorer(_connection.channel, {
535
+ interfaceId: topologyInterface.info.id,
536
+ maxDepth: _options.maxDepth,
537
+ timeoutMs: _options.timeoutMs ?? DEFAULT_RPC_TIMEOUT_MS
538
+ }) : void 0;
539
+ this._initialize();
540
+ }
541
+ get snapshot() {
542
+ return this._store.snapshot;
543
+ }
544
+ subscribe(listener) {
545
+ return this._store.subscribe(listener);
546
+ }
547
+ async cancel(reason = "topology watch cancelled") {
548
+ if (this._cancelled) return this.done;
549
+ this._cancelled = true;
550
+ this._stopDirectoryWatch?.();
551
+ this._stopDirectoryWatch = void 0;
552
+ this._explorer?.dispose();
553
+ const watches = [...this._sourceWatches.values()];
554
+ this._sourceWatches.clear();
555
+ await Promise.allSettled(watches.map((watch) => watch.cancel(reason)));
556
+ if (!this._readySettled) this._settleReady();
557
+ this._resolveDone();
558
+ }
559
+ async _initialize() {
560
+ try {
561
+ if (this._options.sourceServiceIds !== void 0) {
562
+ const serviceIds = uniqueSorted(this._options.sourceServiceIds);
563
+ this._store.reconcileSources(serviceIds);
564
+ for (const serviceId of serviceIds) this._startSourceWatch(serviceId);
565
+ } else {
566
+ const explorer = this._explorer;
567
+ explorer.subscribe((event) => this._reconcileDirectory(event.snapshot));
568
+ this._stopDirectoryWatch = await explorer.watch(() => {});
569
+ this._reconcileDirectory(explorer.graphSnapshot);
570
+ }
571
+ await Promise.allSettled(this._initialSourceReady.values());
572
+ if (!this._cancelled) {
573
+ this._store.setComplete();
574
+ this._settleReady();
575
+ }
576
+ } catch (error) {
577
+ if (!this._cancelled) {
578
+ this._store.setDirectoryError(error);
579
+ this._store.setComplete();
580
+ this._settleReady();
581
+ }
582
+ }
583
+ }
584
+ _reconcileDirectory(snapshot) {
585
+ if (this._cancelled) return;
586
+ this._store.setDirectory(snapshot);
587
+ const serviceIds = topologyServiceIds(snapshot);
588
+ const desired = new Set(serviceIds);
589
+ for (const serviceId of serviceIds) this._startSourceWatch(serviceId);
590
+ for (const [serviceId, watch] of this._sourceWatches) {
591
+ if (desired.has(serviceId)) continue;
592
+ this._sourceWatches.delete(serviceId);
593
+ this._initialSourceReady.delete(serviceId);
594
+ this._store.removeSource(serviceId);
595
+ watch.cancel("topology source removed").catch(() => void 0);
596
+ }
597
+ }
598
+ _startSourceWatch(serviceId) {
599
+ if (this._sourceWatches.has(serviceId) || this._cancelled) return;
600
+ this._store.ensureSource(serviceId);
601
+ const watch = new TopologyClient(this._connection, serviceId, this._options.timeoutMs).watch({
602
+ onGraph: (graph) => {
603
+ if (!this._cancelled && this._sourceWatches.get(serviceId) === watch) this._store.setSourceGraph(serviceId, graph);
604
+ },
605
+ onError: (error) => {
606
+ if (!this._cancelled && this._sourceWatches.get(serviceId) === watch) this._store.setSourceError(serviceId, error);
607
+ }
608
+ });
609
+ this._sourceWatches.set(serviceId, watch);
610
+ const initial = watch.ready.then(() => void 0, (error) => {
611
+ if (!this._cancelled && this._sourceWatches.get(serviceId) === watch) this._store.setSourceError(serviceId, error);
612
+ });
613
+ this._initialSourceReady.set(serviceId, initial);
614
+ watch.done.catch((error) => {
615
+ if (!this._cancelled && this._sourceWatches.get(serviceId) === watch) this._store.setSourceError(serviceId, error);
616
+ });
617
+ }
618
+ _settleReady() {
619
+ if (this._readySettled) return;
620
+ this._readySettled = true;
621
+ this._resolveReady(this._store.snapshot);
622
+ }
623
+ };
624
+ var TopologyNetworkStore = class {
625
+ _sources = /* @__PURE__ */ new Map();
626
+ _listeners = /* @__PURE__ */ new Set();
627
+ _revision = 0;
628
+ _complete = false;
629
+ _directory;
630
+ _directoryError;
631
+ get snapshot() {
632
+ const sources = [...this._sources.values()].sort((a, b) => a.serviceId.localeCompare(b.serviceId)).map((source) => ({
633
+ serviceId: source.serviceId,
634
+ state: source.state,
635
+ ...source.graph !== void 0 ? { graph: source.graph } : {},
636
+ ...source.error !== void 0 ? { error: source.error } : {}
637
+ }));
638
+ return {
639
+ revision: this._revision,
640
+ complete: this._complete,
641
+ ...this._directory !== void 0 ? { directory: this._directory } : {},
642
+ ...this._directoryError !== void 0 ? { directoryError: this._directoryError } : {},
643
+ sources,
644
+ graph: mergeTopologyGraphs(sources.flatMap((source) => source.graph === void 0 ? [] : [{
645
+ source: source.serviceId,
646
+ graph: source.graph
647
+ }]))
648
+ };
649
+ }
650
+ subscribe(listener) {
651
+ this._listeners.add(listener);
652
+ listener(this.snapshot);
653
+ return () => this._listeners.delete(listener);
654
+ }
655
+ hasSource(serviceId) {
656
+ return this._sources.has(serviceId);
657
+ }
658
+ ensureSource(serviceId) {
659
+ if (this._sources.has(serviceId)) return;
660
+ this._sources.set(serviceId, {
661
+ serviceId,
662
+ state: "loading"
663
+ });
664
+ this._emit();
665
+ }
666
+ reconcileSources(serviceIds) {
667
+ const desired = new Set(serviceIds);
668
+ let changed = false;
669
+ for (const serviceId of serviceIds) if (!this._sources.has(serviceId)) {
670
+ this._sources.set(serviceId, {
671
+ serviceId,
672
+ state: "loading"
673
+ });
674
+ changed = true;
675
+ }
676
+ for (const serviceId of this._sources.keys()) if (!desired.has(serviceId)) {
677
+ this._sources.delete(serviceId);
678
+ changed = true;
679
+ }
680
+ if (changed) this._emit();
681
+ }
682
+ removeSource(serviceId) {
683
+ if (this._sources.delete(serviceId)) this._emit();
684
+ }
685
+ setSourceGraph(serviceId, graph) {
686
+ const source = this._sources.get(serviceId);
687
+ if (source === void 0) return;
688
+ source.state = "ready";
689
+ source.graph = graph;
690
+ source.error = void 0;
691
+ this._emit();
692
+ }
693
+ setSourceError(serviceId, error) {
694
+ const source = this._sources.get(serviceId);
695
+ if (source === void 0) return;
696
+ source.state = "error";
697
+ source.error = errorMessage(error);
698
+ this._emit();
699
+ }
700
+ setDirectory(snapshot) {
701
+ this._directory = snapshot;
702
+ this._directoryError = void 0;
703
+ this._emit();
704
+ }
705
+ setDirectoryError(error) {
706
+ this._directoryError = errorMessage(error);
707
+ this._emit();
708
+ }
709
+ setComplete() {
710
+ if (this._complete) return;
711
+ this._complete = true;
712
+ this._emit();
713
+ }
714
+ _emit() {
715
+ this._revision++;
716
+ const snapshot = this.snapshot;
717
+ for (const listener of [...this._listeners]) try {
718
+ listener(snapshot);
719
+ } catch {}
720
+ }
721
+ };
722
+ function topologyServiceIds(snapshot) {
723
+ return uniqueSorted(snapshot.result.listings.filter((listing) => listing.interfaceId === topologyInterface.info.id).map((listing) => listing.serviceId));
724
+ }
725
+ function uniqueSorted(values) {
726
+ return [...new Set(values)].sort((a, b) => a.localeCompare(b));
727
+ }
728
+ function errorMessage(error) {
729
+ return error instanceof Error ? error.message : String(error);
730
+ }
731
+ //#endregion
732
+ export { NetworkInspectionClient, NodeInfoClient, TopologyClient, TopologyNetworkClient, TrafficClient, mergeTopologyGraphs };
733
+
734
+ //# sourceMappingURL=index.js.map