hrpc-inspector 0.0.0 → 0.1.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/dist/index.js ADDED
@@ -0,0 +1,442 @@
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 enc = new TextEncoder();
251
+ const topicKey = crypto.hash(enc.encode(topic));
252
+ swarm.on("connection", (conn) => onConnection(conn));
253
+ await swarm.join(topicKey, { client: true, server: false }).flushed();
254
+ }
255
+ function observe(opts = {}) {
256
+ const info = detectRuntime();
257
+ const source = resolveSource(opts.source, info.runtime);
258
+ const redactOn = opts.redact ?? (opts.websocket ? false : true);
259
+ const redactor = opts.redactor ?? (redactOn ? new Redactor({ bodyAllowlist: opts.bodyAllowlist }) : null);
260
+ const wireSource = redactor ? redactSource(source, redactor) : source;
261
+ const invokeEnabled = opts.allowInvoke === true && !!opts.websocket;
262
+ const replayLogMax = opts.replayLogMax ?? 2e3;
263
+ const recentCalls = /* @__PURE__ */ new Map();
264
+ const recordCall = invokeEnabled ? (corrId, method, args) => {
265
+ recentCalls.set(corrId, { method, args });
266
+ if (recentCalls.size > replayLogMax) {
267
+ const oldest = recentCalls.keys().next().value;
268
+ if (oldest !== void 0) recentCalls.delete(oldest);
269
+ }
270
+ } : void 0;
271
+ let wrapped = null;
272
+ let exporter;
273
+ let reporter = null;
274
+ let deferred = null;
275
+ let mode;
276
+ if (opts.exporter) {
277
+ exporter = opts.exporter;
278
+ mode = "custom";
279
+ } else if (opts.websocket) {
280
+ reporter = createWebSocketReporter(opts.websocket, {
281
+ // Advertise the replay capability so the GUI shows Replay only for opted-in sources.
282
+ announce: invokeEnabled ? { ...wireSource, caps: { invoke: true } } : wireSource,
283
+ // Inbound handler ONLY when replay is enabled — otherwise the reporter stays send-only.
284
+ onMessage: invokeEnabled ? handleInvoke : void 0
285
+ });
286
+ exporter = reporter;
287
+ mode = "websocket";
288
+ } else if (opts.stream) {
289
+ exporter = createHyperswarmExporter(opts.stream);
290
+ mode = "stream";
291
+ } else {
292
+ deferred = createDeferredExporter();
293
+ exporter = deferred;
294
+ mode = "buffering";
295
+ }
296
+ const flusher = new BatchFlusher({
297
+ intervalMs: opts.intervalMs ?? 200,
298
+ onFlush: (batch) => {
299
+ const out = redactor ? redactor.redactBatch(batch) : batch;
300
+ exporter.export(out.map((e) => ({ ...e, src: wireSource.id })));
301
+ }
302
+ });
303
+ flusher.start();
304
+ const sink = {
305
+ emit(event) {
306
+ try {
307
+ flusher.add(event);
308
+ } catch {
309
+ }
310
+ }
311
+ };
312
+ const INFLIGHT_MAX = 1e3;
313
+ const inflightCalls = /* @__PURE__ */ new Map();
314
+ const report = (event) => {
315
+ const e = event;
316
+ const corrId = e && typeof e.corrId === "string" ? e.corrId : null;
317
+ if (corrId) {
318
+ if (e.type === "request.start") {
319
+ inflightCalls.set(corrId, { method: String(e.method ?? ""), corrId, t: typeof e.t === "number" ? e.t : Date.now() });
320
+ if (inflightCalls.size > INFLIGHT_MAX) {
321
+ const oldest = inflightCalls.keys().next().value;
322
+ if (oldest !== void 0) inflightCalls.delete(oldest);
323
+ }
324
+ } else if (e.type === "stream.open" || e.type === "request.end") {
325
+ inflightCalls.delete(corrId);
326
+ }
327
+ }
328
+ sink.emit(event);
329
+ };
330
+ function emitInvokeError(invokeId, corrId, error) {
331
+ report({ type: "invoke.error", invokeId, replayOf: corrId, origin: "gui", error, t: Date.now() });
332
+ }
333
+ function handleInvoke(msg) {
334
+ if (!invokeEnabled || !msg || !msg.__invoke) return;
335
+ const corrId = String(msg.__invoke.corrId ?? "");
336
+ const invokeId = String(msg.__invoke.invokeId ?? "");
337
+ if (!wrapped) return emitInvokeError(invokeId, corrId, "client-not-wrapped");
338
+ const rec = recentCalls.get(corrId);
339
+ if (!rec) return emitInvokeError(invokeId, corrId, "unknown-corrId");
340
+ const edited = msg.__invoke.args;
341
+ if (edited !== void 0 && !Array.isArray(edited)) return emitInvokeError(invokeId, corrId, "bad-args");
342
+ const args = edited !== void 0 ? edited : rec.args;
343
+ if (opts.canReplay && !opts.canReplay(rec.method, args)) return emitInvokeError(invokeId, corrId, "blocked-by-canReplay");
344
+ const [ns, m] = rec.method.split(".", 2);
345
+ const fn = m ? wrapped?.[ns]?.[m] : wrapped?.[ns];
346
+ if (typeof fn !== "function") return emitInvokeError(invokeId, corrId, "method-unavailable");
347
+ try {
348
+ setInvokeContext({ invokeId, replayOf: corrId, method: rec.method });
349
+ (m ? wrapped[ns][m] : wrapped[ns])(...args);
350
+ } catch (e) {
351
+ emitInvokeError(invokeId, corrId, "threw: " + String(e));
352
+ } finally {
353
+ setInvokeContext(null);
354
+ }
355
+ }
356
+ const attach = (stream) => {
357
+ if (deferred) deferred.attach(stream);
358
+ };
359
+ if (mode === "buffering" && info.isBareRuntime && opts.autoSwarm !== false) {
360
+ autoSwarm(opts.topic ?? DEFAULT_TOPIC, attach).catch(() => {
361
+ });
362
+ }
363
+ return {
364
+ sink,
365
+ flusher,
366
+ runtime: info.runtime,
367
+ mode,
368
+ redactor,
369
+ source,
370
+ report,
371
+ inflight: () => [...inflightCalls.values()],
372
+ wrapClient: (client, o) => {
373
+ wrapped = wrapClient(client, report, { ...o || {}, onCall: recordCall });
374
+ return wrapped;
375
+ },
376
+ bridgeTraces: (setTraceFunction) => {
377
+ let on = true;
378
+ try {
379
+ setTraceFunction((trace) => {
380
+ if (!on) return;
381
+ try {
382
+ const obj = trace && trace.object || {};
383
+ const caller = trace && trace.caller || {};
384
+ const data = caller.props;
385
+ const event = data && data.event || caller.functionName || "trace";
386
+ report({
387
+ type: "trace",
388
+ plane: "trace",
389
+ className: obj.className,
390
+ objId: obj.id,
391
+ objProps: obj.props,
392
+ method: (obj.className ? obj.className + "." : "") + event,
393
+ // GUI groups/labels by this
394
+ event,
395
+ data,
396
+ caller: caller.functionName ? caller.functionName + ":" + caller.line : void 0,
397
+ t: Date.now()
398
+ });
399
+ } catch {
400
+ }
401
+ });
402
+ } catch {
403
+ }
404
+ return () => {
405
+ on = false;
406
+ };
407
+ },
408
+ attach,
409
+ stop: () => {
410
+ flusher.stop();
411
+ reporter?.close();
412
+ }
413
+ };
414
+ }
415
+
416
+ // src/index.ts
417
+ import {
418
+ instrumentDataChannel,
419
+ instrumentPeerConnection
420
+ } from "hrpc-inspector-probe/adapters/webrtc";
421
+ import { instrumentWebSocket } from "hrpc-inspector-probe/adapters/websocket";
422
+ import { instrumentHyperswarmStream } from "hrpc-inspector-probe/adapters/hyperswarm";
423
+ import { CollectorSink } from "hrpc-inspector-probe/core/sink";
424
+ import { BatchFlusher as BatchFlusher2 } from "hrpc-inspector-probe/flush";
425
+ import { createHyperswarmExporter as createHyperswarmExporter2 } from "hrpc-inspector-probe/exporters/hyperswarm";
426
+ import { StreamFramer, encodeFrame } from "hrpc-inspector-probe/transport/framing";
427
+ export {
428
+ BatchFlusher2 as BatchFlusher,
429
+ CollectorSink,
430
+ DEFAULT_TOPIC,
431
+ StreamFramer,
432
+ createHyperswarmExporter2 as createHyperswarmExporter,
433
+ createWebSocketReporter,
434
+ detectRuntime,
435
+ encodeFrame,
436
+ instrumentDataChannel,
437
+ instrumentHyperswarmStream,
438
+ instrumentPeerConnection,
439
+ instrumentWebSocket,
440
+ observe,
441
+ wrapClient
442
+ };
@@ -0,0 +1,119 @@
1
+ import { BatchFlusher } from 'hrpc-inspector-probe/flush';
2
+ import type { WritableStreamLike, HyperswarmExporter } from 'hrpc-inspector-probe/exporters/hyperswarm';
3
+ import { Redactor } from 'hrpc-inspector-probe/redactor';
4
+ import type { EventSink, L2Event } from 'hrpc-inspector-probe/sink';
5
+ import type { Runtime } from './detect.js';
6
+ import type { Report, WrapClientOptions } from './wrap-client.js';
7
+ export declare const DEFAULT_TOPIC = "p2p-observability-hub:v0";
8
+ export interface SourceInfo {
9
+ /** Stable id used to group/select this source in the GUI. */
10
+ id: string;
11
+ /** Human label shown in the selector. */
12
+ label: string;
13
+ /** Detected runtime (react-native | pear | bare | electron | node | browser). */
14
+ runtime: string;
15
+ /** Physical device id (client-supplied; e.g. an app's own options.deviceId). Lets the GUI select by device. */
16
+ deviceId?: string;
17
+ /** App / install instance id — distinguishes MULTIPLE apps sharing one package. */
18
+ appId?: string;
19
+ [extra: string]: unknown;
20
+ }
21
+ export interface ObserveOptions {
22
+ /** Flush cadence; must be in the collector's verified 100-250ms window. Default 200. */
23
+ intervalMs?: number;
24
+ /** Explicit hub connection (a NoiseSecretStream / socket). Skips auto-swarm. */
25
+ stream?: WritableStreamLike;
26
+ /** Fully custom exporter (e.g. Rozenite bridge, OTLP). Skips everything else. */
27
+ exporter?: HyperswarmExporter;
28
+ /** Hub topic string; must match the hub's. Default DEFAULT_TOPIC. */
29
+ topic?: string;
30
+ /** Set false to disable Bare/Pear auto-dial (then attach() a stream yourself). */
31
+ autoSwarm?: boolean;
32
+ /**
33
+ * Send events to a local GUI over WebSocket instead of the Hyperswarm hub — the reliable
34
+ * dev-viewer path for EVERY runtime (RN / Pear / desktop / browser),
35
+ * e.g. 'ws://127.0.0.1:9420/ws'. When set, redaction defaults OFF (local viewing);
36
+ * pass redact:true to force it. Takes precedence over stream/auto-swarm.
37
+ */
38
+ websocket?: string;
39
+ /**
40
+ * Redact every event on the way OUT (peer ids hashed, URLs host-only, bodies off)
41
+ * — ON by default, because the export path (hub / .p2plog) leaves the device.
42
+ * Set false ONLY for a purely-local view that never leaves the machine (F3/F4/F9).
43
+ */
44
+ redact?: boolean;
45
+ /** Supply a shared Redactor (e.g. to reuse its local peer reverse-map). Default: a fresh one. */
46
+ redactor?: Redactor;
47
+ /** Per-`method` opt-in allowlist for full payload bodies (default: none — bodies summarised). */
48
+ bodyAllowlist?: Iterable<string>;
49
+ /**
50
+ * Identity of THIS device/app, for the GUI's device & app selectors. A string label, or an
51
+ * object `{ id?, label?, deviceId?, appId?, app?, device?, variant? }`.
52
+ * • deviceId — physical device (client-supplied; e.g. the app's own options.deviceId) → select by device
53
+ * • appId — app/install instance → distinguishes MULTIPLE apps sharing one package
54
+ * Omit → auto: browser reuses localStorage for a stable id (clears on delete-data); other
55
+ * runtimes get a session id (pass deviceId/appId for stability). Only the client knows its
56
+ * app name / device / variant, so it supplies those.
57
+ */
58
+ source?: string | Record<string, unknown>;
59
+ /**
60
+ * Allow the GUI to REPLAY a logged call back into this client (re-run a call the app already
61
+ * made). Boundary: the GUI references a call by corrId and the METHOD is resolved from this
62
+ * client's own call log — it never crosses the wire, so a replay cannot be redirected to a
63
+ * method the app never called. The ARGS may be supplied by the GUI (edit-and-replay) and are
64
+ * NOT validated here beyond "must be a JSON array"; use `canReplay` to check them.
65
+ *
66
+ * Fails CLOSED: must be set to `true` EXPLICITLY. There is no dev-detection default — omit it
67
+ * and replay is off, so a shipped build never installs the inbound handler. Only meaningful
68
+ * together with `websocket`.
69
+ */
70
+ allowInvoke?: boolean;
71
+ /**
72
+ * Optional gate consulted before a replay runs — return false to block (e.g. to refuse
73
+ * re-running a mutating method like a send). Default: allow. Receives the logged method and the
74
+ * args that will actually be used — which may be UNVALIDATED, GUI-supplied args. This is the
75
+ * only argument check in the system, so an app whose methods mutate state should implement it.
76
+ */
77
+ canReplay?: (method: string, args: unknown[]) => boolean;
78
+ /** Max logged calls retained for replay (corrId→method+args). Default 500. */
79
+ replayLogMax?: number;
80
+ }
81
+ export interface ObserveHandle {
82
+ /** Wire your L1 adapters into this: `instrumentDataChannel(dc, peerId, obs.sink)`. */
83
+ sink: EventSink;
84
+ flusher: BatchFlusher<L2Event>;
85
+ runtime: Runtime;
86
+ mode: 'websocket' | 'stream' | 'custom' | 'buffering';
87
+ /** The redactor applied on export (null if redact:false). Use `.resolvePeer()` for a local names view. */
88
+ redactor: Redactor | null;
89
+ /** This device/app's resolved identity — stamped on every event as `src` and announced to the GUI. */
90
+ source: SourceInfo;
91
+ /** Report one event directly to the sink (this is what wrapClient uses). */
92
+ report: Report;
93
+ /** Wrap an RPC/service client so its calls + subscription streams are observed. */
94
+ wrapClient<T extends Record<string, any>>(client: T, opts?: WrapClientOptions): T;
95
+ /**
96
+ * Snapshot of request/response calls currently in flight — started (request.start) but not yet
97
+ * settled (request.end). Each `{ method, corrId, t }`. Subscription streams are not included (see
98
+ * observe.ts). Wire this to a thread-occupancy probe's `getInflight` so a stall names the call
99
+ * that was running (e.g. hrpc-inspector-threads).
100
+ */
101
+ inflight(): Array<{
102
+ method: string;
103
+ corrId: string;
104
+ t: number;
105
+ }>;
106
+ /**
107
+ * Bridge a hypertrace tracer into the SAME event stream: pass hypertrace's `setTraceFunction`
108
+ * and every `trace()` from instrumented classes (e.g. a WebRTC/call layer) becomes a
109
+ * `type:'trace'` event on a distinct "trace" plane in the GUI. Install this BEFORE the traced
110
+ * objects are constructed (hypertrace only traces objects created after the function is set).
111
+ * Dependency-free: the app supplies `setTraceFunction`, so the package never imports hypertrace.
112
+ * Returns a disposer that stops forwarding.
113
+ */
114
+ bridgeTraces(setTraceFunction: (fn: (trace: any) => void) => void): () => void;
115
+ /** Attach a hub connection later (auto-swarm calls this on 'connection'). */
116
+ attach(stream: WritableStreamLike): void;
117
+ stop(): void;
118
+ }
119
+ export declare function observe(opts?: ObserveOptions): ObserveHandle;