@net-mesh/core 0.21.0 → 0.23.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/aggregator.d.ts +56 -0
- package/aggregator.js +130 -0
- package/mesh_rpc.d.ts +551 -0
- package/mesh_rpc.js +675 -1
- package/meshdb.d.ts +23 -0
- package/meshdb.js +101 -0
- 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 +20 -12
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
|
+
}
|