@net-mesh/core 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mesh_rpc.d.ts +551 -0
- package/mesh_rpc.js +675 -1
- package/net.darwin-arm64.node +0 -0
- package/net.darwin-x64.node +0 -0
- package/net.linux-arm64-gnu.node +0 -0
- package/net.linux-arm64-musl.node +0 -0
- package/net.linux-x64-gnu.node +0 -0
- package/net.linux-x64-musl.node +0 -0
- package/net.win32-arm64-msvc.node +0 -0
- package/net.win32-x64-msvc.node +0 -0
- package/package.json +9 -9
package/mesh_rpc.d.ts
CHANGED
|
@@ -72,11 +72,240 @@ export interface RawMeshRpc {
|
|
|
72
72
|
call(targetNodeId: bigint, service: string, request: Buffer, opts?: CallOptions): Promise<Buffer>;
|
|
73
73
|
callService(service: string, request: Buffer, opts?: CallOptions): Promise<Buffer>;
|
|
74
74
|
callStreaming(targetNodeId: bigint, service: string, request: Buffer, opts?: CallOptions): Promise<RawRpcStream>;
|
|
75
|
+
/**
|
|
76
|
+
* Open a client-streaming call. Returns a `RawClientStreamCall`
|
|
77
|
+
* — push chunks via `send`, then `finish` to await the terminal
|
|
78
|
+
* response.
|
|
79
|
+
*/
|
|
80
|
+
callClientStream(targetNodeId: bigint, service: string, opts?: CallOptions): Promise<RawClientStreamCall>;
|
|
81
|
+
/**
|
|
82
|
+
* Register a client-streaming handler. The handler receives a
|
|
83
|
+
* `RawRequestStream` and returns the terminal response as a
|
|
84
|
+
* Buffer. Throw `appError(code, body)` to surface a typed
|
|
85
|
+
* Application status.
|
|
86
|
+
*/
|
|
87
|
+
serveClientStream(service: string, handler: (stream: RawRequestStream) => Promise<Buffer>): ServeHandle;
|
|
88
|
+
/**
|
|
89
|
+
* Open a duplex call. Both `requestWindowInitial` (upload flow
|
|
90
|
+
* control) and `streamWindowInitial` (download flow control)
|
|
91
|
+
* on `CallOptions` are independently opt-in.
|
|
92
|
+
*/
|
|
93
|
+
callDuplex(targetNodeId: bigint, service: string, opts?: CallOptions): Promise<RawDuplexCall>;
|
|
94
|
+
/**
|
|
95
|
+
* Register a duplex handler. The napi side passes a 2-tuple
|
|
96
|
+
* `[stream, sink]` to the handler (per the
|
|
97
|
+
* `DuplexHandlerArgs::ToNapiValue` impl in the napi binding).
|
|
98
|
+
* Handler returns a terminal Buffer that the substrate discards;
|
|
99
|
+
* intermediate chunks come from `sink.send`. Throw
|
|
100
|
+
* `appError(code, body)` to surface a typed Application status.
|
|
101
|
+
*/
|
|
102
|
+
serveDuplex(service: string, handler: (args: [RawRequestStream, RawResponseSink]) => Promise<Buffer>): ServeHandle;
|
|
75
103
|
findServiceNodes(service: string): bigint[];
|
|
76
104
|
/** Mint a fresh cancel token (`bigint`). */
|
|
77
105
|
reserveCancelToken(): bigint;
|
|
78
106
|
/** Abort the in-flight call associated with `token`. Idempotent. */
|
|
79
107
|
cancelCall(token: bigint): void;
|
|
108
|
+
/**
|
|
109
|
+
* Install (or clear with `null` / `undefined`) the caller-side
|
|
110
|
+
* nRPC observer. The handler receives the napi POD
|
|
111
|
+
* (`RawRpcCallEvent`); the typed wrapper layer normalizes into
|
|
112
|
+
* a tagged-union {@link RpcCallEvent} before reaching user code.
|
|
113
|
+
* See `TypedMeshRpc.setObserver` for the locked-decision
|
|
114
|
+
* contract.
|
|
115
|
+
*/
|
|
116
|
+
setObserver(observer: ((evt: RawRpcCallEvent) => void) | null | undefined): void;
|
|
117
|
+
/** Snapshot the per-service nRPC metrics registry. */
|
|
118
|
+
metricsSnapshot(): RpcMetricsSnapshot;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Status discriminant for an observed RPC call. Match on `kind`
|
|
122
|
+
* before reading other fields; this is a string-tagged union
|
|
123
|
+
* because the napi POD layer doesn't support TS discriminated
|
|
124
|
+
* unions natively (the napi side flattens it to `statusKind:
|
|
125
|
+
* string` + `statusMessage?: string` — see {@link RpcCallEvent}).
|
|
126
|
+
*/
|
|
127
|
+
export type RpcCallStatus = {
|
|
128
|
+
kind: 'ok';
|
|
129
|
+
} | {
|
|
130
|
+
kind: 'error';
|
|
131
|
+
message: string;
|
|
132
|
+
} | {
|
|
133
|
+
kind: 'timeout';
|
|
134
|
+
} | {
|
|
135
|
+
kind: 'canceled';
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Single observed RPC call boundary. Surfaced to the observer
|
|
139
|
+
* callback installed via `TypedMeshRpc.setObserver`. Mirrors the
|
|
140
|
+
* substrate's `RpcCallEvent` field-by-field — the typed wrapper
|
|
141
|
+
* reconstructs the `RpcCallStatus` tagged union from the napi
|
|
142
|
+
* POD's flat `statusKind` / `statusMessage` pair so user code
|
|
143
|
+
* sees a TS-idiomatic discriminated union.
|
|
144
|
+
*/
|
|
145
|
+
export interface RpcCallEvent {
|
|
146
|
+
/** 64-bit node id of the calling node. */
|
|
147
|
+
caller: bigint;
|
|
148
|
+
/** 64-bit node id of the responding node. */
|
|
149
|
+
callee: bigint;
|
|
150
|
+
/** Service / method name. */
|
|
151
|
+
method: string;
|
|
152
|
+
/** Elapsed time in milliseconds. */
|
|
153
|
+
latencyMs: number;
|
|
154
|
+
/** Tagged-union outcome. Match on `status.kind`. */
|
|
155
|
+
status: RpcCallStatus;
|
|
156
|
+
/** Wire payload size of the request body (0 when not available). */
|
|
157
|
+
requestBytes: number;
|
|
158
|
+
/** Wire payload size of the response body (0 when not available). */
|
|
159
|
+
responseBytes: number;
|
|
160
|
+
/** v1 only emits `"outbound"`; `"inbound"` is reserved. */
|
|
161
|
+
direction: 'outbound' | 'inbound';
|
|
162
|
+
/** Unix-ms timestamp captured at fire time. */
|
|
163
|
+
tsUnixMs: bigint;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Status-kind discriminants emitted on `RawRpcCallEvent.statusKind`
|
|
167
|
+
* and consumed when normalizing into the tagged-union
|
|
168
|
+
* {@link RpcCallStatus}. Prefer these constants over hard-coding
|
|
169
|
+
* the string literal in `if (evt.statusKind === '...')` checks —
|
|
170
|
+
* a typo on the literal silently never fires.
|
|
171
|
+
*
|
|
172
|
+
* Named `STATUS_KIND_*` to disambiguate from the wire-level
|
|
173
|
+
* `STATUS_*` u16 status codes used by `RpcServerError`.
|
|
174
|
+
*/
|
|
175
|
+
export declare const STATUS_KIND_OK: "ok";
|
|
176
|
+
export declare const STATUS_KIND_ERROR: "error";
|
|
177
|
+
export declare const STATUS_KIND_TIMEOUT: "timeout";
|
|
178
|
+
export declare const STATUS_KIND_CANCELED: "canceled";
|
|
179
|
+
/** Direction-kind discriminants on `RawRpcCallEvent.direction`. */
|
|
180
|
+
export declare const DIRECTION_OUTBOUND: "outbound";
|
|
181
|
+
export declare const DIRECTION_INBOUND: "inbound";
|
|
182
|
+
export type StatusKind = typeof STATUS_KIND_OK | typeof STATUS_KIND_ERROR | typeof STATUS_KIND_TIMEOUT | typeof STATUS_KIND_CANCELED;
|
|
183
|
+
export type DirectionKind = typeof DIRECTION_OUTBOUND | typeof DIRECTION_INBOUND;
|
|
184
|
+
/**
|
|
185
|
+
* Raw napi observer event shape (POD with flat `statusKind` /
|
|
186
|
+
* `statusMessage` strings). Internal — the typed wrapper
|
|
187
|
+
* normalizes it into {@link RpcCallEvent} before reaching user
|
|
188
|
+
* code. Exported so test stubs can construct events directly.
|
|
189
|
+
*/
|
|
190
|
+
export interface RawRpcCallEvent {
|
|
191
|
+
caller: bigint;
|
|
192
|
+
callee: bigint;
|
|
193
|
+
method: string;
|
|
194
|
+
latencyMs: number;
|
|
195
|
+
statusKind: StatusKind;
|
|
196
|
+
statusMessage?: string | null;
|
|
197
|
+
requestBytes: number;
|
|
198
|
+
responseBytes: number;
|
|
199
|
+
direction: DirectionKind;
|
|
200
|
+
tsUnixMs: bigint;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Per-service caller + server-side nRPC counters at a point in
|
|
204
|
+
* time. Element of `RpcMetricsSnapshot.services`. All `bigint`
|
|
205
|
+
* fields are direct u64 counters; `inFlight` and `handlerInFlight`
|
|
206
|
+
* are signed i64 to track concurrent calls without underflow.
|
|
207
|
+
*/
|
|
208
|
+
export interface ServiceMetrics {
|
|
209
|
+
service: string;
|
|
210
|
+
callsTotal: bigint;
|
|
211
|
+
errorsNoRoute: bigint;
|
|
212
|
+
errorsTimeout: bigint;
|
|
213
|
+
errorsServer: bigint;
|
|
214
|
+
errorsTransport: bigint;
|
|
215
|
+
inFlight: number;
|
|
216
|
+
latencySumNs: bigint;
|
|
217
|
+
latencyCount: bigint;
|
|
218
|
+
/**
|
|
219
|
+
* Cumulative histogram bucket counts; index `i` corresponds to
|
|
220
|
+
* the substrate's `DEFAULT_LATENCY_BUCKETS_SECS[i]`. Last entry
|
|
221
|
+
* is the `+Inf` bucket.
|
|
222
|
+
*/
|
|
223
|
+
latencyBuckets: bigint[];
|
|
224
|
+
handlerInvocationsTotal: bigint;
|
|
225
|
+
handlerPanicsTotal: bigint;
|
|
226
|
+
handlerInFlight: number;
|
|
227
|
+
handlerDurationSumNs: bigint;
|
|
228
|
+
handlerDurationCount: bigint;
|
|
229
|
+
handlerDurationBuckets: bigint[];
|
|
230
|
+
streamingChunksEmittedTotal: bigint;
|
|
231
|
+
streamingChunksDroppedTotal: bigint;
|
|
232
|
+
capabilityDeniedTotal: bigint;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Snapshot of the per-service nRPC metrics registry. Returned by
|
|
236
|
+
* `TypedMeshRpc.metricsSnapshot`. Cheap — one DashMap iteration
|
|
237
|
+
* on the substrate side. Safe to collect on every Prometheus
|
|
238
|
+
* scrape.
|
|
239
|
+
*/
|
|
240
|
+
export interface RpcMetricsSnapshot {
|
|
241
|
+
/**
|
|
242
|
+
* One entry per service that has been called at least once
|
|
243
|
+
* since the mesh was created. Sorted by service name.
|
|
244
|
+
*/
|
|
245
|
+
services: ServiceMetrics[];
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Raw napi `ClientStreamCall` — minimal shape consumed by
|
|
249
|
+
* `TypedClientStreamCall`. Caller pushes typed chunks via `send`
|
|
250
|
+
* then awaits the terminal response with `finish`.
|
|
251
|
+
*/
|
|
252
|
+
export interface RawClientStreamCall {
|
|
253
|
+
send(body: Buffer): Promise<void>;
|
|
254
|
+
finish(): Promise<Buffer>;
|
|
255
|
+
callId(): Promise<bigint>;
|
|
256
|
+
flowControlled(): Promise<boolean>;
|
|
257
|
+
close(): Promise<void>;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Raw napi `JsRequestStream` — minimal shape consumed by
|
|
261
|
+
* `TypedRequestStream` server-side. Drain via `next()` until it
|
|
262
|
+
* returns `null` (clean EOF) or throws on terminal error.
|
|
263
|
+
*
|
|
264
|
+
* The diagnostic getters (`callerOrigin`, `callId`, `deadlineNs`,
|
|
265
|
+
* `headers`) are populated at handler-dispatch time and stable
|
|
266
|
+
* for the lifetime of the stream.
|
|
267
|
+
*/
|
|
268
|
+
export interface RawRequestStream {
|
|
269
|
+
next(): Promise<Buffer | null>;
|
|
270
|
+
readonly callerOrigin: bigint;
|
|
271
|
+
readonly callId: bigint;
|
|
272
|
+
readonly deadlineNs: bigint;
|
|
273
|
+
readonly headers: [string, Buffer][];
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Raw napi `DuplexCall` — combined send + receive surface. Use
|
|
277
|
+
* `intoSplit` to separate into independent sink + stream halves.
|
|
278
|
+
*/
|
|
279
|
+
export interface RawDuplexCall {
|
|
280
|
+
send(body: Buffer): Promise<void>;
|
|
281
|
+
finishSending(): Promise<void>;
|
|
282
|
+
next(): Promise<Buffer | null>;
|
|
283
|
+
intoSplit(): Promise<[RawDuplexSink, RawDuplexStream]>;
|
|
284
|
+
callId(): Promise<bigint>;
|
|
285
|
+
flowControlled(): Promise<boolean>;
|
|
286
|
+
close(): Promise<void>;
|
|
287
|
+
}
|
|
288
|
+
/** Send-half of a `RawDuplexCall` after `intoSplit`. */
|
|
289
|
+
export interface RawDuplexSink {
|
|
290
|
+
send(body: Buffer): Promise<void>;
|
|
291
|
+
finish(): Promise<void>;
|
|
292
|
+
callId(): Promise<bigint>;
|
|
293
|
+
flowControlled(): Promise<boolean>;
|
|
294
|
+
close(): Promise<void>;
|
|
295
|
+
}
|
|
296
|
+
/** Receive-half of a `RawDuplexCall` after `intoSplit`. */
|
|
297
|
+
export interface RawDuplexStream {
|
|
298
|
+
next(): Promise<Buffer | null>;
|
|
299
|
+
callId(): Promise<bigint>;
|
|
300
|
+
close(): Promise<void>;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Raw napi `JsResponseSink` — outbound side surfaced to duplex
|
|
304
|
+
* server handlers. Non-async (try_send under the hood; drops on
|
|
305
|
+
* mpsc overflow). Returns `false` if the sink has been closed.
|
|
306
|
+
*/
|
|
307
|
+
export interface RawResponseSink {
|
|
308
|
+
send(body: Buffer): boolean;
|
|
80
309
|
}
|
|
81
310
|
/** Raw napi `RpcStream` — minimal shape consumed by `TypedRpcStream`. */
|
|
82
311
|
export interface RawRpcStream {
|
|
@@ -136,6 +365,94 @@ export declare class TypedMeshRpc {
|
|
|
136
365
|
callStreaming<Req = unknown, Resp = unknown>(targetNodeId: bigint, service: string, req: Req, opts?: CallOptions): Promise<TypedRpcStream<Resp>>;
|
|
137
366
|
/** Pass-through to `MeshRpc.findServiceNodes`. */
|
|
138
367
|
findServiceNodes(service: string): bigint[];
|
|
368
|
+
/**
|
|
369
|
+
* Open a typed client-streaming call. Returns a
|
|
370
|
+
* `TypedClientStreamCall<Req, Resp>` — push each request via
|
|
371
|
+
* `send(value)`, then `finish()` to drain the terminal
|
|
372
|
+
* response.
|
|
373
|
+
*
|
|
374
|
+
* Cancellation (v3 / C-A1): `opts.signal` is wired end-to-end
|
|
375
|
+
* via the same `wireAbortSignal` helper unary calls use.
|
|
376
|
+
* `signal.aborted` triggers the substrate's
|
|
377
|
+
* `MeshNode::cancel(token)`, which drops the pending
|
|
378
|
+
* client-stream entry — the next `send()` / `finish()` observes
|
|
379
|
+
* a stream-closed error within bounded time, and the call's
|
|
380
|
+
* Drop emits CANCEL on the wire. Invoking `typedCall.close()`
|
|
381
|
+
* explicitly is still supported as the imperative cancel
|
|
382
|
+
* surface.
|
|
383
|
+
*/
|
|
384
|
+
callClientStream<Req = unknown, Resp = unknown>(targetNodeId: bigint, service: string, opts?: CallOptions): Promise<TypedClientStreamCall<Req, Resp>>;
|
|
385
|
+
/**
|
|
386
|
+
* Register a typed client-streaming handler. The handler
|
|
387
|
+
* receives a `TypedRequestStream<Req>` (auto-decodes each
|
|
388
|
+
* inbound chunk to `Req`) and returns the terminal response
|
|
389
|
+
* `Resp` (encoded back to JSON before reaching the caller).
|
|
390
|
+
*
|
|
391
|
+
* Decode failure on a chunk surfaces from `stream.next()` as
|
|
392
|
+
* `nrpc:codec_decode:` — the handler may catch it (e.g. skip
|
|
393
|
+
* the bad chunk) or let it propagate. Letting it propagate
|
|
394
|
+
* surfaces to the caller as `RpcServerError(Internal)`; the
|
|
395
|
+
* handler may instead wrap it with `appError(NRPC_TYPED_BAD_REQUEST,
|
|
396
|
+
* ...)` to signal a typed bad-request status code.
|
|
397
|
+
*/
|
|
398
|
+
serveClientStream<Req = unknown, Resp = unknown>(service: string, handler: (stream: TypedRequestStream<Req>) => Promise<Resp>): ServeHandle;
|
|
399
|
+
/**
|
|
400
|
+
* Open a typed duplex call. Returns a `TypedDuplexCall<Req,
|
|
401
|
+
* Resp>` — push typed requests via `send`, pull typed responses
|
|
402
|
+
* via `next` (or `for await`), or `intoSplit` to separate the
|
|
403
|
+
* halves for the encoder-task / decoder-task pattern.
|
|
404
|
+
*
|
|
405
|
+
* Cancellation (v3 / C-A1): `opts.signal` is wired through to
|
|
406
|
+
* the substrate's cancel-token primitive — same pattern as
|
|
407
|
+
* `callClientStream`. Aborting the signal drops both halves
|
|
408
|
+
* cleanly; the receive side observes EOF on its next pull and
|
|
409
|
+
* Drop emits CANCEL on the wire. `typedCall.close()` is still
|
|
410
|
+
* supported as the imperative cancel surface.
|
|
411
|
+
*/
|
|
412
|
+
callDuplex<Req = unknown, Resp = unknown>(targetNodeId: bigint, service: string, opts?: CallOptions): Promise<TypedDuplexCall<Req, Resp>>;
|
|
413
|
+
/**
|
|
414
|
+
* Register a typed duplex handler. The user signature is
|
|
415
|
+
* `(stream: TypedRequestStream<Req>, sink: TypedResponseSink<Resp>) => Promise<void>`
|
|
416
|
+
* — the wrapper destructures the napi-side `[stream, sink]`
|
|
417
|
+
* tuple internally so the user handler never sees the array
|
|
418
|
+
* form. Handler return is `void`; the substrate emits the
|
|
419
|
+
* terminal frame automatically. To surface a typed Application
|
|
420
|
+
* status, throw `appError(code, body)` from the handler.
|
|
421
|
+
*/
|
|
422
|
+
serveDuplex<Req = unknown, Resp = unknown>(service: string, handler: (stream: TypedRequestStream<Req>, sink: TypedResponseSink<Resp>) => Promise<void>): ServeHandle;
|
|
423
|
+
/**
|
|
424
|
+
* Install (pass a handler) or clear (pass `null`) the caller-side
|
|
425
|
+
* nRPC observer. The handler fires once per completed outbound
|
|
426
|
+
* RPC with a decoded {@link RpcCallEvent} — the tagged
|
|
427
|
+
* `status` discriminator is reconstructed from the napi POD's
|
|
428
|
+
* flat `statusKind` / `statusMessage` fields.
|
|
429
|
+
*
|
|
430
|
+
* **Callback contract (locked decision #1).** The handler fires
|
|
431
|
+
* synchronously from the substrate's dispatch path via a TSFN in
|
|
432
|
+
* `NonBlocking` mode — if the Node event loop is wedged, events
|
|
433
|
+
* are **dropped**, not buffered. Callbacks must therefore be
|
|
434
|
+
* cheap: push into a queue or ring buffer for slow consumers;
|
|
435
|
+
* do not do work inline. A bounded-mpsc + drop-counter layer is
|
|
436
|
+
* a deliberate post-v1 follow-up.
|
|
437
|
+
*
|
|
438
|
+
* **Mid-call swap.** The substrate uses `ArcSwap` for the
|
|
439
|
+
* observer slot, so swaps are atomic — but a swap mid-call
|
|
440
|
+
* means some events fire against the old handler, some against
|
|
441
|
+
* the new. Set the observer once at startup for clean
|
|
442
|
+
* semantics.
|
|
443
|
+
*
|
|
444
|
+
* v1 only emits `direction === 'outbound'` events; the
|
|
445
|
+
* server-side `'inbound'` hook is a planned follow-up.
|
|
446
|
+
*/
|
|
447
|
+
setObserver(handler: ((evt: RpcCallEvent) => void) | null): void;
|
|
448
|
+
/**
|
|
449
|
+
* Snapshot the per-service nRPC metrics registry. Cheap — one
|
|
450
|
+
* DashMap iteration on the substrate side. Safe to collect on
|
|
451
|
+
* every Prometheus scrape. The returned object is a plain POD
|
|
452
|
+
* (no class wrapping); read fields directly or feed into your
|
|
453
|
+
* own exporter.
|
|
454
|
+
*/
|
|
455
|
+
metricsSnapshot(): RpcMetricsSnapshot;
|
|
139
456
|
/**
|
|
140
457
|
* Direct-addressed typed call with retry. See {@link RetryPolicy}.
|
|
141
458
|
*/
|
|
@@ -167,6 +484,232 @@ export declare class TypedRpcStream<Resp = unknown> implements AsyncIterable<Res
|
|
|
167
484
|
/** Close the stream; emits CANCEL to the server. Idempotent. */
|
|
168
485
|
close(): Promise<void>;
|
|
169
486
|
}
|
|
487
|
+
/**
|
|
488
|
+
* Typed client-streaming call handle. Push typed requests via
|
|
489
|
+
* `send`, then await the terminal response with `finish`. Drop
|
|
490
|
+
* / `close` fires CANCEL via the underlying raw call's drop.
|
|
491
|
+
*/
|
|
492
|
+
export declare class TypedClientStreamCall<Req = unknown, Resp = unknown> {
|
|
493
|
+
private readonly _raw;
|
|
494
|
+
private readonly _detachSignal;
|
|
495
|
+
/**
|
|
496
|
+
* `detachSignal` is the cleanup function returned by
|
|
497
|
+
* `wireAbortSignal`. Run on `close()` (or on call resolution
|
|
498
|
+
* via `finish()`) so the AbortSignal listener doesn't outlive
|
|
499
|
+
* the call.
|
|
500
|
+
*/
|
|
501
|
+
constructor(rawCall: RawClientStreamCall, detachSignal?: () => void);
|
|
502
|
+
/** Underlying raw call for users who want the Buffer-level surface. */
|
|
503
|
+
get raw(): RawClientStreamCall;
|
|
504
|
+
/**
|
|
505
|
+
* Encode `value` as JSON and push it as one request chunk.
|
|
506
|
+
* Throws `nrpc:codec_encode:` if encoding fails (the chunk is
|
|
507
|
+
* NOT sent in that case).
|
|
508
|
+
*/
|
|
509
|
+
send(value: Req): Promise<void>;
|
|
510
|
+
/**
|
|
511
|
+
* Close the upload direction and await the terminal response.
|
|
512
|
+
* Consumes the call — subsequent `send` / `finish` throw
|
|
513
|
+
* `stream_closed`. Decode failure on the terminal Buffer
|
|
514
|
+
* surfaces as `nrpc:codec_decode:`.
|
|
515
|
+
*/
|
|
516
|
+
finish(): Promise<Resp>;
|
|
517
|
+
/** Server-assigned `call_id`. */
|
|
518
|
+
callId(): Promise<bigint>;
|
|
519
|
+
/** `true` if the call was opened with `requestWindowInitial`. */
|
|
520
|
+
flowControlled(): Promise<boolean>;
|
|
521
|
+
/**
|
|
522
|
+
* Close without finishing. Fires CANCEL on the wire if the
|
|
523
|
+
* initial REQUEST has flown. Idempotent. A concurrent in-flight
|
|
524
|
+
* `send()` awaiting flow-control credit observes `stream_closed`.
|
|
525
|
+
*
|
|
526
|
+
* Detaches the `AbortSignal` listener (if one was wired by
|
|
527
|
+
* `TypedMeshRpc.callClientStream(opts.signal)`) so the
|
|
528
|
+
* signal can be reused for a subsequent call.
|
|
529
|
+
*/
|
|
530
|
+
close(): Promise<void>;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Typed inbound request stream surfaced to client-streaming +
|
|
534
|
+
* duplex server handlers. Drain via `next()` until it returns
|
|
535
|
+
* `null` (clean EOF) or implements `AsyncIterable<Req>` for
|
|
536
|
+
* `for await (const req of stream)` style.
|
|
537
|
+
*
|
|
538
|
+
* Decode failure on a chunk surfaces as `nrpc:codec_decode:` —
|
|
539
|
+
* the handler may catch and skip, or let it propagate to abort
|
|
540
|
+
* the call. The underlying raw stream is closed on decode
|
|
541
|
+
* failure so subsequent `next()` returns `null`.
|
|
542
|
+
*/
|
|
543
|
+
export declare class TypedRequestStream<Req = unknown> implements AsyncIterable<Req> {
|
|
544
|
+
private readonly _raw;
|
|
545
|
+
private _done;
|
|
546
|
+
constructor(rawStream: RawRequestStream);
|
|
547
|
+
/** Underlying raw stream for users who need the Buffer-level surface. */
|
|
548
|
+
get raw(): RawRequestStream;
|
|
549
|
+
/** Caller's peer origin hash (`0n` on the loopback fast path). */
|
|
550
|
+
get callerOrigin(): bigint;
|
|
551
|
+
/** Server-assigned `call_id`. */
|
|
552
|
+
get callId(): bigint;
|
|
553
|
+
/**
|
|
554
|
+
* Caller's declared deadline as a Unix-nanoseconds absolute
|
|
555
|
+
* timestamp. `0n` means no deadline was declared.
|
|
556
|
+
*/
|
|
557
|
+
get deadlineNs(): bigint;
|
|
558
|
+
/**
|
|
559
|
+
* Initial-REQUEST headers carried by the caller. Each entry is
|
|
560
|
+
* `[name, value]` with `name` lowercase.
|
|
561
|
+
*/
|
|
562
|
+
get headers(): [string, Buffer][];
|
|
563
|
+
/**
|
|
564
|
+
* Pull the next decoded request. Returns `null` on clean EOF.
|
|
565
|
+
* Throws `nrpc:codec_decode:` on decode failure (the underlying
|
|
566
|
+
* raw stream is marked closed; subsequent `next()` returns
|
|
567
|
+
* `null`).
|
|
568
|
+
*/
|
|
569
|
+
next(): Promise<Req | null>;
|
|
570
|
+
/** Async iterator support: `for await (const req of stream) { ... }`. */
|
|
571
|
+
[Symbol.asyncIterator](): AsyncIterator<Req>;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Typed duplex call handle. Push typed requests via `send`,
|
|
575
|
+
* pull typed responses via `next` or `for await`, or call
|
|
576
|
+
* `intoSplit` to peel off independent sink + stream halves.
|
|
577
|
+
*
|
|
578
|
+
* After `intoSplit` returns, this call is "consumed" — calling
|
|
579
|
+
* `send` / `finishSending` / `next` / `close` on it throws.
|
|
580
|
+
*/
|
|
581
|
+
export declare class TypedDuplexCall<Req = unknown, Resp = unknown> implements AsyncIterable<Resp> {
|
|
582
|
+
private readonly _raw;
|
|
583
|
+
private _done;
|
|
584
|
+
private readonly _detachSignal;
|
|
585
|
+
/**
|
|
586
|
+
* `detachSignal` is the cleanup function returned by
|
|
587
|
+
* `wireAbortSignal`. Run on `close()` (or transferred to one
|
|
588
|
+
* of the split halves on `intoSplit`) so the AbortSignal
|
|
589
|
+
* listener doesn't outlive the call.
|
|
590
|
+
*/
|
|
591
|
+
constructor(rawCall: RawDuplexCall, detachSignal?: () => void);
|
|
592
|
+
/** Underlying raw call for users who want the Buffer-level surface. */
|
|
593
|
+
get raw(): RawDuplexCall;
|
|
594
|
+
/** Encode + push one request chunk. */
|
|
595
|
+
send(value: Req): Promise<void>;
|
|
596
|
+
/** Close the upload direction (emit REQUEST_END). */
|
|
597
|
+
finishSending(): Promise<void>;
|
|
598
|
+
/**
|
|
599
|
+
* Pull the next decoded response. Returns `null` on clean EOF.
|
|
600
|
+
* Decode failure throws `nrpc:codec_decode:` and closes the
|
|
601
|
+
* underlying duplex call — subsequent `next()` returns `null`.
|
|
602
|
+
*/
|
|
603
|
+
next(): Promise<Resp | null>;
|
|
604
|
+
/** Async iterator support over the response stream. */
|
|
605
|
+
[Symbol.asyncIterator](): AsyncIterator<Resp>;
|
|
606
|
+
/**
|
|
607
|
+
* Split the duplex into independent typed sink + stream halves.
|
|
608
|
+
* After return, this `TypedDuplexCall` is consumed — subsequent
|
|
609
|
+
* `send` / `finishSending` / `next` throw `stream_closed`.
|
|
610
|
+
* CANCEL fires only when BOTH split halves drop without
|
|
611
|
+
* observing the response stream's terminal frame.
|
|
612
|
+
*
|
|
613
|
+
* The AbortSignal listener (if one was wired) transfers to the
|
|
614
|
+
* sink half — the sink is the half that issues the upload,
|
|
615
|
+
* which is typically dropped first. Wherever the listener
|
|
616
|
+
* ends up, it stays attached until that half's `close()` runs;
|
|
617
|
+
* the stream half can still observe cancel-mid-flight via the
|
|
618
|
+
* substrate's cancel-token primitive even if the sink half's
|
|
619
|
+
* listener has already detached.
|
|
620
|
+
*/
|
|
621
|
+
intoSplit(): Promise<[
|
|
622
|
+
TypedDuplexSink<Req>,
|
|
623
|
+
TypedDuplexStream<Resp>
|
|
624
|
+
]>;
|
|
625
|
+
/** Server-assigned `call_id`. */
|
|
626
|
+
callId(): Promise<bigint>;
|
|
627
|
+
/**
|
|
628
|
+
* `true` if the call was opened with non-`None`
|
|
629
|
+
* `requestWindowInitial`. Reports the UPLOAD-direction
|
|
630
|
+
* flow-control state.
|
|
631
|
+
*/
|
|
632
|
+
flowControlled(): Promise<boolean>;
|
|
633
|
+
/**
|
|
634
|
+
* Close without observing the response terminator. Fires
|
|
635
|
+
* CANCEL on the wire. Idempotent. Concurrent in-flight
|
|
636
|
+
* `send()` awaiting credit observes `stream_closed`.
|
|
637
|
+
*
|
|
638
|
+
* Detaches the `AbortSignal` listener (if one was wired by
|
|
639
|
+
* `TypedMeshRpc.callDuplex(opts.signal)`) so the signal can
|
|
640
|
+
* be reused for a subsequent call.
|
|
641
|
+
*/
|
|
642
|
+
close(): Promise<void>;
|
|
643
|
+
}
|
|
644
|
+
/** Send-half of a typed duplex call after `intoSplit`. */
|
|
645
|
+
export declare class TypedDuplexSink<Req = unknown> {
|
|
646
|
+
private readonly _raw;
|
|
647
|
+
private readonly _detachSignal;
|
|
648
|
+
constructor(rawSink: RawDuplexSink, detachSignal?: () => void);
|
|
649
|
+
/** Underlying raw sink for Buffer-level access. */
|
|
650
|
+
get raw(): RawDuplexSink;
|
|
651
|
+
/** Encode + push one request chunk. */
|
|
652
|
+
send(value: Req): Promise<void>;
|
|
653
|
+
/** Close the upload direction (emit REQUEST_END). */
|
|
654
|
+
finish(): Promise<void>;
|
|
655
|
+
/** Server-assigned `call_id`. */
|
|
656
|
+
callId(): Promise<bigint>;
|
|
657
|
+
/** `true` if the call was opened with `requestWindowInitial`. */
|
|
658
|
+
flowControlled(): Promise<boolean>;
|
|
659
|
+
/**
|
|
660
|
+
* Close without emitting REQUEST_END. Idempotent. Concurrent
|
|
661
|
+
* in-flight `send()` awaiting credit observes `stream_closed`.
|
|
662
|
+
*
|
|
663
|
+
* Detaches the AbortSignal listener (if one was transferred from
|
|
664
|
+
* the parent `TypedDuplexCall` via `intoSplit`).
|
|
665
|
+
*/
|
|
666
|
+
close(): Promise<void>;
|
|
667
|
+
}
|
|
668
|
+
/** Receive-half of a typed duplex call after `intoSplit`. */
|
|
669
|
+
export declare class TypedDuplexStream<Resp = unknown> implements AsyncIterable<Resp> {
|
|
670
|
+
private readonly _raw;
|
|
671
|
+
private _done;
|
|
672
|
+
constructor(rawStream: RawDuplexStream);
|
|
673
|
+
/** Underlying raw stream for Buffer-level access. */
|
|
674
|
+
get raw(): RawDuplexStream;
|
|
675
|
+
/**
|
|
676
|
+
* Pull the next decoded response. Returns `null` on clean EOF.
|
|
677
|
+
* Decode failure throws `nrpc:codec_decode:` and closes the
|
|
678
|
+
* underlying stream.
|
|
679
|
+
*/
|
|
680
|
+
next(): Promise<Resp | null>;
|
|
681
|
+
/** Async iterator support over the response stream. */
|
|
682
|
+
[Symbol.asyncIterator](): AsyncIterator<Resp>;
|
|
683
|
+
/** Server-assigned `call_id`. */
|
|
684
|
+
callId(): Promise<bigint>;
|
|
685
|
+
/** Close the stream. Idempotent. */
|
|
686
|
+
close(): Promise<void>;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Typed outbound response sink for duplex server handlers.
|
|
690
|
+
* Non-async — mirrors `RawResponseSink.send`. Returns `true`
|
|
691
|
+
* when the chunk was enqueued; `false` if the underlying sink
|
|
692
|
+
* is closed. Encode failure throws `nrpc:codec_encode:` and the
|
|
693
|
+
* chunk is NOT sent.
|
|
694
|
+
*
|
|
695
|
+
* Flow control: the underlying sink `try_send`s into a bounded
|
|
696
|
+
* 1024-chunk mpsc; bursts past the credit window are dropped
|
|
697
|
+
* (counted by `streaming_chunks_dropped_total`). Pace your
|
|
698
|
+
* `send` calls via REQUEST_GRANT cadence for lossless flow
|
|
699
|
+
* control.
|
|
700
|
+
*/
|
|
701
|
+
export declare class TypedResponseSink<Resp = unknown> {
|
|
702
|
+
private readonly _raw;
|
|
703
|
+
constructor(rawSink: RawResponseSink);
|
|
704
|
+
/** Underlying raw sink for Buffer-level access. */
|
|
705
|
+
get raw(): RawResponseSink;
|
|
706
|
+
/**
|
|
707
|
+
* Encode + emit one response chunk. Returns `true` on
|
|
708
|
+
* successful enqueue; `false` if the sink has been closed.
|
|
709
|
+
* Throws `nrpc:codec_encode:` on encoding failure.
|
|
710
|
+
*/
|
|
711
|
+
send(value: Resp): boolean;
|
|
712
|
+
}
|
|
170
713
|
export declare function defaultRetryable(err: unknown): boolean;
|
|
171
714
|
export interface RetryPolicyOptions {
|
|
172
715
|
/** Total attempts (NOT additional retries). Default 3. Must be >= 1. */
|
|
@@ -295,3 +838,11 @@ export declare function appError(code: number, body: string | Buffer): Error;
|
|
|
295
838
|
export declare const NRPC_TYPED_BAD_REQUEST: 32768;
|
|
296
839
|
/** RpcStatus::Application(0x8001): typed handler returned `throw`. */
|
|
297
840
|
export declare const NRPC_TYPED_HANDLER_ERROR: 32769;
|
|
841
|
+
/**
|
|
842
|
+
* Convert the napi POD shape (`RawRpcCallEvent` with flat
|
|
843
|
+
* `statusKind` / `statusMessage` fields) into the TS-idiomatic
|
|
844
|
+
* tagged `RpcCallEvent`. Exported for tests that need to
|
|
845
|
+
* synthesize observer events; production code receives already-
|
|
846
|
+
* decoded events via `TypedMeshRpc.setObserver`.
|
|
847
|
+
*/
|
|
848
|
+
export declare function rawEventToTyped(raw: RawRpcCallEvent): RpcCallEvent;
|
package/mesh_rpc.js
CHANGED
|
@@ -21,10 +21,28 @@
|
|
|
21
21
|
// const policy = new RetryPolicy({ maxAttempts: 3 })
|
|
22
22
|
// const reply = await rpc.callWithRetry(targetId, 'echo', req, undefined, policy)
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
-
exports.NRPC_TYPED_HANDLER_ERROR = exports.NRPC_TYPED_BAD_REQUEST = exports.CircuitBreaker = exports.BreakerOpenError = exports.HedgePolicy = exports.RetryPolicy = exports.TypedRpcStream = exports.TypedMeshRpc = void 0;
|
|
24
|
+
exports.NRPC_TYPED_HANDLER_ERROR = exports.NRPC_TYPED_BAD_REQUEST = exports.CircuitBreaker = exports.BreakerOpenError = exports.HedgePolicy = exports.RetryPolicy = exports.TypedResponseSink = exports.TypedDuplexStream = exports.TypedDuplexSink = exports.TypedDuplexCall = exports.TypedRequestStream = exports.TypedClientStreamCall = exports.TypedRpcStream = exports.TypedMeshRpc = exports.DIRECTION_INBOUND = exports.DIRECTION_OUTBOUND = exports.STATUS_KIND_CANCELED = exports.STATUS_KIND_TIMEOUT = exports.STATUS_KIND_ERROR = exports.STATUS_KIND_OK = void 0;
|
|
25
25
|
exports.defaultRetryable = defaultRetryable;
|
|
26
26
|
exports.defaultBreakerFailure = defaultBreakerFailure;
|
|
27
27
|
exports.appError = appError;
|
|
28
|
+
exports.rawEventToTyped = rawEventToTyped;
|
|
29
|
+
/**
|
|
30
|
+
* Status-kind discriminants emitted on `RawRpcCallEvent.statusKind`
|
|
31
|
+
* and consumed when normalizing into the tagged-union
|
|
32
|
+
* {@link RpcCallStatus}. Prefer these constants over hard-coding
|
|
33
|
+
* the string literal in `if (evt.statusKind === '...')` checks —
|
|
34
|
+
* a typo on the literal silently never fires.
|
|
35
|
+
*
|
|
36
|
+
* Named `STATUS_KIND_*` to disambiguate from the wire-level
|
|
37
|
+
* `STATUS_*` u16 status codes used by `RpcServerError`.
|
|
38
|
+
*/
|
|
39
|
+
exports.STATUS_KIND_OK = 'ok';
|
|
40
|
+
exports.STATUS_KIND_ERROR = 'error';
|
|
41
|
+
exports.STATUS_KIND_TIMEOUT = 'timeout';
|
|
42
|
+
exports.STATUS_KIND_CANCELED = 'canceled';
|
|
43
|
+
/** Direction-kind discriminants on `RawRpcCallEvent.direction`. */
|
|
44
|
+
exports.DIRECTION_OUTBOUND = 'outbound';
|
|
45
|
+
exports.DIRECTION_INBOUND = 'inbound';
|
|
28
46
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
29
47
|
const native = require('./index');
|
|
30
48
|
// Duck-typed message extractor — keeps the "any rejected value
|
|
@@ -172,6 +190,147 @@ class TypedMeshRpc {
|
|
|
172
190
|
findServiceNodes(service) {
|
|
173
191
|
return this._raw.findServiceNodes(service);
|
|
174
192
|
}
|
|
193
|
+
// ---- client-streaming (S2-B1) -------------------------------------------
|
|
194
|
+
/**
|
|
195
|
+
* Open a typed client-streaming call. Returns a
|
|
196
|
+
* `TypedClientStreamCall<Req, Resp>` — push each request via
|
|
197
|
+
* `send(value)`, then `finish()` to drain the terminal
|
|
198
|
+
* response.
|
|
199
|
+
*
|
|
200
|
+
* Cancellation (v3 / C-A1): `opts.signal` is wired end-to-end
|
|
201
|
+
* via the same `wireAbortSignal` helper unary calls use.
|
|
202
|
+
* `signal.aborted` triggers the substrate's
|
|
203
|
+
* `MeshNode::cancel(token)`, which drops the pending
|
|
204
|
+
* client-stream entry — the next `send()` / `finish()` observes
|
|
205
|
+
* a stream-closed error within bounded time, and the call's
|
|
206
|
+
* Drop emits CANCEL on the wire. Invoking `typedCall.close()`
|
|
207
|
+
* explicitly is still supported as the imperative cancel
|
|
208
|
+
* surface.
|
|
209
|
+
*/
|
|
210
|
+
async callClientStream(targetNodeId, service, opts) {
|
|
211
|
+
const { rawOpts, detach } = wireAbortSignal(this._raw, opts);
|
|
212
|
+
try {
|
|
213
|
+
const rawCall = await this._raw.callClientStream(targetNodeId, service, rawOpts);
|
|
214
|
+
return new TypedClientStreamCall(rawCall, detach);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
detach();
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Register a typed client-streaming handler. The handler
|
|
223
|
+
* receives a `TypedRequestStream<Req>` (auto-decodes each
|
|
224
|
+
* inbound chunk to `Req`) and returns the terminal response
|
|
225
|
+
* `Resp` (encoded back to JSON before reaching the caller).
|
|
226
|
+
*
|
|
227
|
+
* Decode failure on a chunk surfaces from `stream.next()` as
|
|
228
|
+
* `nrpc:codec_decode:` — the handler may catch it (e.g. skip
|
|
229
|
+
* the bad chunk) or let it propagate. Letting it propagate
|
|
230
|
+
* surfaces to the caller as `RpcServerError(Internal)`; the
|
|
231
|
+
* handler may instead wrap it with `appError(NRPC_TYPED_BAD_REQUEST,
|
|
232
|
+
* ...)` to signal a typed bad-request status code.
|
|
233
|
+
*/
|
|
234
|
+
serveClientStream(service, handler) {
|
|
235
|
+
return this._raw.serveClientStream(service, async (rawStream) => {
|
|
236
|
+
const typedStream = new TypedRequestStream(rawStream);
|
|
237
|
+
const resp = await handler(typedStream);
|
|
238
|
+
return jsonEncode(resp);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
// ---- duplex (S2-B2) ----------------------------------------------------
|
|
242
|
+
/**
|
|
243
|
+
* Open a typed duplex call. Returns a `TypedDuplexCall<Req,
|
|
244
|
+
* Resp>` — push typed requests via `send`, pull typed responses
|
|
245
|
+
* via `next` (or `for await`), or `intoSplit` to separate the
|
|
246
|
+
* halves for the encoder-task / decoder-task pattern.
|
|
247
|
+
*
|
|
248
|
+
* Cancellation (v3 / C-A1): `opts.signal` is wired through to
|
|
249
|
+
* the substrate's cancel-token primitive — same pattern as
|
|
250
|
+
* `callClientStream`. Aborting the signal drops both halves
|
|
251
|
+
* cleanly; the receive side observes EOF on its next pull and
|
|
252
|
+
* Drop emits CANCEL on the wire. `typedCall.close()` is still
|
|
253
|
+
* supported as the imperative cancel surface.
|
|
254
|
+
*/
|
|
255
|
+
async callDuplex(targetNodeId, service, opts) {
|
|
256
|
+
const { rawOpts, detach } = wireAbortSignal(this._raw, opts);
|
|
257
|
+
try {
|
|
258
|
+
const rawCall = await this._raw.callDuplex(targetNodeId, service, rawOpts);
|
|
259
|
+
return new TypedDuplexCall(rawCall, detach);
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
detach();
|
|
263
|
+
throw err;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Register a typed duplex handler. The user signature is
|
|
268
|
+
* `(stream: TypedRequestStream<Req>, sink: TypedResponseSink<Resp>) => Promise<void>`
|
|
269
|
+
* — the wrapper destructures the napi-side `[stream, sink]`
|
|
270
|
+
* tuple internally so the user handler never sees the array
|
|
271
|
+
* form. Handler return is `void`; the substrate emits the
|
|
272
|
+
* terminal frame automatically. To surface a typed Application
|
|
273
|
+
* status, throw `appError(code, body)` from the handler.
|
|
274
|
+
*/
|
|
275
|
+
serveDuplex(service, handler) {
|
|
276
|
+
return this._raw.serveDuplex(service, async ([rawStream, rawSink]) => {
|
|
277
|
+
const typedStream = new TypedRequestStream(rawStream);
|
|
278
|
+
const typedSink = new TypedResponseSink(rawSink);
|
|
279
|
+
await handler(typedStream, typedSink);
|
|
280
|
+
// The napi side discards the terminal Buffer for duplex
|
|
281
|
+
// handlers (only the Ok/Err outcome matters — see
|
|
282
|
+
// NodeDuplexRpcHandler at bindings/node/src/mesh_rpc.rs).
|
|
283
|
+
// Return an empty Buffer so the JS signature is satisfied.
|
|
284
|
+
return DUPLEX_TERMINAL_SENTINEL;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
// ---- observer + metrics (S2-B3) ----------------------------------------
|
|
288
|
+
/**
|
|
289
|
+
* Install (pass a handler) or clear (pass `null`) the caller-side
|
|
290
|
+
* nRPC observer. The handler fires once per completed outbound
|
|
291
|
+
* RPC with a decoded {@link RpcCallEvent} — the tagged
|
|
292
|
+
* `status` discriminator is reconstructed from the napi POD's
|
|
293
|
+
* flat `statusKind` / `statusMessage` fields.
|
|
294
|
+
*
|
|
295
|
+
* **Callback contract (locked decision #1).** The handler fires
|
|
296
|
+
* synchronously from the substrate's dispatch path via a TSFN in
|
|
297
|
+
* `NonBlocking` mode — if the Node event loop is wedged, events
|
|
298
|
+
* are **dropped**, not buffered. Callbacks must therefore be
|
|
299
|
+
* cheap: push into a queue or ring buffer for slow consumers;
|
|
300
|
+
* do not do work inline. A bounded-mpsc + drop-counter layer is
|
|
301
|
+
* a deliberate post-v1 follow-up.
|
|
302
|
+
*
|
|
303
|
+
* **Mid-call swap.** The substrate uses `ArcSwap` for the
|
|
304
|
+
* observer slot, so swaps are atomic — but a swap mid-call
|
|
305
|
+
* means some events fire against the old handler, some against
|
|
306
|
+
* the new. Set the observer once at startup for clean
|
|
307
|
+
* semantics.
|
|
308
|
+
*
|
|
309
|
+
* v1 only emits `direction === 'outbound'` events; the
|
|
310
|
+
* server-side `'inbound'` hook is a planned follow-up.
|
|
311
|
+
*/
|
|
312
|
+
setObserver(handler) {
|
|
313
|
+
if (handler === null) {
|
|
314
|
+
this._raw.setObserver(null);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
// Normalize the napi POD's flat `statusKind` / `statusMessage`
|
|
318
|
+
// into the TS-idiomatic tagged `RpcCallStatus` discriminator
|
|
319
|
+
// before reaching user code.
|
|
320
|
+
this._raw.setObserver((raw) => {
|
|
321
|
+
handler(rawEventToTyped(raw));
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Snapshot the per-service nRPC metrics registry. Cheap — one
|
|
326
|
+
* DashMap iteration on the substrate side. Safe to collect on
|
|
327
|
+
* every Prometheus scrape. The returned object is a plain POD
|
|
328
|
+
* (no class wrapping); read fields directly or feed into your
|
|
329
|
+
* own exporter.
|
|
330
|
+
*/
|
|
331
|
+
metricsSnapshot() {
|
|
332
|
+
return this._raw.metricsSnapshot();
|
|
333
|
+
}
|
|
175
334
|
// ---- resilience helpers --------------------------------------------------
|
|
176
335
|
/**
|
|
177
336
|
* Direct-addressed typed call with retry. See {@link RetryPolicy}.
|
|
@@ -277,6 +436,486 @@ class TypedRpcStream {
|
|
|
277
436
|
}
|
|
278
437
|
exports.TypedRpcStream = TypedRpcStream;
|
|
279
438
|
// ============================================================================
|
|
439
|
+
// TypedClientStreamCall + TypedRequestStream (S2-B1).
|
|
440
|
+
//
|
|
441
|
+
// Client-streaming: caller pushes typed Reqs via `send`, then
|
|
442
|
+
// `finish` awaits a single terminal Resp.
|
|
443
|
+
//
|
|
444
|
+
// Server-side handler shape mirrors the unary `serve`'s decode
|
|
445
|
+
// boundary — chunks decode lazily inside `TypedRequestStream.next`
|
|
446
|
+
// so a single malformed chunk doesn't poison the entire stream
|
|
447
|
+
// (the handler can catch and skip).
|
|
448
|
+
//
|
|
449
|
+
// Cancellation contract (locked decision #2): v1 supports
|
|
450
|
+
// `close()`-only cancellation. AbortSignal / cancel-token wiring
|
|
451
|
+
// across streaming shapes is a deliberate post-v1 follow-up.
|
|
452
|
+
// ============================================================================
|
|
453
|
+
/**
|
|
454
|
+
* Typed client-streaming call handle. Push typed requests via
|
|
455
|
+
* `send`, then await the terminal response with `finish`. Drop
|
|
456
|
+
* / `close` fires CANCEL via the underlying raw call's drop.
|
|
457
|
+
*/
|
|
458
|
+
class TypedClientStreamCall {
|
|
459
|
+
_raw;
|
|
460
|
+
_detachSignal;
|
|
461
|
+
/**
|
|
462
|
+
* `detachSignal` is the cleanup function returned by
|
|
463
|
+
* `wireAbortSignal`. Run on `close()` (or on call resolution
|
|
464
|
+
* via `finish()`) so the AbortSignal listener doesn't outlive
|
|
465
|
+
* the call.
|
|
466
|
+
*/
|
|
467
|
+
constructor(rawCall, detachSignal = () => { }) {
|
|
468
|
+
this._raw = rawCall;
|
|
469
|
+
this._detachSignal = detachSignal;
|
|
470
|
+
}
|
|
471
|
+
/** Underlying raw call for users who want the Buffer-level surface. */
|
|
472
|
+
get raw() {
|
|
473
|
+
return this._raw;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Encode `value` as JSON and push it as one request chunk.
|
|
477
|
+
* Throws `nrpc:codec_encode:` if encoding fails (the chunk is
|
|
478
|
+
* NOT sent in that case).
|
|
479
|
+
*/
|
|
480
|
+
async send(value) {
|
|
481
|
+
const buf = jsonEncode(value);
|
|
482
|
+
await this._raw.send(buf);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Close the upload direction and await the terminal response.
|
|
486
|
+
* Consumes the call — subsequent `send` / `finish` throw
|
|
487
|
+
* `stream_closed`. Decode failure on the terminal Buffer
|
|
488
|
+
* surfaces as `nrpc:codec_decode:`.
|
|
489
|
+
*/
|
|
490
|
+
async finish() {
|
|
491
|
+
try {
|
|
492
|
+
const buf = await this._raw.finish();
|
|
493
|
+
return jsonDecode(buf);
|
|
494
|
+
}
|
|
495
|
+
finally {
|
|
496
|
+
this._detachSignal();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/** Server-assigned `call_id`. */
|
|
500
|
+
async callId() {
|
|
501
|
+
return await this._raw.callId();
|
|
502
|
+
}
|
|
503
|
+
/** `true` if the call was opened with `requestWindowInitial`. */
|
|
504
|
+
async flowControlled() {
|
|
505
|
+
return await this._raw.flowControlled();
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Close without finishing. Fires CANCEL on the wire if the
|
|
509
|
+
* initial REQUEST has flown. Idempotent. A concurrent in-flight
|
|
510
|
+
* `send()` awaiting flow-control credit observes `stream_closed`.
|
|
511
|
+
*
|
|
512
|
+
* Detaches the `AbortSignal` listener (if one was wired by
|
|
513
|
+
* `TypedMeshRpc.callClientStream(opts.signal)`) so the
|
|
514
|
+
* signal can be reused for a subsequent call.
|
|
515
|
+
*/
|
|
516
|
+
async close() {
|
|
517
|
+
this._detachSignal();
|
|
518
|
+
try {
|
|
519
|
+
await this._raw.close();
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
/* swallow — best-effort */
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
exports.TypedClientStreamCall = TypedClientStreamCall;
|
|
527
|
+
/**
|
|
528
|
+
* Typed inbound request stream surfaced to client-streaming +
|
|
529
|
+
* duplex server handlers. Drain via `next()` until it returns
|
|
530
|
+
* `null` (clean EOF) or implements `AsyncIterable<Req>` for
|
|
531
|
+
* `for await (const req of stream)` style.
|
|
532
|
+
*
|
|
533
|
+
* Decode failure on a chunk surfaces as `nrpc:codec_decode:` —
|
|
534
|
+
* the handler may catch and skip, or let it propagate to abort
|
|
535
|
+
* the call. The underlying raw stream is closed on decode
|
|
536
|
+
* failure so subsequent `next()` returns `null`.
|
|
537
|
+
*/
|
|
538
|
+
class TypedRequestStream {
|
|
539
|
+
_raw;
|
|
540
|
+
_done;
|
|
541
|
+
constructor(rawStream) {
|
|
542
|
+
this._raw = rawStream;
|
|
543
|
+
this._done = false;
|
|
544
|
+
}
|
|
545
|
+
/** Underlying raw stream for users who need the Buffer-level surface. */
|
|
546
|
+
get raw() {
|
|
547
|
+
return this._raw;
|
|
548
|
+
}
|
|
549
|
+
/** Caller's peer origin hash (`0n` on the loopback fast path). */
|
|
550
|
+
get callerOrigin() {
|
|
551
|
+
return this._raw.callerOrigin;
|
|
552
|
+
}
|
|
553
|
+
/** Server-assigned `call_id`. */
|
|
554
|
+
get callId() {
|
|
555
|
+
return this._raw.callId;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Caller's declared deadline as a Unix-nanoseconds absolute
|
|
559
|
+
* timestamp. `0n` means no deadline was declared.
|
|
560
|
+
*/
|
|
561
|
+
get deadlineNs() {
|
|
562
|
+
return this._raw.deadlineNs;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Initial-REQUEST headers carried by the caller. Each entry is
|
|
566
|
+
* `[name, value]` with `name` lowercase.
|
|
567
|
+
*/
|
|
568
|
+
get headers() {
|
|
569
|
+
return this._raw.headers;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Pull the next decoded request. Returns `null` on clean EOF.
|
|
573
|
+
* Throws `nrpc:codec_decode:` on decode failure (the underlying
|
|
574
|
+
* raw stream is marked closed; subsequent `next()` returns
|
|
575
|
+
* `null`).
|
|
576
|
+
*/
|
|
577
|
+
async next() {
|
|
578
|
+
if (this._done)
|
|
579
|
+
return null;
|
|
580
|
+
let buf;
|
|
581
|
+
try {
|
|
582
|
+
buf = await this._raw.next();
|
|
583
|
+
}
|
|
584
|
+
catch (e) {
|
|
585
|
+
this._done = true;
|
|
586
|
+
throw e;
|
|
587
|
+
}
|
|
588
|
+
if (buf === null || buf === undefined) {
|
|
589
|
+
this._done = true;
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
return jsonDecode(buf);
|
|
594
|
+
}
|
|
595
|
+
catch (e) {
|
|
596
|
+
// Mark done so subsequent next() returns null — refuse to
|
|
597
|
+
// continue draining a stream whose framing is broken.
|
|
598
|
+
this._done = true;
|
|
599
|
+
throw e;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/** Async iterator support: `for await (const req of stream) { ... }`. */
|
|
603
|
+
async *[Symbol.asyncIterator]() {
|
|
604
|
+
while (true) {
|
|
605
|
+
const value = await this.next();
|
|
606
|
+
if (value === null)
|
|
607
|
+
return;
|
|
608
|
+
yield value;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
exports.TypedRequestStream = TypedRequestStream;
|
|
613
|
+
// ============================================================================
|
|
614
|
+
// TypedDuplexCall + TypedDuplexSink + TypedDuplexStream +
|
|
615
|
+
// TypedResponseSink (S2-B2).
|
|
616
|
+
//
|
|
617
|
+
// Duplex: caller pushes Reqs and pulls Resps concurrently on a
|
|
618
|
+
// single call. `intoSplit` separates the halves for the encoder-
|
|
619
|
+
// task / decoder-task pattern where each half lives in its own
|
|
620
|
+
// async task.
|
|
621
|
+
//
|
|
622
|
+
// Cancellation contract (locked decision #2): v1 `.close()`-only,
|
|
623
|
+
// same as client-streaming.
|
|
624
|
+
// ============================================================================
|
|
625
|
+
/**
|
|
626
|
+
* Constant sentinel returned by the duplex serve-handler shim.
|
|
627
|
+
* The napi `NodeDuplexRpcHandler` discards the terminal Buffer
|
|
628
|
+
* (only the Ok/Err outcome matters for duplex), so a stable
|
|
629
|
+
* empty Buffer is enough.
|
|
630
|
+
*/
|
|
631
|
+
const DUPLEX_TERMINAL_SENTINEL = Buffer.alloc(0);
|
|
632
|
+
/**
|
|
633
|
+
* Typed duplex call handle. Push typed requests via `send`,
|
|
634
|
+
* pull typed responses via `next` or `for await`, or call
|
|
635
|
+
* `intoSplit` to peel off independent sink + stream halves.
|
|
636
|
+
*
|
|
637
|
+
* After `intoSplit` returns, this call is "consumed" — calling
|
|
638
|
+
* `send` / `finishSending` / `next` / `close` on it throws.
|
|
639
|
+
*/
|
|
640
|
+
class TypedDuplexCall {
|
|
641
|
+
_raw;
|
|
642
|
+
_done;
|
|
643
|
+
_detachSignal;
|
|
644
|
+
/**
|
|
645
|
+
* `detachSignal` is the cleanup function returned by
|
|
646
|
+
* `wireAbortSignal`. Run on `close()` (or transferred to one
|
|
647
|
+
* of the split halves on `intoSplit`) so the AbortSignal
|
|
648
|
+
* listener doesn't outlive the call.
|
|
649
|
+
*/
|
|
650
|
+
constructor(rawCall, detachSignal = () => { }) {
|
|
651
|
+
this._raw = rawCall;
|
|
652
|
+
this._done = false;
|
|
653
|
+
this._detachSignal = detachSignal;
|
|
654
|
+
}
|
|
655
|
+
/** Underlying raw call for users who want the Buffer-level surface. */
|
|
656
|
+
get raw() {
|
|
657
|
+
return this._raw;
|
|
658
|
+
}
|
|
659
|
+
/** Encode + push one request chunk. */
|
|
660
|
+
async send(value) {
|
|
661
|
+
const buf = jsonEncode(value);
|
|
662
|
+
await this._raw.send(buf);
|
|
663
|
+
}
|
|
664
|
+
/** Close the upload direction (emit REQUEST_END). */
|
|
665
|
+
async finishSending() {
|
|
666
|
+
await this._raw.finishSending();
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Pull the next decoded response. Returns `null` on clean EOF.
|
|
670
|
+
* Decode failure throws `nrpc:codec_decode:` and closes the
|
|
671
|
+
* underlying duplex call — subsequent `next()` returns `null`.
|
|
672
|
+
*/
|
|
673
|
+
async next() {
|
|
674
|
+
if (this._done)
|
|
675
|
+
return null;
|
|
676
|
+
let buf;
|
|
677
|
+
try {
|
|
678
|
+
buf = await this._raw.next();
|
|
679
|
+
}
|
|
680
|
+
catch (e) {
|
|
681
|
+
this._done = true;
|
|
682
|
+
throw e;
|
|
683
|
+
}
|
|
684
|
+
if (buf === null || buf === undefined) {
|
|
685
|
+
this._done = true;
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
return jsonDecode(buf);
|
|
690
|
+
}
|
|
691
|
+
catch (e) {
|
|
692
|
+
this._done = true;
|
|
693
|
+
try {
|
|
694
|
+
await this._raw.close();
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
/* swallow — best-effort */
|
|
698
|
+
}
|
|
699
|
+
throw e;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
/** Async iterator support over the response stream. */
|
|
703
|
+
async *[Symbol.asyncIterator]() {
|
|
704
|
+
while (true) {
|
|
705
|
+
const value = await this.next();
|
|
706
|
+
if (value === null)
|
|
707
|
+
return;
|
|
708
|
+
yield value;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Split the duplex into independent typed sink + stream halves.
|
|
713
|
+
* After return, this `TypedDuplexCall` is consumed — subsequent
|
|
714
|
+
* `send` / `finishSending` / `next` throw `stream_closed`.
|
|
715
|
+
* CANCEL fires only when BOTH split halves drop without
|
|
716
|
+
* observing the response stream's terminal frame.
|
|
717
|
+
*
|
|
718
|
+
* The AbortSignal listener (if one was wired) transfers to the
|
|
719
|
+
* sink half — the sink is the half that issues the upload,
|
|
720
|
+
* which is typically dropped first. Wherever the listener
|
|
721
|
+
* ends up, it stays attached until that half's `close()` runs;
|
|
722
|
+
* the stream half can still observe cancel-mid-flight via the
|
|
723
|
+
* substrate's cancel-token primitive even if the sink half's
|
|
724
|
+
* listener has already detached.
|
|
725
|
+
*/
|
|
726
|
+
async intoSplit() {
|
|
727
|
+
const [rawSink, rawStream] = await this._raw.intoSplit();
|
|
728
|
+
this._done = true;
|
|
729
|
+
return [
|
|
730
|
+
new TypedDuplexSink(rawSink, this._detachSignal),
|
|
731
|
+
new TypedDuplexStream(rawStream),
|
|
732
|
+
];
|
|
733
|
+
}
|
|
734
|
+
/** Server-assigned `call_id`. */
|
|
735
|
+
async callId() {
|
|
736
|
+
return await this._raw.callId();
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* `true` if the call was opened with non-`None`
|
|
740
|
+
* `requestWindowInitial`. Reports the UPLOAD-direction
|
|
741
|
+
* flow-control state.
|
|
742
|
+
*/
|
|
743
|
+
async flowControlled() {
|
|
744
|
+
return await this._raw.flowControlled();
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Close without observing the response terminator. Fires
|
|
748
|
+
* CANCEL on the wire. Idempotent. Concurrent in-flight
|
|
749
|
+
* `send()` awaiting credit observes `stream_closed`.
|
|
750
|
+
*
|
|
751
|
+
* Detaches the `AbortSignal` listener (if one was wired by
|
|
752
|
+
* `TypedMeshRpc.callDuplex(opts.signal)`) so the signal can
|
|
753
|
+
* be reused for a subsequent call.
|
|
754
|
+
*/
|
|
755
|
+
async close() {
|
|
756
|
+
this._done = true;
|
|
757
|
+
this._detachSignal();
|
|
758
|
+
try {
|
|
759
|
+
await this._raw.close();
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
/* swallow — best-effort */
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
exports.TypedDuplexCall = TypedDuplexCall;
|
|
767
|
+
/** Send-half of a typed duplex call after `intoSplit`. */
|
|
768
|
+
class TypedDuplexSink {
|
|
769
|
+
_raw;
|
|
770
|
+
_detachSignal;
|
|
771
|
+
constructor(rawSink, detachSignal = () => { }) {
|
|
772
|
+
this._raw = rawSink;
|
|
773
|
+
this._detachSignal = detachSignal;
|
|
774
|
+
}
|
|
775
|
+
/** Underlying raw sink for Buffer-level access. */
|
|
776
|
+
get raw() {
|
|
777
|
+
return this._raw;
|
|
778
|
+
}
|
|
779
|
+
/** Encode + push one request chunk. */
|
|
780
|
+
async send(value) {
|
|
781
|
+
const buf = jsonEncode(value);
|
|
782
|
+
await this._raw.send(buf);
|
|
783
|
+
}
|
|
784
|
+
/** Close the upload direction (emit REQUEST_END). */
|
|
785
|
+
async finish() {
|
|
786
|
+
await this._raw.finish();
|
|
787
|
+
}
|
|
788
|
+
/** Server-assigned `call_id`. */
|
|
789
|
+
async callId() {
|
|
790
|
+
return await this._raw.callId();
|
|
791
|
+
}
|
|
792
|
+
/** `true` if the call was opened with `requestWindowInitial`. */
|
|
793
|
+
async flowControlled() {
|
|
794
|
+
return await this._raw.flowControlled();
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Close without emitting REQUEST_END. Idempotent. Concurrent
|
|
798
|
+
* in-flight `send()` awaiting credit observes `stream_closed`.
|
|
799
|
+
*
|
|
800
|
+
* Detaches the AbortSignal listener (if one was transferred from
|
|
801
|
+
* the parent `TypedDuplexCall` via `intoSplit`).
|
|
802
|
+
*/
|
|
803
|
+
async close() {
|
|
804
|
+
this._detachSignal();
|
|
805
|
+
try {
|
|
806
|
+
await this._raw.close();
|
|
807
|
+
}
|
|
808
|
+
catch {
|
|
809
|
+
/* swallow — best-effort */
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
exports.TypedDuplexSink = TypedDuplexSink;
|
|
814
|
+
/** Receive-half of a typed duplex call after `intoSplit`. */
|
|
815
|
+
class TypedDuplexStream {
|
|
816
|
+
_raw;
|
|
817
|
+
_done;
|
|
818
|
+
constructor(rawStream) {
|
|
819
|
+
this._raw = rawStream;
|
|
820
|
+
this._done = false;
|
|
821
|
+
}
|
|
822
|
+
/** Underlying raw stream for Buffer-level access. */
|
|
823
|
+
get raw() {
|
|
824
|
+
return this._raw;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Pull the next decoded response. Returns `null` on clean EOF.
|
|
828
|
+
* Decode failure throws `nrpc:codec_decode:` and closes the
|
|
829
|
+
* underlying stream.
|
|
830
|
+
*/
|
|
831
|
+
async next() {
|
|
832
|
+
if (this._done)
|
|
833
|
+
return null;
|
|
834
|
+
let buf;
|
|
835
|
+
try {
|
|
836
|
+
buf = await this._raw.next();
|
|
837
|
+
}
|
|
838
|
+
catch (e) {
|
|
839
|
+
this._done = true;
|
|
840
|
+
throw e;
|
|
841
|
+
}
|
|
842
|
+
if (buf === null || buf === undefined) {
|
|
843
|
+
this._done = true;
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
return jsonDecode(buf);
|
|
848
|
+
}
|
|
849
|
+
catch (e) {
|
|
850
|
+
this._done = true;
|
|
851
|
+
try {
|
|
852
|
+
await this._raw.close();
|
|
853
|
+
}
|
|
854
|
+
catch {
|
|
855
|
+
/* swallow — best-effort */
|
|
856
|
+
}
|
|
857
|
+
throw e;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
/** Async iterator support over the response stream. */
|
|
861
|
+
async *[Symbol.asyncIterator]() {
|
|
862
|
+
while (true) {
|
|
863
|
+
const value = await this.next();
|
|
864
|
+
if (value === null)
|
|
865
|
+
return;
|
|
866
|
+
yield value;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
/** Server-assigned `call_id`. */
|
|
870
|
+
async callId() {
|
|
871
|
+
return await this._raw.callId();
|
|
872
|
+
}
|
|
873
|
+
/** Close the stream. Idempotent. */
|
|
874
|
+
async close() {
|
|
875
|
+
this._done = true;
|
|
876
|
+
try {
|
|
877
|
+
await this._raw.close();
|
|
878
|
+
}
|
|
879
|
+
catch {
|
|
880
|
+
/* swallow — best-effort */
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
exports.TypedDuplexStream = TypedDuplexStream;
|
|
885
|
+
/**
|
|
886
|
+
* Typed outbound response sink for duplex server handlers.
|
|
887
|
+
* Non-async — mirrors `RawResponseSink.send`. Returns `true`
|
|
888
|
+
* when the chunk was enqueued; `false` if the underlying sink
|
|
889
|
+
* is closed. Encode failure throws `nrpc:codec_encode:` and the
|
|
890
|
+
* chunk is NOT sent.
|
|
891
|
+
*
|
|
892
|
+
* Flow control: the underlying sink `try_send`s into a bounded
|
|
893
|
+
* 1024-chunk mpsc; bursts past the credit window are dropped
|
|
894
|
+
* (counted by `streaming_chunks_dropped_total`). Pace your
|
|
895
|
+
* `send` calls via REQUEST_GRANT cadence for lossless flow
|
|
896
|
+
* control.
|
|
897
|
+
*/
|
|
898
|
+
class TypedResponseSink {
|
|
899
|
+
_raw;
|
|
900
|
+
constructor(rawSink) {
|
|
901
|
+
this._raw = rawSink;
|
|
902
|
+
}
|
|
903
|
+
/** Underlying raw sink for Buffer-level access. */
|
|
904
|
+
get raw() {
|
|
905
|
+
return this._raw;
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Encode + emit one response chunk. Returns `true` on
|
|
909
|
+
* successful enqueue; `false` if the sink has been closed.
|
|
910
|
+
* Throws `nrpc:codec_encode:` on encoding failure.
|
|
911
|
+
*/
|
|
912
|
+
send(value) {
|
|
913
|
+
const buf = jsonEncode(value);
|
|
914
|
+
return this._raw.send(buf);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
exports.TypedResponseSink = TypedResponseSink;
|
|
918
|
+
// ============================================================================
|
|
280
919
|
// RetryPolicy — mirrors net_sdk::mesh_rpc_resilience::RetryPolicy.
|
|
281
920
|
//
|
|
282
921
|
// Defaults: 3 attempts, 50ms initial backoff, doubling per
|
|
@@ -783,3 +1422,38 @@ function appError(code, body) {
|
|
|
783
1422
|
exports.NRPC_TYPED_BAD_REQUEST = 0x8000;
|
|
784
1423
|
/** RpcStatus::Application(0x8001): typed handler returned `throw`. */
|
|
785
1424
|
exports.NRPC_TYPED_HANDLER_ERROR = 0x8001;
|
|
1425
|
+
/**
|
|
1426
|
+
* Convert the napi POD shape (`RawRpcCallEvent` with flat
|
|
1427
|
+
* `statusKind` / `statusMessage` fields) into the TS-idiomatic
|
|
1428
|
+
* tagged `RpcCallEvent`. Exported for tests that need to
|
|
1429
|
+
* synthesize observer events; production code receives already-
|
|
1430
|
+
* decoded events via `TypedMeshRpc.setObserver`.
|
|
1431
|
+
*/
|
|
1432
|
+
function rawEventToTyped(raw) {
|
|
1433
|
+
let status;
|
|
1434
|
+
switch (raw.statusKind) {
|
|
1435
|
+
case exports.STATUS_KIND_OK:
|
|
1436
|
+
status = { kind: 'ok' };
|
|
1437
|
+
break;
|
|
1438
|
+
case exports.STATUS_KIND_ERROR:
|
|
1439
|
+
status = { kind: 'error', message: raw.statusMessage ?? '' };
|
|
1440
|
+
break;
|
|
1441
|
+
case exports.STATUS_KIND_TIMEOUT:
|
|
1442
|
+
status = { kind: 'timeout' };
|
|
1443
|
+
break;
|
|
1444
|
+
case exports.STATUS_KIND_CANCELED:
|
|
1445
|
+
status = { kind: 'canceled' };
|
|
1446
|
+
break;
|
|
1447
|
+
}
|
|
1448
|
+
return {
|
|
1449
|
+
caller: raw.caller,
|
|
1450
|
+
callee: raw.callee,
|
|
1451
|
+
method: raw.method,
|
|
1452
|
+
latencyMs: raw.latencyMs,
|
|
1453
|
+
status,
|
|
1454
|
+
requestBytes: raw.requestBytes,
|
|
1455
|
+
responseBytes: raw.responseBytes,
|
|
1456
|
+
direction: raw.direction,
|
|
1457
|
+
tsUnixMs: raw.tsUnixMs,
|
|
1458
|
+
};
|
|
1459
|
+
}
|
package/net.darwin-arm64.node
CHANGED
|
Binary file
|
package/net.darwin-x64.node
CHANGED
|
Binary file
|
package/net.linux-arm64-gnu.node
CHANGED
|
Binary file
|
|
Binary file
|
package/net.linux-x64-gnu.node
CHANGED
|
Binary file
|
package/net.linux-x64-musl.node
CHANGED
|
Binary file
|
|
Binary file
|
package/net.win32-x64-msvc.node
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@net-mesh/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "High-performance, schema-agnostic event bus for AI runtime workloads",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -91,13 +91,13 @@
|
|
|
91
91
|
"node": ">=20"
|
|
92
92
|
},
|
|
93
93
|
"optionalDependencies": {
|
|
94
|
-
"@net-mesh/core-win32-x64-msvc": "0.
|
|
95
|
-
"@net-mesh/core-win32-arm64-msvc": "0.
|
|
96
|
-
"@net-mesh/core-darwin-x64": "0.
|
|
97
|
-
"@net-mesh/core-darwin-arm64": "0.
|
|
98
|
-
"@net-mesh/core-linux-x64-gnu": "0.
|
|
99
|
-
"@net-mesh/core-linux-x64-musl": "0.
|
|
100
|
-
"@net-mesh/core-linux-arm64-gnu": "0.
|
|
101
|
-
"@net-mesh/core-linux-arm64-musl": "0.
|
|
94
|
+
"@net-mesh/core-win32-x64-msvc": "0.24.0",
|
|
95
|
+
"@net-mesh/core-win32-arm64-msvc": "0.24.0",
|
|
96
|
+
"@net-mesh/core-darwin-x64": "0.24.0",
|
|
97
|
+
"@net-mesh/core-darwin-arm64": "0.24.0",
|
|
98
|
+
"@net-mesh/core-linux-x64-gnu": "0.24.0",
|
|
99
|
+
"@net-mesh/core-linux-x64-musl": "0.24.0",
|
|
100
|
+
"@net-mesh/core-linux-arm64-gnu": "0.24.0",
|
|
101
|
+
"@net-mesh/core-linux-arm64-musl": "0.24.0"
|
|
102
102
|
}
|
|
103
103
|
}
|