@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.
- package/README.md +155 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/inspection/index.d.ts +169 -0
- package/dist/inspection/index.js +734 -0
- package/dist/inspection/index.js.map +1 -0
- package/dist/json-document/index.d.ts +76 -0
- package/dist/json-document/index.js +131 -0
- package/dist/json-document/index.js.map +1 -0
- package/dist/json-rpc/index.d.ts +82 -0
- package/dist/json-rpc/index.js +439 -0
- package/dist/json-rpc/index.js.map +1 -0
- package/dist/logging/index.d.ts +118 -0
- package/dist/logging/index.js +68 -0
- package/dist/logging/index.js.map +1 -0
- package/package.json +44 -0
|
@@ -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
|