@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 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
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@net-mesh/core",
3
- "version": "0.22.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.22.0",
95
- "@net-mesh/core-win32-arm64-msvc": "0.22.0",
96
- "@net-mesh/core-darwin-x64": "0.22.0",
97
- "@net-mesh/core-darwin-arm64": "0.22.0",
98
- "@net-mesh/core-linux-x64-gnu": "0.22.0",
99
- "@net-mesh/core-linux-x64-musl": "0.22.0",
100
- "@net-mesh/core-linux-arm64-gnu": "0.22.0",
101
- "@net-mesh/core-linux-arm64-musl": "0.22.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
  }