@camstack/system 1.2.218 → 1.2.220
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/addon-runner.js +2 -2
- package/dist/addon-runner.mjs +2 -2
- package/dist/addon-utils.js +2 -2
- package/dist/addon-utils.mjs +2 -2
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/gpu-probe.d.ts +18 -0
- package/dist/builtins/native-metrics/native-metrics-provider.d.ts +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.d.ts +26 -3
- package/dist/builtins/native-metrics/native-metrics.addon.js +1320 -1093
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1320 -1096
- package/dist/builtins/native-metrics/proc-process-table.d.ts +25 -0
- package/dist/builtins/platform-probe/index.js +3 -3
- package/dist/builtins/platform-probe/index.mjs +3 -3
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-pragmas.d.ts +23 -0
- package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +1 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/sqlite-storage/wal-checkpoint-policy.d.ts +115 -0
- package/dist/builtins/sqlite-storage/wal-checkpoint-worker.d.ts +1 -0
- package/dist/builtins/sqlite-storage/wal-checkpointer.d.ts +70 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{custom-action-registry-BDwM5qAK.js → custom-action-registry-B42MC_Pv.js} +1 -1
- package/dist/{custom-action-registry-CsONUlhC.mjs → custom-action-registry-CpR58DXT.mjs} +1 -1
- package/dist/{dist-DigmGsQS.mjs → dist-Ca_jaxXs.mjs} +232 -4
- package/dist/{dist-BOlMCOeT.js → dist-CllKpcTd.js} +232 -4
- package/dist/index.d.ts +6 -1
- package/dist/index.js +99 -65
- package/dist/index.mjs +84 -67
- package/dist/kernel/gc-census.d.ts +144 -0
- package/dist/kernel/heap-watch.d.ts +32 -1
- package/dist/kernel/moleculer/mesh-queue-report.d.ts +51 -0
- package/dist/kernel/outbound-queue-watch.d.ts +94 -0
- package/dist/kernel/socket-plane-report.d.ts +112 -4
- package/dist/kernel/transport/cap-action-census.d.ts +218 -0
- package/dist/kernel/transport/cap-child-index.d.ts +49 -0
- package/dist/kernel/transport/cap-routing-hints.d.ts +21 -0
- package/dist/kernel/transport/child-cap-protocol.d.ts +28 -2
- package/dist/kernel/transport/frame-codec.d.ts +49 -0
- package/dist/kernel/transport/index.d.ts +2 -0
- package/dist/kernel/transport/local-child-client.d.ts +7 -0
- package/dist/kernel/transport/local-child-registry.d.ts +99 -5
- package/dist/kernel/transport/local-transport.d.ts +54 -2
- package/dist/kernel/transport/socket-channel.d.ts +40 -1
- package/dist/{manifest-system-deps-Bo92lg5u.js → manifest-system-deps-DfpwaXkh.js} +1725 -614
- package/dist/{manifest-system-deps-D--wyMD9.mjs → manifest-system-deps-iadzxSBr.mjs} +1850 -841
- package/dist/process/proc-stat.d.ts +75 -0
- package/dist/process/resource-monitor.d.ts +9 -1
- package/dist/{resource-monitor-CdnzxBLP.js → resource-monitor-LVE1BoLs.js} +42 -23
- package/dist/{resource-monitor-BWmQ5i-o.mjs → resource-monitor-Vp0bPV3U.mjs} +42 -22
- package/dist/{retired-settings-keys-xEAlYnv7.js → retired-settings-keys-7iEsSzmD.js} +1 -1
- package/dist/{retired-settings-keys-vK_U-CG2.mjs → retired-settings-keys-DQnfr__p.mjs} +1 -1
- package/dist/wal-checkpoint-policy-CAcg63o-.mjs +92 -0
- package/dist/wal-checkpoint-policy-CCJZngds.js +127 -0
- package/dist/wal-checkpoint-worker.js +138 -0
- package/dist/wal-checkpoint-worker.mjs +137 -0
- package/package.json +1 -1
- package/dist/{event-loop-stall-monitor-DM9OAzy8.js → event-loop-stall-monitor-CAsHdMwP.js} +1 -1
- package/dist/{event-loop-stall-monitor-B3CFd9tM.mjs → event-loop-stall-monitor-GPrVvx14.mjs} +1 -1
- package/dist/{tls-DOTmtLCW.mjs → tls-2xbgg78V.mjs} +2 -2
- package/dist/{tls-BxQlomxd.js → tls-BFs4PzoW.js} +2 -2
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cap-action-census — which cap METHOD moves the bytes, and how much of each
|
|
3
|
+
* one this parent forwarded without ever reading it.
|
|
4
|
+
*
|
|
5
|
+
* ## Why `socket-traffic.ts` keeps no method, and why this is a separate file
|
|
6
|
+
*
|
|
7
|
+
* Read `socket-traffic.ts` before changing anything here. It does not omit the
|
|
8
|
+
* method by oversight; three separate properties of that file forbid it, and
|
|
9
|
+
* this module exists because each can be answered somewhere else rather than
|
|
10
|
+
* relaxed:
|
|
11
|
+
*
|
|
12
|
+
* 1. **The channel does not know one.** `SocketChannel` sees a {@link
|
|
13
|
+
* import('./local-transport.js').Frame} — `{ k, id, body }` — and `body` is
|
|
14
|
+
* `unknown` by contract. The cap envelope lives one layer up, in
|
|
15
|
+
* `child-cap-protocol.ts`. A counter inside the channel that reached into
|
|
16
|
+
* `body.capName` would make the transport depend on the protocol it
|
|
17
|
+
* carries. So the LABEL is supplied from above ({@link capActionLabel}) and
|
|
18
|
+
* only the BYTES come from the wire.
|
|
19
|
+
*
|
|
20
|
+
* 2. **A `res` frame carries no method at all** — only its correlation `id`.
|
|
21
|
+
* And the responses are the whole question: measured 2026-09-11, hub-main
|
|
22
|
+
* wrote 144–262 MB/min to `hub/pipeline-analytics` on 1 966–2 411 responses
|
|
23
|
+
* (a 73–108 kB mean) against 2 kB of requests. Charging those bytes to a
|
|
24
|
+
* method is therefore not a counter at all, it is a CORRELATION: the label
|
|
25
|
+
* is taken off the `req`, held against its id, and settled when the `res`
|
|
26
|
+
* for that id is written. `socket-traffic.ts` is a two-addition hot path
|
|
27
|
+
* with no state per message; this is state per in-flight request, and the
|
|
28
|
+
* two do not belong in one file.
|
|
29
|
+
*
|
|
30
|
+
* 3. **Cardinality.** `recordSocketFrame` costs 2.37–2.41 ns and allocates
|
|
31
|
+
* nothing, on a path that runs ~44 000 times a second. Its docblock's rule
|
|
32
|
+
* — "an instrument that changes what it measures is not an instrument" — is
|
|
33
|
+
* the binding constraint, and a naive per-method map breaks it twice: a Map
|
|
34
|
+
* lookup and a counter object per method NAME, where the names arrive off
|
|
35
|
+
* the wire and are therefore unbounded in principle.
|
|
36
|
+
*
|
|
37
|
+
* ## What was chosen for the cardinality, and why not a sample
|
|
38
|
+
*
|
|
39
|
+
* **Full per-method counters in memory, bounded per peer; top-N only at REPORT
|
|
40
|
+
* time.** The constraint that is real is the LINE's width (D446: Loki reads
|
|
41
|
+
* these prefixes), not the map's size — and those are different limits:
|
|
42
|
+
*
|
|
43
|
+
* - *Measured shape.* The cap-usage graph already keeps `(caller, provider,
|
|
44
|
+
* cap, method)` and, queried on this hub on 2026-09-11, holds **53 rows for
|
|
45
|
+
* the whole cluster** — 14 of them for `pipeline-analytics`, whose top three
|
|
46
|
+
* caps are 2 077 of its 2 141 calls/min. A peer uses a handful of methods,
|
|
47
|
+
* not hundreds. ~39 peers × a few dozen labels is a few hundred counter
|
|
48
|
+
* objects, each allocated ONCE and then only incremented. That is not an
|
|
49
|
+
* explosion; it is smaller than the `childEventStats` map next to it.
|
|
50
|
+
* - *The unbounded case is still refused*, because "measured today" is not a
|
|
51
|
+
* guarantee: {@link MAX_CAP_ACTIONS_PER_PEER} caps the distinct labels per
|
|
52
|
+
* peer and everything past it is charged to ONE
|
|
53
|
+
* {@link CAP_ACTION_OVERFLOW_LABEL} bucket. The bytes are never dropped —
|
|
54
|
+
* they stop being *named*, which is a different and visible failure.
|
|
55
|
+
* - *A sampled window was rejected.* The flow this exists to name is few,
|
|
56
|
+
* enormous responses: 1 966 responses carrying 144 MB. A 1-in-N sample of a
|
|
57
|
+
* distribution whose top item fires ~30 times a minute answers "probably"
|
|
58
|
+
* to a question that has an exact answer for the price of an integer add.
|
|
59
|
+
* And a sample that misses a method would report it as zero bytes, which is
|
|
60
|
+
* exactly the `null`-vs-`0` confusion D393 exists to forbid.
|
|
61
|
+
* - *A byte THRESHOLD was rejected for the same reason in reverse*: a
|
|
62
|
+
* threshold decides what to keep before it knows the window's shape, so the
|
|
63
|
+
* method that only matters during an episode is the one it discards. Rank
|
|
64
|
+
* at report time, when the totals are known.
|
|
65
|
+
*
|
|
66
|
+
* ## What a reader may conclude, and what it may not
|
|
67
|
+
*
|
|
68
|
+
* {@link CapActionWindow.labelledBytes} against `totalBytes` is the census's
|
|
69
|
+
* own coverage, and it is on the line. Bytes this parent moved that carry no
|
|
70
|
+
* cap envelope (`register`, `addon-call`, log and event frames) are charged to
|
|
71
|
+
* {@link CAP_ACTION_UNLABELLED} rather than omitted: an instrument that
|
|
72
|
+
* silently drops what it cannot classify reports a clean attribution of a
|
|
73
|
+
* fraction it never names. A window whose `labelledBytes` is a small part of
|
|
74
|
+
* `totalBytes` has NOT attributed the plane, and says so.
|
|
75
|
+
*
|
|
76
|
+
* ## Mutation, as in `socket-traffic.ts`
|
|
77
|
+
*
|
|
78
|
+
* Same deliberate exception, for the same measured reason: the counters are
|
|
79
|
+
* plain mutable numbers written only by {@link CapActionCensus.record} and
|
|
80
|
+
* friends, and read only through {@link CapActionCensus.read}, which COPIES.
|
|
81
|
+
* The copy is not a nicety — the reporting layer subtracts the previous
|
|
82
|
+
* reading, and a `read()` that handed back the live maps would advance the
|
|
83
|
+
* baseline together with the counters and report every window as empty.
|
|
84
|
+
*/
|
|
85
|
+
/**
|
|
86
|
+
* The channel's half of the census — see `socket-channel.ts` for the seam.
|
|
87
|
+
*
|
|
88
|
+
* The channel owns the BYTES and the request↔response correlation and NOTHING
|
|
89
|
+
* else: it sees `{ k, id, body }` with `body: unknown`, and a `res` frame
|
|
90
|
+
* carries no method at all. So the layer that understands the protocol supplies
|
|
91
|
+
* {@link CapActionChannelObserver.label}, and the channel calls it once per
|
|
92
|
+
* `req` frame, holds the answer against the frame's id, and settles it with
|
|
93
|
+
* {@link CapActionChannelObserver.record} when the matching `res` is written or
|
|
94
|
+
* read. A `null` label is still recorded — those bytes crossed, and a census
|
|
95
|
+
* that drops what it cannot name reports a clean attribution of a fraction it
|
|
96
|
+
* never mentions.
|
|
97
|
+
*
|
|
98
|
+
* Declared HERE rather than in the channel so `local-transport.ts` can name it
|
|
99
|
+
* on {@link import('./local-transport.js').LocalChannel} without importing the
|
|
100
|
+
* implementation that satisfies it.
|
|
101
|
+
*/
|
|
102
|
+
export interface CapActionChannelObserver {
|
|
103
|
+
/** `<cap>.<method>`, or `null` for a frame that carries no cap envelope. */
|
|
104
|
+
label(body: unknown): string | null;
|
|
105
|
+
/** One completed request/response pair, its on-the-wire bytes both ways. */
|
|
106
|
+
record(label: string | null, reqBytes: number, resBytes: number): void;
|
|
107
|
+
}
|
|
108
|
+
/** One action's cumulative cost on one peer's channel. Mutable by design. */
|
|
109
|
+
export interface CapActionCounters {
|
|
110
|
+
/** Completed request/response pairs. */
|
|
111
|
+
calls: number;
|
|
112
|
+
/** On-the-wire bytes of the requests, prefix and payload included. */
|
|
113
|
+
reqBytes: number;
|
|
114
|
+
/** On-the-wire bytes of the answers. */
|
|
115
|
+
resBytes: number;
|
|
116
|
+
/** Of `calls`, how many this parent forwarded without reading (D448). */
|
|
117
|
+
raw: number;
|
|
118
|
+
/** Of `calls`, how many it materialised the args of. */
|
|
119
|
+
decoded: number;
|
|
120
|
+
}
|
|
121
|
+
/** An immutable reading of one action. */
|
|
122
|
+
export interface CapActionSample {
|
|
123
|
+
readonly calls: number;
|
|
124
|
+
readonly reqBytes: number;
|
|
125
|
+
readonly resBytes: number;
|
|
126
|
+
readonly raw: number;
|
|
127
|
+
readonly decoded: number;
|
|
128
|
+
}
|
|
129
|
+
/** One complete reading: peer → action → counters. */
|
|
130
|
+
export type CapActionReading = ReadonlyMap<string, ReadonlyMap<string, CapActionSample>>;
|
|
131
|
+
/** A process that has never read the census. */
|
|
132
|
+
export declare const EMPTY_CAP_ACTION_READING: CapActionReading;
|
|
133
|
+
/**
|
|
134
|
+
* Bytes that crossed on a frame carrying no cap envelope.
|
|
135
|
+
*
|
|
136
|
+
* Named rather than omitted so the census's coverage is readable off its own
|
|
137
|
+
* output: `register`, `addon-call`, log frames and the event fan-out all land
|
|
138
|
+
* here. A census that reported only what it could name would look like a
|
|
139
|
+
* complete attribution of whatever share it happened to cover.
|
|
140
|
+
*/
|
|
141
|
+
export declare const CAP_ACTION_UNLABELLED = "(unlabelled)";
|
|
142
|
+
/** Bytes of actions past {@link MAX_CAP_ACTIONS_PER_PEER} on one peer. */
|
|
143
|
+
export declare const CAP_ACTION_OVERFLOW_LABEL = "(overflow)";
|
|
144
|
+
/**
|
|
145
|
+
* Distinct named actions kept per peer before the overflow bucket takes over.
|
|
146
|
+
*
|
|
147
|
+
* Sixty-four against a measured worst case of 14 (`pipeline-analytics`, the
|
|
148
|
+
* heaviest peer on this hub, 2026-09-11) — four times the observed shape, so a
|
|
149
|
+
* peer has to change character before it overflows, and a bound that is never
|
|
150
|
+
* reached in practice still forbids the unbounded growth a wire-supplied key
|
|
151
|
+
* makes possible in principle.
|
|
152
|
+
*/
|
|
153
|
+
export declare const MAX_CAP_ACTIONS_PER_PEER = 64;
|
|
154
|
+
/**
|
|
155
|
+
* `<cap>.<method>` for a cap envelope, `null` for anything else.
|
|
156
|
+
*
|
|
157
|
+
* Both directions are labelled by the same function: a child's `cap-call-out`
|
|
158
|
+
* and the parent's `cap-call` carry `capName` + `method` in the same two keys,
|
|
159
|
+
* so one predicate covers the whole plane. Anything else — a `register`, an
|
|
160
|
+
* `addon-call`, a log or an event — is deliberately unlabelled; see
|
|
161
|
+
* {@link CAP_ACTION_UNLABELLED} for where its bytes go.
|
|
162
|
+
*
|
|
163
|
+
* Cost: two property reads, two `typeof`s and one concatenation of two short
|
|
164
|
+
* strings, called once per `req` frame (not per frame). Against the
|
|
165
|
+
* `encodeFrame` it sits beside — 1 948–1 975 ns on a 0.33 kB body — it is
|
|
166
|
+
* noise, and unlike `recordSocketFrame` it is not on the `evt` path at all.
|
|
167
|
+
*/
|
|
168
|
+
export declare function capActionLabel(body: unknown): string | null;
|
|
169
|
+
/** Which half of D448's census one call fell on. */
|
|
170
|
+
export type CapActionForwardKind = 'raw' | 'decoded';
|
|
171
|
+
/** The census as its writers see it. */
|
|
172
|
+
export interface CapActionCensus {
|
|
173
|
+
/** One completed request/response pair on `peerId`, charged to `action`. */
|
|
174
|
+
record(peerId: string, action: string, reqBytes: number, resBytes: number): void;
|
|
175
|
+
/** How `action`'s most recent call was routed — see {@link CapActionForwardKind}. */
|
|
176
|
+
recordForward(peerId: string, action: string, kind: CapActionForwardKind): void;
|
|
177
|
+
/** Bytes on a frame that carried no cap envelope. */
|
|
178
|
+
recordUnlabelled(peerId: string, bytes: number): void;
|
|
179
|
+
/** Drop a peer's counters when its channel closes for good. */
|
|
180
|
+
forget(peerId: string): void;
|
|
181
|
+
/** A COPY of the counters — see the module docblock on why it must be one. */
|
|
182
|
+
read(): CapActionReading;
|
|
183
|
+
}
|
|
184
|
+
export declare function createCapActionCensus(): CapActionCensus;
|
|
185
|
+
/** One (peer, action) pair over the window since the previous reading. */
|
|
186
|
+
export interface CapActionDelta {
|
|
187
|
+
readonly peerId: string;
|
|
188
|
+
readonly action: string;
|
|
189
|
+
readonly calls: number;
|
|
190
|
+
readonly reqBytes: number;
|
|
191
|
+
readonly resBytes: number;
|
|
192
|
+
readonly raw: number;
|
|
193
|
+
readonly decoded: number;
|
|
194
|
+
}
|
|
195
|
+
/** The census over one heartbeat window. */
|
|
196
|
+
export interface CapActionWindow {
|
|
197
|
+
/**
|
|
198
|
+
* Heaviest first by `reqBytes + resBytes`, at most the requested N.
|
|
199
|
+
*
|
|
200
|
+
* By BYTES and never by calls, for the reason D454 records: the flow this
|
|
201
|
+
* exists to name is few, enormous responses, and a ranking by count cannot
|
|
202
|
+
* see it by construction.
|
|
203
|
+
*/
|
|
204
|
+
readonly top: readonly CapActionDelta[];
|
|
205
|
+
/** Bytes the census could put a cap method on, this window. */
|
|
206
|
+
readonly labelledBytes: number;
|
|
207
|
+
/** Every byte it saw, named or not. `labelled/total` is its own coverage. */
|
|
208
|
+
readonly totalBytes: number;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Subtract the previous reading from the current one.
|
|
212
|
+
*
|
|
213
|
+
* A peer whose runner RESPAWNED reconnects under the same id with counters
|
|
214
|
+
* that went backwards. Clamped at 0, exactly as `diffSocketPlane` clamps for
|
|
215
|
+
* the same reason: its traffic in that window is genuinely unknown, and a
|
|
216
|
+
* negative number on a rate reads as a broken meter.
|
|
217
|
+
*/
|
|
218
|
+
export declare function diffCapActionCensus(before: CapActionReading, current: CapActionReading, topN: number): CapActionWindow;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ChildCapDescriptor } from './child-cap-protocol.js';
|
|
2
|
+
/**
|
|
3
|
+
* One connected child as the index reads it: its id and the cap manifest it
|
|
4
|
+
* registered. A structural subset of `LocalChildRegistry`'s own child entry,
|
|
5
|
+
* so the index can be built from `children.values()` directly and unit-tested
|
|
6
|
+
* without a socket.
|
|
7
|
+
*/
|
|
8
|
+
export interface CapIndexEntry {
|
|
9
|
+
readonly childId: string;
|
|
10
|
+
readonly caps: readonly ChildCapDescriptor[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The two questions `resolveChildId` asks, answered in O(1).
|
|
14
|
+
*
|
|
15
|
+
* Deliberately NOT "resolve a child": the operator's `setActiveSingleton`
|
|
16
|
+
* preference is read at CALL time from the capability registry and can change
|
|
17
|
+
* without any child connecting or leaving, so the index must not be allowed to
|
|
18
|
+
* hold an answer that depends on it. It holds only what the `children` map
|
|
19
|
+
* says, which is the same thing that invalidates it.
|
|
20
|
+
*/
|
|
21
|
+
export interface CapChildIndex {
|
|
22
|
+
/** The FIRST child owning the exact `(capName, deviceId)` pair, or null. */
|
|
23
|
+
deviceOwner(capName: string, deviceId: number): string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Every child owning a deviceId-LESS descriptor for `capName`, in children
|
|
26
|
+
* insertion order (so `[0]` is the first-registered), each named once.
|
|
27
|
+
*/
|
|
28
|
+
singletonCandidates(capName: string): readonly string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build the `(cap, device?) → child` index from the connected children.
|
|
32
|
+
*
|
|
33
|
+
* A PURE function of what it is handed — it captures nothing, subscribes to
|
|
34
|
+
* nothing and cannot be updated in place. That is the whole safety argument
|
|
35
|
+
* (D3, D457): the only way to change what it says is to build a new one from
|
|
36
|
+
* the `children` map, so an index can never name a child that map has already
|
|
37
|
+
* dropped. An incremental index — add on register, remove on close — would be
|
|
38
|
+
* a second registry that can disagree with the first, and a stale one routes
|
|
39
|
+
* to a dead runner, which is strictly worse than a slow scan.
|
|
40
|
+
*
|
|
41
|
+
* Cost: O(children × descriptors), paid once per mutation of the children map
|
|
42
|
+
* (a register, a re-register, a disconnect) rather than once per cap call.
|
|
43
|
+
*
|
|
44
|
+
* The two tiers stay separate, exactly as the scan had them: a device lookup
|
|
45
|
+
* is never answered from a deviceId-less descriptor. `resolveChildId` owns
|
|
46
|
+
* the fallback from tier 1 to tier 2, and folding it in here would make it
|
|
47
|
+
* unreachable.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildCapChildIndex(entries: Iterable<CapIndexEntry>): CapChildIndex;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The routing facts a parent reads OUT OF a cap call's args.
|
|
3
|
+
*
|
|
4
|
+
* `LocalChildRegistry` routes a child's `cap-call-out` on two things it used
|
|
5
|
+
* to read straight out of `args`: an inline `nodeId` (the historical per-call
|
|
6
|
+
* node pin — `extractNodeId`) and an `addonId` selector on a collection cap
|
|
7
|
+
* (`addonPinOf`). Under raw-forward the parent never decodes `args`, so the
|
|
8
|
+
* SENDER lifts those two fields into the envelope, and the parent routes on
|
|
9
|
+
* the lifted copy in both modes — one code path, whichever way the args
|
|
10
|
+
* travelled.
|
|
11
|
+
*
|
|
12
|
+
* Only top-level string fields are lifted, which is exactly what the two
|
|
13
|
+
* readers ever looked at. Nothing else about the args is visible to the
|
|
14
|
+
* parent under raw-forward, and that is the point.
|
|
15
|
+
*/
|
|
16
|
+
export interface CapRoutingHints {
|
|
17
|
+
readonly nodeId?: string;
|
|
18
|
+
readonly addonId?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Lift {@link CapRoutingHints} from a call's args. Never throws. */
|
|
21
|
+
export declare function liftRoutingHints(args: unknown): CapRoutingHints;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ChildCustomActionCatalogs, CustomActionCaller, IReadinessRegistryRecord, LogLevel, LogTags, SystemEvent } from '@camstack/types';
|
|
2
|
+
import { CapRoutingHints } from './cap-routing-hints.js';
|
|
2
3
|
/**
|
|
3
4
|
* Routing descriptor a child sends so the parent can route cap calls to it.
|
|
4
5
|
* The provider implementation never crosses the wire — only these keys do.
|
|
@@ -47,6 +48,22 @@ export interface RegisterMessage {
|
|
|
47
48
|
* and from a legacy child; absent means "not described yet", never "none".
|
|
48
49
|
*/
|
|
49
50
|
readonly customActions?: ChildCustomActionCatalogs;
|
|
51
|
+
/**
|
|
52
|
+
* The child can RECEIVE a `cap-call` whose args ride as an opaque payload
|
|
53
|
+
* beside the envelope (raw-forward), and answer with one. Declared, never
|
|
54
|
+
* assumed: a parent forwards a payload only to a child that said so, and
|
|
55
|
+
* decodes it for one that did not. Absent = legacy child = inline args.
|
|
56
|
+
*/
|
|
57
|
+
readonly rawForward?: true;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parent → child answer to `register`. `rawForward` says the parent accepts
|
|
61
|
+
* a `cap-call-out` whose args ride as a payload — a child packs its args
|
|
62
|
+
* only after seeing it. Absent = legacy parent = inline args.
|
|
63
|
+
*/
|
|
64
|
+
export interface RegisterAck {
|
|
65
|
+
readonly ok: true;
|
|
66
|
+
readonly rawForward?: true;
|
|
50
67
|
}
|
|
51
68
|
/**
|
|
52
69
|
* Child → parent, fire-and-forget: the child's category-pattern subscription
|
|
@@ -59,12 +76,21 @@ export interface EventSubUpdateMessage {
|
|
|
59
76
|
readonly kind: 'event-sub';
|
|
60
77
|
readonly patterns: readonly string[];
|
|
61
78
|
}
|
|
62
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Child → parent outbound cap invocation — the child asks the parent to route
|
|
81
|
+
* a cap call it does not own.
|
|
82
|
+
*
|
|
83
|
+
* Under raw-forward `args` is ABSENT from the envelope: the packed args ride
|
|
84
|
+
* as the frame's opaque payload and `hints` carries the two fields the parent
|
|
85
|
+
* routes on. A legacy child sends `args` inline and no `hints`; the parent
|
|
86
|
+
* lifts the hints itself. See `cap-routing-hints.ts`.
|
|
87
|
+
*/
|
|
63
88
|
export interface CapCallOutMessage {
|
|
64
89
|
readonly kind: 'cap-call-out';
|
|
65
90
|
readonly capName: string;
|
|
66
91
|
readonly method: string;
|
|
67
|
-
readonly args
|
|
92
|
+
readonly args?: unknown;
|
|
93
|
+
readonly hints?: CapRoutingHints;
|
|
68
94
|
readonly deviceId?: number;
|
|
69
95
|
/**
|
|
70
96
|
* Optional per-call node pin (out-of-band; NOT part of the validated method
|
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
/** Encode a value as a length-prefixed (4-byte big-endian) MsgPack frame. */
|
|
2
2
|
export declare function encodeFrame(value: unknown): Buffer;
|
|
3
|
+
/**
|
|
4
|
+
* The key under which an opaque payload rides on a frame — see
|
|
5
|
+
* {@link encodeFrameHead}. Short on purpose: it is written once per frame by
|
|
6
|
+
* hand, and it is the one msgpack key on this plane a decoder must never
|
|
7
|
+
* interpret.
|
|
8
|
+
*/
|
|
9
|
+
export declare const PAYLOAD_KEY = "pl";
|
|
10
|
+
/**
|
|
11
|
+
* Encode the HEAD of a frame that carries an opaque payload: the length
|
|
12
|
+
* prefix, the msgpack envelope with one extra key ({@link PAYLOAD_KEY}), and
|
|
13
|
+
* the `bin 32` header for the payload — everything but the payload bytes
|
|
14
|
+
* themselves, which the caller writes straight after it.
|
|
15
|
+
*
|
|
16
|
+
* ## Why the payload is the frame's TAIL and not a field
|
|
17
|
+
*
|
|
18
|
+
* hub-main routes every sibling↔sibling call. Until this existed it decoded
|
|
19
|
+
* the whole request (args included), built a new message, and re-encoded it
|
|
20
|
+
* — walking a 13.5 KB PCM chunk or a 2 000-row settings answer twice on the
|
|
21
|
+
* one thread that serves every tRPC request, and allocating an `Encoder`
|
|
22
|
+
* buffer that doubles from 2 KiB up to the body's size on every write
|
|
23
|
+
* (D441: ~27 % of hub-main's busy time in msgpack decode, ~6 % in encode,
|
|
24
|
+
* and the churn that fed a 29.5 % GC share).
|
|
25
|
+
*
|
|
26
|
+
* With the payload appended as the LAST key of the map, the receiver's
|
|
27
|
+
* ordinary msgpack decode yields it as a zero-copy VIEW of the frame
|
|
28
|
+
* (`@msgpack/msgpack` decodes `bin` as `subarray`), the router reads only the
|
|
29
|
+
* envelope, and forwarding is: encode a ~60-byte head, write it, write the
|
|
30
|
+
* view. The wire stays plain msgpack — a peer that never heard of payloads
|
|
31
|
+
* decodes `{ …frame, pl: <bytes> }` and can be told, at register, not to be
|
|
32
|
+
* sent one.
|
|
33
|
+
*
|
|
34
|
+
* ## The one hand-written msgpack in this repository
|
|
35
|
+
*
|
|
36
|
+
* `@msgpack/msgpack` has no "encode a map, then append a key" API, so the
|
|
37
|
+
* envelope is encoded normally and its `fixmap` count byte is bumped by one
|
|
38
|
+
* before the key and the `bin 32` header are appended. That patch is only
|
|
39
|
+
* valid for a `fixmap` (≤ 15 keys); a `Frame` has at most four, and anything
|
|
40
|
+
* else is refused rather than silently corrupted.
|
|
41
|
+
*/
|
|
42
|
+
export declare function encodeFrameHead(frame: Record<string, unknown>, payload: Uint8Array): Buffer;
|
|
43
|
+
/**
|
|
44
|
+
* Serialise a value into the bytes a payload carries. The SAME options as the
|
|
45
|
+
* envelope codec, so `undefined` properties are omitted rather than turned
|
|
46
|
+
* into `null` — a `z.void()` cap output must read back as `undefined` on the
|
|
47
|
+
* far side exactly as it did when it travelled inline.
|
|
48
|
+
*/
|
|
49
|
+
export declare function packPayload(value: unknown): Uint8Array;
|
|
50
|
+
/** Inverse of {@link packPayload}. A `bin` inside decodes as a view of `bytes`. */
|
|
51
|
+
export declare function unpackPayload(bytes: Uint8Array): unknown;
|
|
3
52
|
/**
|
|
4
53
|
* Reassembles length-prefixed MsgPack frames from a byte stream.
|
|
5
54
|
*
|
|
@@ -2,6 +2,8 @@ export type { Frame, RequestHandler, EventHandler, LocalChannel, LocalTransportS
|
|
|
2
2
|
export { encodeFrame, FrameDecoder } from './frame-codec.js';
|
|
3
3
|
export type { SocketDirectionCounters, SocketDirectionSample, SocketMessageKind, SocketTrafficSample, } from './socket-traffic.js';
|
|
4
4
|
export { createSocketDirectionCounters, EMPTY_SOCKET_DIRECTION, recordSocketFrame, sampleSocketDirection, socketDirectionBytes, socketDirectionMessages, } from './socket-traffic.js';
|
|
5
|
+
export type { CapActionCensus, CapActionChannelObserver, CapActionDelta, CapActionReading, CapActionSample, CapActionWindow, } from './cap-action-census.js';
|
|
6
|
+
export { CAP_ACTION_OVERFLOW_LABEL, CAP_ACTION_UNLABELLED, capActionLabel, createCapActionCensus, diffCapActionCensus, EMPTY_CAP_ACTION_READING, MAX_CAP_ACTIONS_PER_PEER, } from './cap-action-census.js';
|
|
5
7
|
export { localEndpointPath } from './local-endpoint-path.js';
|
|
6
8
|
export { SocketChannel } from './socket-channel.js';
|
|
7
9
|
export { UdsLocalTransportServer, UdsLocalTransportClient } from './uds-local-transport.js';
|
|
@@ -67,6 +67,13 @@ export declare class LocalChildClient {
|
|
|
67
67
|
private latestCustomActions;
|
|
68
68
|
/** Events and logs queued while the channel is not yet open. */
|
|
69
69
|
private readonly pendingEmits;
|
|
70
|
+
/**
|
|
71
|
+
* Whether the parent acknowledged raw-forward at `register`. Until it has,
|
|
72
|
+
* `callOut` sends args inline exactly as every child always did — a legacy
|
|
73
|
+
* parent would decode a payload-borne call to `args: undefined` and run the
|
|
74
|
+
* provider on nothing, silently. Re-read on every re-register.
|
|
75
|
+
*/
|
|
76
|
+
private parentRawForward;
|
|
70
77
|
/** Handler for parent→child events. Registered via `onEvent`. */
|
|
71
78
|
private eventHandler;
|
|
72
79
|
/**
|
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
import { IReadinessRegistryRecord, SystemEvent } from '@camstack/types';
|
|
2
2
|
import { CapUsageCallRecord } from '../moleculer/cap-usage-registry.js';
|
|
3
|
+
import { CapActionReading } from './cap-action-census.js';
|
|
4
|
+
import { CapRoutingHints } from './cap-routing-hints.js';
|
|
3
5
|
import { AddonCallInput, CapCallInput, ChildLogMessage, RegisteredChild } from './child-cap-protocol.js';
|
|
4
6
|
import { LocalTransportServer } from './local-transport.js';
|
|
5
7
|
import { SocketTrafficSample } from './socket-traffic.js';
|
|
8
|
+
/**
|
|
9
|
+
* How many child `cap-call-out`s this parent forwarded WITHOUT reading their
|
|
10
|
+
* args (`raw`) against how many it decoded (`decoded`: an inline-args frame,
|
|
11
|
+
* a target that cannot take a payload, or a call that left the sibling path
|
|
12
|
+
* for `onUnownedCall`). Cumulative; the heartbeat reports the window.
|
|
13
|
+
*
|
|
14
|
+
* It exists because raw-forward makes the parent blind to the bytes on
|
|
15
|
+
* purpose, and the one thing it must not be blind to is how often that
|
|
16
|
+
* happens. A plane that says "raw=0" under load is a plane where the
|
|
17
|
+
* negotiation failed, and nothing else would show it.
|
|
18
|
+
*/
|
|
19
|
+
export interface ForwardCensus {
|
|
20
|
+
readonly raw: number;
|
|
21
|
+
readonly decoded: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Peer id the action census charges frames that arrived before `register`.
|
|
25
|
+
*
|
|
26
|
+
* One frame per connection, and it is named rather than dropped: a census is
|
|
27
|
+
* only worth its coverage, and coverage you cannot see is coverage you cannot
|
|
28
|
+
* check.
|
|
29
|
+
*/
|
|
30
|
+
export declare const PRE_REGISTER_PEER_ID = "(pre-register)";
|
|
6
31
|
/**
|
|
7
32
|
* Fan-out mode for parent→child event delivery, from the
|
|
8
33
|
* `CAMSTACK_UDS_EVENT_FANOUT` env (read once at registry construction):
|
|
@@ -104,6 +129,13 @@ export interface LocalChildRegistryOptions {
|
|
|
104
129
|
* which is the one thing the caller took the trouble to say it did not want.
|
|
105
130
|
* It goes to `onUnownedCall`, which resolves the named provider.
|
|
106
131
|
*
|
|
132
|
+
* The third argument is the call's ROUTING HINTS (`{ nodeId?, addonId? }`,
|
|
133
|
+
* `cap-routing-hints.ts`) — the two top-level string fields the parent ever
|
|
134
|
+
* read out of the args — not the args themselves. Under raw-forward the
|
|
135
|
+
* parent does not decode the args at all; the sender lifts the hints, and
|
|
136
|
+
* for a legacy frame the parent lifts them itself. Both modes reach this
|
|
137
|
+
* predicate with the same shape.
|
|
138
|
+
*
|
|
107
139
|
* The array-method bypass above and this one are the same defect seen twice.
|
|
108
140
|
* `broker.getBrokerConfig({id:'ha_001', addonId:'provider-homeassistant'})`
|
|
109
141
|
* from the Home Assistant export was answered by `provider-homematic` — which
|
|
@@ -117,7 +149,7 @@ export interface LocalChildRegistryOptions {
|
|
|
117
149
|
* wiring side injects it, sharing the predicate with the handler that then
|
|
118
150
|
* performs the routing — a rule implemented twice is a rule that drifts.
|
|
119
151
|
*/
|
|
120
|
-
readonly isAddonPinnedCall?: (capName: string, method: string,
|
|
152
|
+
readonly isAddonPinnedCall?: (capName: string, method: string, hints: CapRoutingHints) => boolean;
|
|
121
153
|
/**
|
|
122
154
|
* Per-cap-method bound for UDS cap calls, in ms — read from the cap
|
|
123
155
|
* definition's declared `timeoutMs`. The SAME resolver the remote plane uses
|
|
@@ -199,6 +231,17 @@ export declare class LocalChildRegistry {
|
|
|
199
231
|
/** See {@link LocalChildRegistryOptions.isAggregatedCollectionMethod}. */
|
|
200
232
|
private readonly isAggregatedCollectionMethod?;
|
|
201
233
|
private readonly isAddonPinnedCall?;
|
|
234
|
+
/** See {@link ForwardCensus}. Mutable counters on the hot path, by design. */
|
|
235
|
+
private forwardedRaw;
|
|
236
|
+
private forwardedDecoded;
|
|
237
|
+
/**
|
|
238
|
+
* The per-ACTION byte census (D456). `ForwardCensus` above is the same
|
|
239
|
+
* question asked of the whole plane; this one asks it per `<cap>.<method>`,
|
|
240
|
+
* beside the bytes that method actually moved. Both are kept: `socketFwd=`
|
|
241
|
+
* says whether the negotiation is working at all, and a `raw=0` on ONE
|
|
242
|
+
* method with `raw>0` on its neighbours is a different fault.
|
|
243
|
+
*/
|
|
244
|
+
private readonly actionCensus;
|
|
202
245
|
/** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
|
|
203
246
|
private readonly capTimeoutMs?;
|
|
204
247
|
/** See {@link LocalChildRegistryOptions.capUsageObserver}. */
|
|
@@ -211,6 +254,12 @@ export declare class LocalChildRegistry {
|
|
|
211
254
|
private readonly childEventStats;
|
|
212
255
|
/** Last pattern-set string logged per child, to dedup the INFO line. */
|
|
213
256
|
private readonly loggedPatternSet;
|
|
257
|
+
/**
|
|
258
|
+
* Derived `(cap, device?) → child` index over {@link children}, or `null`
|
|
259
|
+
* when it must be rebuilt. See {@link capIndex} for why it is a cache and
|
|
260
|
+
* never a registry (D3, D457).
|
|
261
|
+
*/
|
|
262
|
+
private capChildIndex;
|
|
214
263
|
/**
|
|
215
264
|
* Accepts either a plain positional `server` argument (backward-compatible)
|
|
216
265
|
* or a full `LocalChildRegistryOptions` object.
|
|
@@ -242,6 +291,28 @@ export declare class LocalChildRegistry {
|
|
|
242
291
|
* pair for the whole process and has no per-socket breakdown.
|
|
243
292
|
*/
|
|
244
293
|
getChildSocketTraffic(childId: string): SocketTrafficSample | null;
|
|
294
|
+
/**
|
|
295
|
+
* Bytes this process has written to a child that the child has not yet
|
|
296
|
+
* taken, live, or `null` when the channel keeps no such reading. Read on
|
|
297
|
+
* every heartbeat probe (D446) — a child that stops draining is the shape
|
|
298
|
+
* of an outbound-queue episode, and the number lives on the channel.
|
|
299
|
+
*/
|
|
300
|
+
getChildQueuedBytes(childId: string): number | null;
|
|
301
|
+
/** Cumulative {@link ForwardCensus} of this parent's `cap-call-out` routing. */
|
|
302
|
+
readForwardCensus(): ForwardCensus;
|
|
303
|
+
/**
|
|
304
|
+
* Cumulative per-action byte census: peer → `<cap>.<method>` → bytes, calls
|
|
305
|
+
* and the raw/decoded split. A COPY (`cap-action-census.ts` explains why it
|
|
306
|
+
* must be one); the heartbeat subtracts the previous reading.
|
|
307
|
+
*
|
|
308
|
+
* This is the instrument D454 closed by asking for. `socketTopBytes=` names
|
|
309
|
+
* the peer that moves the bytes and `txkB=` says they are responses; neither
|
|
310
|
+
* can say WHICH method originates them, and after raw-forward the bytes that
|
|
311
|
+
* cross are no longer the proxy for what hub-main pays — 85.7 % of sibling
|
|
312
|
+
* calls cross unread. What costs is what hub-main LOOKS at, and that is the
|
|
313
|
+
* `decoded` half of an entry, next to the bytes it carried.
|
|
314
|
+
*/
|
|
315
|
+
readActionCensus(): CapActionReading;
|
|
245
316
|
/**
|
|
246
317
|
* The regime the counters above were produced under, read once from
|
|
247
318
|
* `CAMSTACK_UDS_EVENT_FANOUT` at construction.
|
|
@@ -331,10 +402,33 @@ export declare class LocalChildRegistry {
|
|
|
331
402
|
* is not the registry.
|
|
332
403
|
*/
|
|
333
404
|
private recordCapUsage;
|
|
334
|
-
/**
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
405
|
+
/**
|
|
406
|
+
* The cap index over the CURRENT children, rebuilt on first use after any
|
|
407
|
+
* mutation of {@link children}.
|
|
408
|
+
*
|
|
409
|
+
* This replaced a linear walk of every child's cap manifest per
|
|
410
|
+
* `cap-call-out`, measured at **7.1 % of hub-main's busy main thread**
|
|
411
|
+
* across 39 children (D456). Why it did not exist before is the whole
|
|
412
|
+
* design: what it indexes moves — a runner is replaced on every update, a
|
|
413
|
+
* crash respawns one, a child re-registers with a replacement manifest after
|
|
414
|
+
* init — and an index that names a dead child routes work into a socket
|
|
415
|
+
* nobody is reading, which is strictly worse than a slow scan.
|
|
416
|
+
*
|
|
417
|
+
* So it is not maintained; it is DISCARDED. `buildCapChildIndex` is a pure
|
|
418
|
+
* function of `children.values()`, the only two writers of that map
|
|
419
|
+
* ({@link invalidateCapIndex}'s call sites) drop the index in the same
|
|
420
|
+
* statement, and nothing else can construct one. There is no add-on-register
|
|
421
|
+
* / remove-on-close path that could disagree with the map — i.e. no shadow
|
|
422
|
+
* registry (D3): the map remains the single authority and this holds no fact
|
|
423
|
+
* it does not.
|
|
424
|
+
*
|
|
425
|
+
* The operator's active-singleton preference is deliberately NOT indexed —
|
|
426
|
+
* it changes without any child connecting or leaving, so `resolveChildId`
|
|
427
|
+
* reads it live and the index only supplies the candidate list.
|
|
428
|
+
*/
|
|
429
|
+
private capIndex;
|
|
430
|
+
/** Drop the derived index. MUST follow every write to {@link children}. */
|
|
431
|
+
private invalidateCapIndex;
|
|
338
432
|
/**
|
|
339
433
|
* Does the named child currently provide `(capName, deviceId?)`?
|
|
340
434
|
*
|
|
@@ -1,14 +1,25 @@
|
|
|
1
|
+
import { CapActionChannelObserver } from './cap-action-census.js';
|
|
1
2
|
import { SocketTrafficSample } from './socket-traffic.js';
|
|
2
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Wire frame exchanged over a local channel.
|
|
5
|
+
*
|
|
6
|
+
* `pl` is an OPAQUE payload the channel carries beside the envelope and never
|
|
7
|
+
* interprets: the bytes a `req` handler receives as its second argument, or
|
|
8
|
+
* the bytes a {@link PayloadReply} put on a `res`. On the wire it is the
|
|
9
|
+
* frame's last msgpack key (`frame-codec.ts` `encodeFrameHead`), which is
|
|
10
|
+
* what lets a router forward it as a view without decoding it.
|
|
11
|
+
*/
|
|
3
12
|
export type Frame = {
|
|
4
13
|
readonly k: 'req';
|
|
5
14
|
readonly id: number;
|
|
6
15
|
readonly body: unknown;
|
|
16
|
+
readonly pl?: Uint8Array;
|
|
7
17
|
} | {
|
|
8
18
|
readonly k: 'res';
|
|
9
19
|
readonly id: number;
|
|
10
20
|
readonly ok: true;
|
|
11
21
|
readonly body: unknown;
|
|
22
|
+
readonly pl?: Uint8Array;
|
|
12
23
|
} | {
|
|
13
24
|
readonly k: 'res';
|
|
14
25
|
readonly id: number;
|
|
@@ -18,8 +29,27 @@ export type Frame = {
|
|
|
18
29
|
readonly k: 'evt';
|
|
19
30
|
readonly body: unknown;
|
|
20
31
|
};
|
|
21
|
-
|
|
32
|
+
/**
|
|
33
|
+
* A request handler receives the envelope body and, when the request carried
|
|
34
|
+
* one, its opaque payload. It may answer with a plain value or with a
|
|
35
|
+
* {@link PayloadReply}; a plain value travels inline exactly as before.
|
|
36
|
+
*/
|
|
37
|
+
export type RequestHandler = (body: unknown, payload?: Uint8Array) => Promise<unknown>;
|
|
22
38
|
export type EventHandler = (body: unknown) => void;
|
|
39
|
+
/**
|
|
40
|
+
* An answer that carries an opaque payload beside its body.
|
|
41
|
+
*
|
|
42
|
+
* A CLASS rather than a shape so the channel can tell it from a provider's
|
|
43
|
+
* ordinary `{ body, payload }` return value by identity, never by duck-typing:
|
|
44
|
+
* a cap method is free to return an object with those two keys and it must
|
|
45
|
+
* travel inline like any other. `request()` resolves with one of these only
|
|
46
|
+
* when the peer's `res` frame carried a payload.
|
|
47
|
+
*/
|
|
48
|
+
export declare class PayloadReply {
|
|
49
|
+
readonly body: unknown;
|
|
50
|
+
readonly payload: Uint8Array;
|
|
51
|
+
constructor(body: unknown, payload: Uint8Array);
|
|
52
|
+
}
|
|
23
53
|
/** Per-request options a {@link LocalChannel} implementation may honour. */
|
|
24
54
|
export interface LocalRequestOptions {
|
|
25
55
|
/**
|
|
@@ -29,6 +59,13 @@ export interface LocalRequestOptions {
|
|
|
29
59
|
* not cut short.
|
|
30
60
|
*/
|
|
31
61
|
readonly timeoutMs?: number;
|
|
62
|
+
/**
|
|
63
|
+
* Opaque bytes to carry beside `body`. The peer's request handler receives
|
|
64
|
+
* them as its second argument; the channel never reads them. An
|
|
65
|
+
* implementation without a wire (in-process, test doubles) may hand them
|
|
66
|
+
* through as-is.
|
|
67
|
+
*/
|
|
68
|
+
readonly payload?: Uint8Array;
|
|
32
69
|
}
|
|
33
70
|
/** A bidirectional request/response + one-way event channel over one socket. */
|
|
34
71
|
export interface LocalChannel {
|
|
@@ -48,6 +85,21 @@ export interface LocalChannel {
|
|
|
48
85
|
* finds it absent reports the peer as unmeasured rather than as silent.
|
|
49
86
|
*/
|
|
50
87
|
readTraffic?(): SocketTrafficSample;
|
|
88
|
+
/**
|
|
89
|
+
* Bytes written to the peer that it has not yet taken, read live. OPTIONAL
|
|
90
|
+
* for the same reason as {@link readTraffic}: only a real socket has a
|
|
91
|
+
* write queue. A reader that finds it absent reports the peer's queue as
|
|
92
|
+
* unmeasured — never as zero (D393).
|
|
93
|
+
*/
|
|
94
|
+
queuedBytes?(): number;
|
|
95
|
+
/**
|
|
96
|
+
* Wire the per-action byte census (`cap-action-census.ts`). OPTIONAL for the
|
|
97
|
+
* same reason as {@link readTraffic}: only a real socket has bytes to
|
|
98
|
+
* charge. A registry whose channel does not implement it keeps no per-action
|
|
99
|
+
* numbers for that peer, and the report omits the field rather than printing
|
|
100
|
+
* a zero (D393).
|
|
101
|
+
*/
|
|
102
|
+
observeActions?(observer: CapActionChannelObserver): void;
|
|
51
103
|
}
|
|
52
104
|
/** Parent side: listens and hands a channel per accepted child connection. */
|
|
53
105
|
export interface LocalTransportServer {
|