@source-repo/rpc-cli 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1131 -0
  3. package/dist/bench.d.ts +66 -0
  4. package/dist/bench.d.ts.map +1 -0
  5. package/dist/bench.js +109 -0
  6. package/dist/bench.js.map +1 -0
  7. package/dist/broker.d.ts +61 -0
  8. package/dist/broker.d.ts.map +1 -0
  9. package/dist/broker.js +56 -0
  10. package/dist/broker.js.map +1 -0
  11. package/dist/bus.d.ts +142 -0
  12. package/dist/bus.d.ts.map +1 -0
  13. package/dist/bus.js +272 -0
  14. package/dist/bus.js.map +1 -0
  15. package/dist/bus.types.json +269 -0
  16. package/dist/conform.d.ts +75 -0
  17. package/dist/conform.d.ts.map +1 -0
  18. package/dist/conform.js +152 -0
  19. package/dist/conform.js.map +1 -0
  20. package/dist/console.d.ts +285 -0
  21. package/dist/console.d.ts.map +1 -0
  22. package/dist/console.js +686 -0
  23. package/dist/console.js.map +1 -0
  24. package/dist/console.types.json +1730 -0
  25. package/dist/extract.d.ts +37 -0
  26. package/dist/extract.d.ts.map +1 -0
  27. package/dist/extract.js +272 -0
  28. package/dist/extract.js.map +1 -0
  29. package/dist/fake.d.ts +58 -0
  30. package/dist/fake.d.ts.map +1 -0
  31. package/dist/fake.js +164 -0
  32. package/dist/fake.js.map +1 -0
  33. package/dist/index.d.ts +3 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +905 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/mcp.d.ts +15 -0
  38. package/dist/mcp.d.ts.map +1 -0
  39. package/dist/mcp.js +507 -0
  40. package/dist/mcp.js.map +1 -0
  41. package/dist/network.d.ts +58 -0
  42. package/dist/network.d.ts.map +1 -0
  43. package/dist/network.js +64 -0
  44. package/dist/network.js.map +1 -0
  45. package/dist/record.d.ts +74 -0
  46. package/dist/record.d.ts.map +1 -0
  47. package/dist/record.js +221 -0
  48. package/dist/record.js.map +1 -0
  49. package/dist/tapping.d.ts +11 -0
  50. package/dist/tapping.d.ts.map +1 -0
  51. package/dist/tapping.js +71 -0
  52. package/dist/tapping.js.map +1 -0
  53. package/dist/verbs.d.ts +59 -0
  54. package/dist/verbs.d.ts.map +1 -0
  55. package/dist/verbs.js +324 -0
  56. package/dist/verbs.js.map +1 -0
  57. package/dist/web/app.css +1 -0
  58. package/dist/web/app.js +2063 -0
  59. package/dist/web/app.js.map +1 -0
  60. package/dist/web/index.html +13 -0
  61. package/package.json +65 -0
@@ -0,0 +1,66 @@
1
+ import { type NetworkOptions } from './network.js';
2
+ /**
3
+ * Calling one method over and over, and reporting what it cost.
4
+ *
5
+ * `call` already returns the time a single call took, and the console shows it once and forgets it.
6
+ * The question a plant actually asks is the one a single call cannot answer: this device is fine at
7
+ * one call a second - what does it do at twenty? Finding the rate at which a device falls over is
8
+ * ordinarily done by writing a script, and the script is always the same script.
9
+ *
10
+ * Percentiles rather than an average, because an average hides exactly the calls worth knowing
11
+ * about: a device answering in 2 ms with one reply a second taking 4 seconds averages out to
12
+ * something that looks healthy.
13
+ */
14
+ export interface BenchOptions extends NetworkOptions {
15
+ peer: string;
16
+ namespace: string;
17
+ method: string;
18
+ /** Already coerced; `runBench` does that from the contract when given words. */
19
+ args: unknown[];
20
+ /** Calls per second to aim for. */
21
+ rate: number;
22
+ /** How long to keep going. */
23
+ forMs: number;
24
+ /** How many calls may be outstanding at once before the rest are counted as fallen behind. */
25
+ concurrency: number;
26
+ wait?: number;
27
+ }
28
+ export interface BenchReport {
29
+ peer: string;
30
+ method: string;
31
+ calls: number;
32
+ ok: number;
33
+ failed: number;
34
+ /**
35
+ * Calls not sent because too many were already outstanding. A device that cannot keep up shows
36
+ * here rather than in the latencies, where it would look like the network was fine.
37
+ */
38
+ behind: number;
39
+ /** How the failures broke down, by RPC error code. */
40
+ codes: {
41
+ [code: string]: number;
42
+ };
43
+ ms: {
44
+ min: number;
45
+ p50: number;
46
+ p90: number;
47
+ p95: number;
48
+ p99: number;
49
+ max: number;
50
+ mean: number;
51
+ };
52
+ rate: {
53
+ asked: number;
54
+ achieved: number;
55
+ };
56
+ ranForMs: number;
57
+ }
58
+ export declare const bench: (options: BenchOptions) => Promise<BenchReport>;
59
+ /** Coerces the words a shell gives against the peer's own contract, the way `call` does. */
60
+ export declare const benchArguments: (options: NetworkOptions & {
61
+ peer: string;
62
+ namespace: string;
63
+ method: string;
64
+ texts: string[];
65
+ }) => Promise<unknown[]>;
66
+ //# sourceMappingURL=bench.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bench.d.ts","sourceRoot":"","sources":["../src/bench.ts"],"names":[],"mappings":"AACA,OAAO,EAA6B,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAG7E;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,YAAa,SAAQ,cAAc;IAChD,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,IAAI,EAAE,OAAO,EAAE,CAAA;IACf,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,8FAA8F;IAC9F,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAA;IACd,sDAAsD;IACtD,KAAK,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAA;IACjC,EAAE,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IAClG,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;IACzC,QAAQ,EAAE,MAAM,CAAA;CACnB;AAYD,eAAO,MAAM,KAAK,YAAmB,YAAY,KAAG,OAAO,CAAC,WAAW,CAgFtE,CAAA;AAED,4FAA4F;AAC5F,eAAO,MAAM,cAAc,YAAmB,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,uBAYlI,CAAA"}
package/dist/bench.js ADDED
@@ -0,0 +1,109 @@
1
+ import { awaitPeer, connectNetwork } from './network.js';
2
+ import { coerceArguments } from './verbs.js';
3
+ const percentile = (sorted, fraction) => {
4
+ if (!sorted.length)
5
+ return 0;
6
+ // Nearest-rank: with 20 samples the 95th is the 19th, which is a sample that happened rather
7
+ // than an interpolation between two that did.
8
+ const rank = Math.min(sorted.length - 1, Math.ceil(fraction * sorted.length) - 1);
9
+ return sorted[Math.max(0, rank)];
10
+ };
11
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
12
+ export const bench = async (options) => {
13
+ const connected = await connectNetwork(options);
14
+ try {
15
+ if (!(await awaitPeer(connected, options.peer, options.wait ?? 5000)))
16
+ throw Object.assign(new Error(`${options.peer} did not appear within ${options.wait ?? 5000} ms`), { code: 'ClassNotFound' });
17
+ const proxy = await connected.network.proxy(options.namespace, options.peer);
18
+ const remote = proxy.remote;
19
+ const latencies = [];
20
+ const codes = {};
21
+ let ok = 0;
22
+ let failed = 0;
23
+ let behind = 0;
24
+ let inFlight = 0;
25
+ const outstanding = [];
26
+ const fire = () => {
27
+ inFlight++;
28
+ const started = Date.now();
29
+ const call = remote[options.method](...options.args)
30
+ .then(() => {
31
+ ok++;
32
+ latencies.push(Date.now() - started);
33
+ })
34
+ .catch((e) => {
35
+ failed++;
36
+ // Counted by code rather than lumped together: a device refusing arguments and
37
+ // a device that stopped answering are different findings with the same shape.
38
+ const code = e.code ?? 'Exception';
39
+ codes[code] = (codes[code] ?? 0) + 1;
40
+ latencies.push(Date.now() - started);
41
+ })
42
+ .finally(() => {
43
+ inFlight--;
44
+ });
45
+ outstanding.push(call);
46
+ };
47
+ const interval = options.rate > 0 ? 1000 / options.rate : 0;
48
+ const began = Date.now();
49
+ const deadline = began + options.forMs;
50
+ let next = began;
51
+ while (Date.now() < deadline) {
52
+ const waiting = next - Date.now();
53
+ if (waiting > 0)
54
+ await sleep(Math.min(waiting, deadline - Date.now()));
55
+ if (Date.now() >= deadline)
56
+ break;
57
+ // Not sent rather than queued: piling calls onto a device that is already behind
58
+ // measures the queue rather than the device.
59
+ if (inFlight >= options.concurrency)
60
+ behind++;
61
+ else
62
+ fire();
63
+ next = interval > 0 ? next + interval : Date.now();
64
+ }
65
+ // The last calls are waited out, so a run does not report latencies it never collected.
66
+ await Promise.allSettled(outstanding);
67
+ const ranForMs = Date.now() - began;
68
+ const sorted = [...latencies].sort((a, b) => a - b);
69
+ return {
70
+ peer: options.peer,
71
+ method: `${options.namespace}.${options.method}`,
72
+ calls: ok + failed,
73
+ ok,
74
+ failed,
75
+ behind,
76
+ codes,
77
+ ms: {
78
+ min: sorted[0] ?? 0,
79
+ p50: percentile(sorted, 0.5),
80
+ p90: percentile(sorted, 0.9),
81
+ p95: percentile(sorted, 0.95),
82
+ p99: percentile(sorted, 0.99),
83
+ max: sorted[sorted.length - 1] ?? 0,
84
+ mean: sorted.length ? Math.round(sorted.reduce((total, value) => total + value, 0) / sorted.length) : 0
85
+ },
86
+ rate: { asked: options.rate, achieved: ranForMs ? Math.round(((ok + failed) / ranForMs) * 1000 * 10) / 10 : 0 },
87
+ ranForMs
88
+ };
89
+ }
90
+ finally {
91
+ await connected.close();
92
+ }
93
+ };
94
+ /** Coerces the words a shell gives against the peer's own contract, the way `call` does. */
95
+ export const benchArguments = async (options) => {
96
+ const connected = await connectNetwork(options);
97
+ try {
98
+ const description = await connected.network
99
+ .proxy('msgrpc', options.peer)
100
+ .then((proxy) => proxy.remote.describe())
101
+ .catch(() => undefined);
102
+ const method = description?.namespaces.find((namespace) => namespace.name === options.namespace)?.methods.find((entry) => entry.name === options.method);
103
+ return coerceArguments(options.texts, method, description?.types);
104
+ }
105
+ finally {
106
+ await connected.close();
107
+ }
108
+ };
109
+ //# sourceMappingURL=bench.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bench.js","sourceRoot":"","sources":["../src/bench.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ import { RpcServer, type RpcAuthenticator } from '@source-repo/rpc';
2
+ import type { ServerOptions as TlsServerOptions } from 'node:https';
3
+ import { BusService } from './bus.js';
4
+ /**
5
+ * A socket.io broker: an RpcServer that relays, and says what it is relaying when asked.
6
+ *
7
+ * There is no separate broker implementation, and there should not be. A server already learns who
8
+ * is connected, forwards a frame addressed to another peer, and tells every peer who else is there.
9
+ * Running one that way is what makes it a bus rather than a service - and that is worth a command
10
+ * of its own only because typing it out is tedious, not because it is a different thing.
11
+ *
12
+ * It used to expose nothing at all, so a peer addressing it by name got ClassNotFound - true, and
13
+ * the plainest possible statement that this is a switchboard. It now exposes exactly one namespace,
14
+ * `bus`, because the alternative was a --tap flag, and a plant bus that has to be restarted before
15
+ * it can be watched will not be watched: the run worth looking at is the one already going wrong.
16
+ * The consequence is stated on startup, since anyone who can reach an unauthenticated broker can
17
+ * now mirror everything crossing it - which they could always have done by impersonating a peer,
18
+ * but not this conveniently. `authenticate` and `relay` are what gate it.
19
+ *
20
+ * With `authenticate` it gates the whole bus rather than just the tap: a peer that presents no
21
+ * token this broker knows never reaches the RPC layer, and one that does may only claim the name
22
+ * its token was issued for. That is the difference between a bus behind a trusted network and a bus
23
+ * that can be put on one that is not.
24
+ *
25
+ * With `upstream` it dials another broker as well, which makes the two one network: each side's
26
+ * peers are advertised to the other, and a call crosses without either end knowing there was a hop.
27
+ */
28
+ export interface BrokerOptions {
29
+ /** Listens on every interface: the library's socket.io transport takes no bind address. */
30
+ port: number;
31
+ name: string;
32
+ /** Brokers to join. Peers here become reachable from there, and the other way round. */
33
+ upstream?: string[];
34
+ /**
35
+ * Certificate and key, which is what makes this bus wss:// rather than ws://. Absent means
36
+ * plaintext, which is right on a segment that is already isolated or behind a terminating proxy.
37
+ */
38
+ tls?: TlsServerOptions;
39
+ /**
40
+ * Verify what a peer presents when it dials in. Without it the broker relays for anyone who can
41
+ * reach the port, and every peer name on it is an unchecked claim.
42
+ */
43
+ authenticate?: RpcAuthenticator;
44
+ /**
45
+ * Presented to each upstream, for joining a broker that authenticates. One value for all of
46
+ * them: a token names the peer that holds it, and this broker is one peer however many brokers
47
+ * it joins, so the same token is the right thing to send to each.
48
+ */
49
+ upstreamCredentials?: unknown;
50
+ /** Called for every arrival and departure, so a command line can show the network filling up. */
51
+ onPeer?: (peer: string, state: 'online' | 'offline', where: string) => void;
52
+ }
53
+ export declare const startBroker: (options: BrokerOptions) => Promise<{
54
+ server: RpcServer;
55
+ /** The tap, so an embedder can watch frames without going through RPC to reach it. */
56
+ bus: BusService;
57
+ /** Peers reachable through this broker right now, including any learned from an upstream. */
58
+ peers: () => string[];
59
+ close: () => Promise<void>;
60
+ }>;
61
+ //# sourceMappingURL=broker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broker.d.ts","sourceRoot":"","sources":["../src/broker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAqC,KAAK,gBAAgB,EAAkC,MAAM,kBAAkB,CAAA;AACtI,OAAO,KAAK,EAAE,aAAa,IAAI,gBAAgB,EAAE,MAAM,YAAY,CAAA;AACnE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAKrC;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,WAAW,aAAa;IAC1B,2FAA2F;IAC3F,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB;;;OAGG;IACH,GAAG,CAAC,EAAE,gBAAgB,CAAA;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE,gBAAgB,CAAA;IAC/B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,iGAAiG;IACjG,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAC9E;AAED,eAAO,MAAM,WAAW,YAAmB,aAAa;;IAyChD,sFAAsF;;IAEtF,6FAA6F;;;EAOpG,CAAA"}
package/dist/broker.js ADDED
@@ -0,0 +1,56 @@
1
+ import { RpcServer, TransportEvent } from '@source-repo/rpc';
2
+ import { BusService } from './bus.js';
3
+ // Extracted from bus.ts by `npm run contract` and committed, so a console pointed at a broker gets
4
+ // argument fields for tap() rather than `tap(…)`.
5
+ import contract from './bus.types.json' with { type: 'json' };
6
+ export const startBroker = async (options) => {
7
+ const upstream = options.upstream ?? [];
8
+ const bus = new BusService(options.name);
9
+ const server = new RpcServer({
10
+ name: options.name,
11
+ transports: [
12
+ { port: options.port, ...(options.tls ? { tls: options.tls } : {}) },
13
+ ...upstream.map((url) => ({ connect: url, ...(options.upstreamCredentials ? { credentials: options.upstreamCredentials } : {}) }))
14
+ ],
15
+ ...(options.authenticate ? { authenticate: options.authenticate } : {}),
16
+ // The tap is the only thing here, and it describes itself: a console pointed at a broker
17
+ // used to be told ClassNotFound, which is indistinguishable from a device whose server was
18
+ // started without exposeIntrospection.
19
+ schema: contract,
20
+ exposeIntrospection: true,
21
+ readyTimeout: 15000
22
+ });
23
+ server.exposeClassInstance(bus);
24
+ try {
25
+ // ready() reports a listener that cannot bind rather than waiting it out.
26
+ await server.ready();
27
+ }
28
+ catch (e) {
29
+ // A broker that could not start still holds a socket.io server and whatever upstream links
30
+ // it opened. Throwing without closing them leaves them behind for the life of the process.
31
+ await server.close().catch(() => undefined);
32
+ throw e;
33
+ }
34
+ const where = (transport, index) => (index === 0 ? `:${options.port}` : (upstream[index - 1] ?? transport.getName()));
35
+ server.transports.forEach((transport, index) => {
36
+ transport.on(TransportEvent.peerOnline, (peer) => options.onPeer?.(peer, 'online', where(transport, index)));
37
+ transport.on(TransportEvent.peerGone, (peer) => options.onPeer?.(peer, 'offline', where(transport, index)));
38
+ // Attached for the life of the broker rather than added and removed with each tap: the
39
+ // transport only emits this when something is listening, and `observe` returns on the first
40
+ // line when no tap matches. Attaching on demand would mean a tap started mid-flight seeing
41
+ // the traffic from whenever the listener happened to land instead.
42
+ transport.on(TransportEvent.relayed, (relayed) => bus.observe(relayed));
43
+ });
44
+ return {
45
+ server,
46
+ /** The tap, so an embedder can watch frames without going through RPC to reach it. */
47
+ bus,
48
+ /** Peers reachable through this broker right now, including any learned from an upstream. */
49
+ peers: () => server.peers.names().filter((peer) => peer !== options.name).sort(),
50
+ close: async () => {
51
+ await bus.releaseAll();
52
+ await server.close();
53
+ }
54
+ };
55
+ };
56
+ //# sourceMappingURL=broker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broker.js","sourceRoot":"","sources":["../src/broker.ts"],"names":[],"mappings":"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"}
package/dist/bus.d.ts ADDED
@@ -0,0 +1,142 @@
1
+ import { EventEmitter } from 'events';
2
+ import { type RelayedFrame } from '@source-repo/rpc';
3
+ /**
4
+ * The traffic tap: what the broker is relaying, turned on while it runs.
5
+ *
6
+ * A console can only ever see its own calls and the events it subscribed to, which on a real
7
+ * network is a small fraction of what is happening. The broker sees everything by definition - it
8
+ * is the thing forwarding it - so this is where the rest becomes visible.
9
+ *
10
+ * **It is turned on by a call, not by a flag.** A plant bus that has to be restarted to be observed
11
+ * will not be observed: the run you want to look at is the one already going wrong. The cost of
12
+ * that decision is that the broker now exposes something, where it used to expose nothing at all
13
+ * and answer ClassNotFound to anyone who addressed it - see the note in broker.ts.
14
+ *
15
+ * What this offers over pointing a generic MQTT monitor at the same wire is that it knows what a
16
+ * frame *is*. A call and its reply share a correlation id, so they can be paired and the reply
17
+ * reported with the time it took and the method it answers - neither of which is in the reply
18
+ * itself. A topic browser shows you a MsgPack blob; this shows you
19
+ * `plantServer.plant.writeSetpoint(1200) -> ok 42ms`.
20
+ */
21
+ /** What a tap asks to be shown. Everything is optional; a tap with no filter sees all of it. */
22
+ export interface TapFilter {
23
+ /** Only frames this peer sent or received - "mirror that device", which is the usual request. */
24
+ peer?: string;
25
+ /** Only this namespace. Applies to replies too, since a reply is paired with its call first. */
26
+ namespace?: string;
27
+ /** Only these kinds: POST, SUCCESS, ERROR, EVENT. */
28
+ kinds?: string[];
29
+ /**
30
+ * Include arguments, results and event payloads.
31
+ *
32
+ * Off by default, and deliberately: the metadata is what a debugging session usually needs, and
33
+ * a plant bus carries values that nobody meant to hand to whoever happened to be tapping.
34
+ */
35
+ payloads?: boolean;
36
+ /**
37
+ * Seconds before this tap drops itself. A console that closes without untapping would otherwise
38
+ * leave the broker building and emitting frames for a subscriber that is not there any more.
39
+ */
40
+ ttl?: number;
41
+ }
42
+ /** One frame the broker passed between two peers. */
43
+ export interface TappedFrame {
44
+ at: number;
45
+ source: string;
46
+ target: string;
47
+ /** POST, SUCCESS, ERROR or EVENT. */
48
+ kind: string;
49
+ /** The namespace, taken from the call when this is a reply - a reply does not carry one. */
50
+ namespace?: string;
51
+ method?: string;
52
+ event?: string;
53
+ /** Correlation id, so a call and its reply can be lined up by whoever is reading. */
54
+ id?: string;
55
+ /** Milliseconds since the call this answers, when that call was seen. */
56
+ ms?: number;
57
+ /** The error code on a refused call. */
58
+ code?: string;
59
+ error?: string;
60
+ /** Only when a matching tap asked for payloads. */
61
+ params?: unknown[];
62
+ result?: unknown;
63
+ /** Which taps this frame matched, so several people can watch with different filters at once. */
64
+ taps: string[];
65
+ }
66
+ export interface TapRecord {
67
+ token: string;
68
+ filter: TapFilter;
69
+ /** Epoch milliseconds. */
70
+ expires: number;
71
+ frames: number;
72
+ }
73
+ /** Exported so the console can give its own record of a tap the same life as the tap itself. */
74
+ export declare const DEFAULT_TAP_TTL = 300;
75
+ export declare class BusService extends EventEmitter {
76
+ /** Named in what `taps()` reports, so a reader knows which broker they are looking at. */
77
+ private readonly busName;
78
+ rpcEvents: {
79
+ frame: [frame: TappedFrame];
80
+ };
81
+ private readonly active;
82
+ /** Calls seen and not yet answered, so a reply can be given the time it took and the method. */
83
+ private readonly pending;
84
+ private counter;
85
+ constructor(
86
+ /** Named in what `taps()` reports, so a reader knows which broker they are looking at. */
87
+ busName: string);
88
+ /**
89
+ * Opened when the first tap starts and closed after the last one ends, for a source that costs
90
+ * something to hold open - an MQTT subscription to every peer's traffic, in particular. A broker
91
+ * needs none of this: it is already forwarding the frames, and `observe` returns on its first
92
+ * line when nothing is watching.
93
+ */
94
+ onDemand?: {
95
+ start: () => Promise<void>;
96
+ stop: () => Promise<void>;
97
+ };
98
+ /**
99
+ * Starts or stops the source to match whether anything is watching.
100
+ *
101
+ * Failures on the way down are swallowed and failures on the way up are not: a tap that could
102
+ * not subscribe has to say so, or it reports an empty network and looks like a quiet one.
103
+ */
104
+ private settleSource;
105
+ /**
106
+ * Start watching. Returns a token: several people can tap at once with different filters, and
107
+ * untapping has to remove yours rather than everyone's.
108
+ */
109
+ tap(filter?: TapFilter): Promise<{
110
+ token: string;
111
+ expires: number;
112
+ filter: TapFilter;
113
+ }>;
114
+ untap(token: string): Promise<{
115
+ tapping: boolean;
116
+ already: boolean;
117
+ }>;
118
+ /** Who is watching what, including how much each tap has seen. */
119
+ taps(): Promise<{
120
+ bus: string;
121
+ taps: TapRecord[];
122
+ pending: number;
123
+ }>;
124
+ /** Whether anything is listening, so the broker can leave the transport hook unattached. */
125
+ get tapping(): boolean;
126
+ private expire;
127
+ /**
128
+ * One relayed frame, from the transport that forwarded it.
129
+ *
130
+ * Everything here is best-effort reading of someone else's message: a frame whose payload is
131
+ * not the shape we expect is reported with what could be read rather than dropped, since a
132
+ * malformed frame on the bus is exactly the sort of thing a tap exists to show.
133
+ */
134
+ observe(relayed: RelayedFrame): void;
135
+ private matches;
136
+ private readFrame;
137
+ /** Fills in what a reply does not carry: the method it answers, and how long it took. */
138
+ private pair;
139
+ /** Drops every tap, so a broker shutting down stops building frames for nobody. */
140
+ releaseAll(): Promise<void>;
141
+ }
142
+ //# sourceMappingURL=bus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bus.d.ts","sourceRoot":"","sources":["../src/bus.ts"],"names":[],"mappings":"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"}