hrpc-inspector 0.0.0 → 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,441 @@
1
+ // src/observe.ts
2
+ import { BatchFlusher } from "hrpc-inspector-probe/flush";
3
+ import { createHyperswarmExporter } from "hrpc-inspector-probe/exporters/hyperswarm";
4
+ import { Redactor } from "hrpc-inspector-probe/redactor";
5
+
6
+ // src/detect.ts
7
+ function detectRuntime(env = globalThis) {
8
+ const has = (v) => typeof v !== "undefined" && v !== null;
9
+ if (has(env.Pear)) return { runtime: "pear", isBareRuntime: true, isBridgeOnly: false };
10
+ if (has(env.Bare)) return { runtime: "bare", isBareRuntime: true, isBridgeOnly: false };
11
+ const rn = env.navigator?.product === "ReactNative" || has(env.__fbBatchedBridge) || has(env.HermesInternal);
12
+ if (rn) return { runtime: "react-native", isBareRuntime: false, isBridgeOnly: true };
13
+ if (has(env.process?.versions?.electron)) {
14
+ return { runtime: "electron", isBareRuntime: false, isBridgeOnly: false };
15
+ }
16
+ if (has(env.process?.versions?.node)) {
17
+ return { runtime: "node", isBareRuntime: false, isBridgeOnly: false };
18
+ }
19
+ if (has(env.window) && has(env.document)) {
20
+ return { runtime: "browser", isBareRuntime: false, isBridgeOnly: true };
21
+ }
22
+ return { runtime: "unknown", isBareRuntime: false, isBridgeOnly: false };
23
+ }
24
+
25
+ // src/reporters.ts
26
+ function createWebSocketReporter(url, opts = {}) {
27
+ const WS = opts.WebSocketImpl ?? globalThis.WebSocket;
28
+ const maxQueue = opts.maxQueue ?? 1e3;
29
+ const reconnectMs = opts.reconnectMs ?? 3e3;
30
+ const OPEN = 1;
31
+ let ws = null;
32
+ let queue = [];
33
+ let closed = false;
34
+ function enqueue(ev) {
35
+ queue.push(ev);
36
+ if (queue.length > maxQueue) queue.shift();
37
+ }
38
+ function rawSend(ev) {
39
+ try {
40
+ ws.send(JSON.stringify(ev));
41
+ } catch {
42
+ enqueue(ev);
43
+ }
44
+ }
45
+ function ensure() {
46
+ if (closed || !WS) return;
47
+ if (ws && (ws.readyState === 0 || ws.readyState === 1)) return;
48
+ try {
49
+ ws = new WS(url);
50
+ ws.onopen = () => {
51
+ if (opts.announce !== void 0) rawSend({ __source: opts.announce });
52
+ const q = queue;
53
+ queue = [];
54
+ for (const e of q) rawSend(e);
55
+ };
56
+ ws.onclose = () => {
57
+ ws = null;
58
+ if (!closed) setTimeout(ensure, reconnectMs);
59
+ };
60
+ ws.onerror = () => {
61
+ };
62
+ if (opts.onMessage) ws.onmessage = (m) => {
63
+ try {
64
+ opts.onMessage(JSON.parse(m.data));
65
+ } catch {
66
+ }
67
+ };
68
+ } catch {
69
+ ws = null;
70
+ }
71
+ }
72
+ return {
73
+ export(batch) {
74
+ if (closed) return;
75
+ ensure();
76
+ for (const ev of batch) {
77
+ if (ws && ws.readyState === OPEN) rawSend(ev);
78
+ else enqueue(ev);
79
+ }
80
+ },
81
+ close() {
82
+ closed = true;
83
+ try {
84
+ ws?.close();
85
+ } catch {
86
+ }
87
+ ws = null;
88
+ }
89
+ };
90
+ }
91
+
92
+ // src/wrap-client.ts
93
+ var seq = 0;
94
+ var invokeCtx = null;
95
+ function setInvokeContext(ctx) {
96
+ invokeCtx = ctx;
97
+ }
98
+ var PREVIEW_MAX_BYTES = 64e3;
99
+ function defaultPreview(v) {
100
+ try {
101
+ const s = JSON.stringify(v);
102
+ if (s === void 0) return String(v);
103
+ if (s.length <= PREVIEW_MAX_BYTES) return v;
104
+ if (typeof v === "string") return v.slice(0, PREVIEW_MAX_BYTES) + `\u2026[truncated ${s.length}B]`;
105
+ return { __truncated: true, bytes: s.length, preview: s.slice(0, 2e3) + "\u2026" };
106
+ } catch {
107
+ return { __unserializable: true };
108
+ }
109
+ }
110
+ function monitorStream(report, endpoint, corrId, stream, t0, preview, tag) {
111
+ report({ type: "stream.open", method: endpoint, corrId, t: Date.now(), ...tag });
112
+ let n = 0;
113
+ let ended = false;
114
+ const settle = (status, extra) => {
115
+ if (ended) return;
116
+ ended = true;
117
+ report({ type: "request.end", method: endpoint, corrId, status, count: n, dur: Date.now() - t0, t: Date.now(), ...tag, ...extra || {} });
118
+ };
119
+ try {
120
+ stream.on("data", (item) => report({ type: "stream.data", method: endpoint, corrId, seq: ++n, item: preview(item), t: Date.now(), ...tag }));
121
+ stream.on("error", (err) => settle("error", { error: String(err) }));
122
+ stream.on("end", () => settle("closed"));
123
+ stream.on("close", () => settle("closed"));
124
+ } catch (e) {
125
+ report({ type: "request.end", method: endpoint, corrId, status: "error", error: "monitor-failed: " + String(e), dur: 0, t: Date.now(), ...tag });
126
+ }
127
+ }
128
+ function wrapFn(report, endpoint, fn, self, preview, monitorStreams, onCall) {
129
+ return function observed(...args) {
130
+ const corrId = "rpc-" + ++seq;
131
+ let tag = {};
132
+ if (invokeCtx && invokeCtx.method === endpoint) {
133
+ tag = { origin: "gui", invokeId: invokeCtx.invokeId, replayOf: invokeCtx.replayOf };
134
+ invokeCtx = null;
135
+ }
136
+ if (onCall) {
137
+ try {
138
+ onCall(corrId, endpoint, args);
139
+ } catch {
140
+ }
141
+ }
142
+ const t = Date.now();
143
+ report({ type: "request.start", method: endpoint, corrId, args: preview(args), t, ...tag });
144
+ let result;
145
+ try {
146
+ result = fn.apply(self, args);
147
+ } catch (err) {
148
+ report({ type: "request.end", method: endpoint, corrId, status: "error", error: String(err), dur: Date.now() - t, t: Date.now(), ...tag });
149
+ throw err;
150
+ }
151
+ if (result && typeof result.then === "function") {
152
+ result.then(
153
+ (res) => report({ type: "request.end", method: endpoint, corrId, status: "ok", response: preview(res), dur: Date.now() - t, t: Date.now(), ...tag }),
154
+ (err) => report({ type: "request.end", method: endpoint, corrId, status: "error", error: String(err), dur: Date.now() - t, t: Date.now(), ...tag })
155
+ );
156
+ } else if (monitorStreams && result && typeof result.on === "function") {
157
+ monitorStream(report, endpoint, corrId, result, t, preview, tag);
158
+ } else {
159
+ report({ type: "request.end", method: endpoint, corrId, status: "ok", response: preview(result), dur: Date.now() - t, t: Date.now(), ...tag });
160
+ }
161
+ return result;
162
+ };
163
+ }
164
+ function wrapClient(client, report, opts = {}) {
165
+ if (!client || typeof client !== "object") return client;
166
+ const preview = opts.preview ?? defaultPreview;
167
+ const monitorStreams = opts.monitorStreams !== false;
168
+ try {
169
+ for (const ns of Object.keys(client)) {
170
+ const group = client[ns];
171
+ if (group && typeof group === "object") {
172
+ for (const m of Object.keys(group)) {
173
+ if (typeof group[m] === "function") group[m] = wrapFn(report, `${ns}.${m}`, group[m], group, preview, monitorStreams, opts.onCall);
174
+ }
175
+ } else if (typeof group === "function") {
176
+ client[ns] = wrapFn(report, ns, group, client, preview, monitorStreams, opts.onCall);
177
+ }
178
+ }
179
+ } catch {
180
+ }
181
+ return client;
182
+ }
183
+
184
+ // src/observe.ts
185
+ var DEFAULT_TOPIC = "p2p-observability-hub:v0";
186
+ function persistentId(runtime) {
187
+ try {
188
+ const ls = globalThis.localStorage;
189
+ if (runtime === "browser" && ls) {
190
+ const KEY = "hrpc-inspector:deviceId";
191
+ let v = ls.getItem(KEY);
192
+ if (!v) {
193
+ v = Math.random().toString(36).slice(2, 10);
194
+ ls.setItem(KEY, v);
195
+ }
196
+ return v;
197
+ }
198
+ } catch {
199
+ }
200
+ return Math.random().toString(36).slice(2, 8);
201
+ }
202
+ function resolveSource(s, runtime) {
203
+ if (typeof s === "string" && s) return { id: s, label: s, runtime };
204
+ const p = s ?? {};
205
+ const deviceId = p.deviceId || void 0;
206
+ const appId = (p.appId ?? p.app) || void 0;
207
+ const enc = (v) => encodeURIComponent(v);
208
+ const explicitId = typeof p.id === "string" && p.id ? p.id : void 0;
209
+ const id = explicitId ?? (deviceId && appId ? `d=${enc(deviceId)};a=${enc(appId)}` : deviceId ? `d=${enc(deviceId)}` : appId ? `a=${enc(appId)}` : `r=${runtime}-${persistentId(runtime)}`);
210
+ const parts = [appId, p.device ?? deviceId, p.variant].filter(Boolean);
211
+ const label = (typeof p.label === "string" && p.label ? p.label : void 0) ?? (parts.length ? parts.join(" \xB7 ") : id);
212
+ return { ...p, id, label, runtime, deviceId, appId };
213
+ }
214
+ function redactSource(src, redactor) {
215
+ const id = redactor.hashPeer(src.id);
216
+ return {
217
+ id,
218
+ label: id,
219
+ // no human name on the wire
220
+ runtime: src.runtime,
221
+ ...src.deviceId ? { deviceId: redactor.hashPeer(src.deviceId) } : {},
222
+ ...src.appId ? { appId: src.appId } : {},
223
+ // a bundle id is not personal
224
+ ...src.variant ? { variant: src.variant } : {}
225
+ // e.g. "nightly"
226
+ };
227
+ }
228
+ function createDeferredExporter() {
229
+ let live = null;
230
+ const buffered = [];
231
+ return {
232
+ export(batch) {
233
+ if (live) live.export(batch);
234
+ else buffered.push(batch);
235
+ },
236
+ attach(stream) {
237
+ live = createHyperswarmExporter(stream);
238
+ for (const b of buffered) live.export(b);
239
+ buffered.length = 0;
240
+ },
241
+ get pendingBatches() {
242
+ return buffered.length;
243
+ }
244
+ };
245
+ }
246
+ async function autoSwarm(topic, onConnection) {
247
+ const Hyperswarm = (await import("hyperswarm")).default;
248
+ const crypto = (await import("hypercore-crypto")).default;
249
+ const swarm = new Hyperswarm();
250
+ const topicKey = crypto.hash(Buffer.from(topic));
251
+ swarm.on("connection", (conn) => onConnection(conn));
252
+ await swarm.join(topicKey, { client: true, server: false }).flushed();
253
+ }
254
+ function observe(opts = {}) {
255
+ const info = detectRuntime();
256
+ const source = resolveSource(opts.source, info.runtime);
257
+ const redactOn = opts.redact ?? (opts.websocket ? false : true);
258
+ const redactor = opts.redactor ?? (redactOn ? new Redactor({ bodyAllowlist: opts.bodyAllowlist }) : null);
259
+ const wireSource = redactor ? redactSource(source, redactor) : source;
260
+ const invokeEnabled = opts.allowInvoke === true && !!opts.websocket;
261
+ const replayLogMax = opts.replayLogMax ?? 2e3;
262
+ const recentCalls = /* @__PURE__ */ new Map();
263
+ const recordCall = invokeEnabled ? (corrId, method, args) => {
264
+ recentCalls.set(corrId, { method, args });
265
+ if (recentCalls.size > replayLogMax) {
266
+ const oldest = recentCalls.keys().next().value;
267
+ if (oldest !== void 0) recentCalls.delete(oldest);
268
+ }
269
+ } : void 0;
270
+ let wrapped = null;
271
+ let exporter;
272
+ let reporter = null;
273
+ let deferred = null;
274
+ let mode;
275
+ if (opts.exporter) {
276
+ exporter = opts.exporter;
277
+ mode = "custom";
278
+ } else if (opts.websocket) {
279
+ reporter = createWebSocketReporter(opts.websocket, {
280
+ // Advertise the replay capability so the GUI shows Replay only for opted-in sources.
281
+ announce: invokeEnabled ? { ...wireSource, caps: { invoke: true } } : wireSource,
282
+ // Inbound handler ONLY when replay is enabled — otherwise the reporter stays send-only.
283
+ onMessage: invokeEnabled ? handleInvoke : void 0
284
+ });
285
+ exporter = reporter;
286
+ mode = "websocket";
287
+ } else if (opts.stream) {
288
+ exporter = createHyperswarmExporter(opts.stream);
289
+ mode = "stream";
290
+ } else {
291
+ deferred = createDeferredExporter();
292
+ exporter = deferred;
293
+ mode = "buffering";
294
+ }
295
+ const flusher = new BatchFlusher({
296
+ intervalMs: opts.intervalMs ?? 200,
297
+ onFlush: (batch) => {
298
+ const out = redactor ? redactor.redactBatch(batch) : batch;
299
+ exporter.export(out.map((e) => ({ ...e, src: wireSource.id })));
300
+ }
301
+ });
302
+ flusher.start();
303
+ const sink = {
304
+ emit(event) {
305
+ try {
306
+ flusher.add(event);
307
+ } catch {
308
+ }
309
+ }
310
+ };
311
+ const INFLIGHT_MAX = 1e3;
312
+ const inflightCalls = /* @__PURE__ */ new Map();
313
+ const report = (event) => {
314
+ const e = event;
315
+ const corrId = e && typeof e.corrId === "string" ? e.corrId : null;
316
+ if (corrId) {
317
+ if (e.type === "request.start") {
318
+ inflightCalls.set(corrId, { method: String(e.method ?? ""), corrId, t: typeof e.t === "number" ? e.t : Date.now() });
319
+ if (inflightCalls.size > INFLIGHT_MAX) {
320
+ const oldest = inflightCalls.keys().next().value;
321
+ if (oldest !== void 0) inflightCalls.delete(oldest);
322
+ }
323
+ } else if (e.type === "stream.open" || e.type === "request.end") {
324
+ inflightCalls.delete(corrId);
325
+ }
326
+ }
327
+ sink.emit(event);
328
+ };
329
+ function emitInvokeError(invokeId, corrId, error) {
330
+ report({ type: "invoke.error", invokeId, replayOf: corrId, origin: "gui", error, t: Date.now() });
331
+ }
332
+ function handleInvoke(msg) {
333
+ if (!invokeEnabled || !msg || !msg.__invoke) return;
334
+ const corrId = String(msg.__invoke.corrId ?? "");
335
+ const invokeId = String(msg.__invoke.invokeId ?? "");
336
+ if (!wrapped) return emitInvokeError(invokeId, corrId, "client-not-wrapped");
337
+ const rec = recentCalls.get(corrId);
338
+ if (!rec) return emitInvokeError(invokeId, corrId, "unknown-corrId");
339
+ const edited = msg.__invoke.args;
340
+ if (edited !== void 0 && !Array.isArray(edited)) return emitInvokeError(invokeId, corrId, "bad-args");
341
+ const args = edited !== void 0 ? edited : rec.args;
342
+ if (opts.canReplay && !opts.canReplay(rec.method, args)) return emitInvokeError(invokeId, corrId, "blocked-by-canReplay");
343
+ const [ns, m] = rec.method.split(".", 2);
344
+ const fn = m ? wrapped?.[ns]?.[m] : wrapped?.[ns];
345
+ if (typeof fn !== "function") return emitInvokeError(invokeId, corrId, "method-unavailable");
346
+ try {
347
+ setInvokeContext({ invokeId, replayOf: corrId, method: rec.method });
348
+ (m ? wrapped[ns][m] : wrapped[ns])(...args);
349
+ } catch (e) {
350
+ emitInvokeError(invokeId, corrId, "threw: " + String(e));
351
+ } finally {
352
+ setInvokeContext(null);
353
+ }
354
+ }
355
+ const attach = (stream) => {
356
+ if (deferred) deferred.attach(stream);
357
+ };
358
+ if (mode === "buffering" && info.isBareRuntime && opts.autoSwarm !== false) {
359
+ autoSwarm(opts.topic ?? DEFAULT_TOPIC, attach).catch(() => {
360
+ });
361
+ }
362
+ return {
363
+ sink,
364
+ flusher,
365
+ runtime: info.runtime,
366
+ mode,
367
+ redactor,
368
+ source,
369
+ report,
370
+ inflight: () => [...inflightCalls.values()],
371
+ wrapClient: (client, o) => {
372
+ wrapped = wrapClient(client, report, { ...o || {}, onCall: recordCall });
373
+ return wrapped;
374
+ },
375
+ bridgeTraces: (setTraceFunction) => {
376
+ let on = true;
377
+ try {
378
+ setTraceFunction((trace) => {
379
+ if (!on) return;
380
+ try {
381
+ const obj = trace && trace.object || {};
382
+ const caller = trace && trace.caller || {};
383
+ const data = caller.props;
384
+ const event = data && data.event || caller.functionName || "trace";
385
+ report({
386
+ type: "trace",
387
+ plane: "trace",
388
+ className: obj.className,
389
+ objId: obj.id,
390
+ objProps: obj.props,
391
+ method: (obj.className ? obj.className + "." : "") + event,
392
+ // GUI groups/labels by this
393
+ event,
394
+ data,
395
+ caller: caller.functionName ? caller.functionName + ":" + caller.line : void 0,
396
+ t: Date.now()
397
+ });
398
+ } catch {
399
+ }
400
+ });
401
+ } catch {
402
+ }
403
+ return () => {
404
+ on = false;
405
+ };
406
+ },
407
+ attach,
408
+ stop: () => {
409
+ flusher.stop();
410
+ reporter?.close();
411
+ }
412
+ };
413
+ }
414
+
415
+ // src/index.ts
416
+ import {
417
+ instrumentDataChannel,
418
+ instrumentPeerConnection
419
+ } from "hrpc-inspector-probe/adapters/webrtc";
420
+ import { instrumentWebSocket } from "hrpc-inspector-probe/adapters/websocket";
421
+ import { instrumentHyperswarmStream } from "hrpc-inspector-probe/adapters/hyperswarm";
422
+ import { CollectorSink } from "hrpc-inspector-probe/core/sink";
423
+ import { BatchFlusher as BatchFlusher2 } from "hrpc-inspector-probe/flush";
424
+ import { createHyperswarmExporter as createHyperswarmExporter2 } from "hrpc-inspector-probe/exporters/hyperswarm";
425
+ import { StreamFramer, encodeFrame } from "hrpc-inspector-probe/transport/framing";
426
+ export {
427
+ BatchFlusher2 as BatchFlusher,
428
+ CollectorSink,
429
+ DEFAULT_TOPIC,
430
+ StreamFramer,
431
+ createHyperswarmExporter2 as createHyperswarmExporter,
432
+ createWebSocketReporter,
433
+ detectRuntime,
434
+ encodeFrame,
435
+ instrumentDataChannel,
436
+ instrumentHyperswarmStream,
437
+ instrumentPeerConnection,
438
+ instrumentWebSocket,
439
+ observe,
440
+ wrapClient
441
+ };