@zudojs/rpc 0.1.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +104 -12
  3. package/dist/index.d.ts +8 -4
  4. package/dist/index.js +3 -4
  5. package/dist/rpc/client/index.d.ts +1 -1
  6. package/dist/rpc/client/rpcClient.core.d.ts +57 -3
  7. package/dist/rpc/client/rpcClient.core.js +189 -32
  8. package/dist/rpc/constants/index.d.ts +1 -1
  9. package/dist/rpc/constants/index.js +1 -1
  10. package/dist/rpc/constants/rpcConstants.core.d.ts +18 -0
  11. package/dist/rpc/constants/rpcConstants.core.js +18 -0
  12. package/dist/rpc/context/rpcContext.type.js +3 -1
  13. package/dist/rpc/dispatcher/index.d.ts +1 -0
  14. package/dist/rpc/dispatcher/rpcDispatcher.core.d.ts +49 -2
  15. package/dist/rpc/dispatcher/rpcDispatcher.core.js +109 -14
  16. package/dist/rpc/middleware/rpcMiddleware.core.d.ts +12 -0
  17. package/dist/rpc/middleware/rpcMiddleware.core.js +29 -6
  18. package/dist/rpc/procedure/rpcProcedure.type.d.ts +25 -3
  19. package/dist/rpc/procedure/rpcProcedure.type.js +9 -0
  20. package/dist/rpc/procedure/rpcProcedureRegistry.core.d.ts +15 -1
  21. package/dist/rpc/procedure/rpcProcedureRegistry.core.js +26 -1
  22. package/dist/rpc/procedure/rpcProcedureRouter.core.d.ts +1 -1
  23. package/dist/rpc/procedure/rpcProcedureRouter.core.js +5 -0
  24. package/dist/rpc/reliability/cancellation/rpcCancellation.helper.d.ts +30 -3
  25. package/dist/rpc/reliability/cancellation/rpcCancellation.helper.js +60 -4
  26. package/dist/rpc/reliability/deadline/rpcDeadline.helper.d.ts +14 -1
  27. package/dist/rpc/reliability/deadline/rpcDeadline.helper.js +23 -2
  28. package/dist/rpc/reliability/index.d.ts +5 -4
  29. package/dist/rpc/reliability/index.js +3 -3
  30. package/dist/rpc/reliability/retry/rpcRetry.helper.d.ts +22 -0
  31. package/dist/rpc/reliability/retry/rpcRetry.helper.js +80 -8
  32. package/dist/rpc/reliability/timeout/rpcTimeout.helper.d.ts +18 -2
  33. package/dist/rpc/reliability/timeout/rpcTimeout.helper.js +71 -7
  34. package/dist/rpc/server/index.d.ts +1 -0
  35. package/dist/rpc/server/rpcServer.core.d.ts +41 -2
  36. package/dist/rpc/server/rpcServer.core.js +117 -16
  37. package/dist/rpc/streaming/rpcStreaming.type.d.ts +9 -0
  38. package/dist/rpc/streaming/rpcStreaming.type.js +9 -0
  39. package/dist/rpc/transport/rpcTransport.type.d.ts +9 -1
  40. package/dist/rpc/validation/index.d.ts +3 -0
  41. package/dist/rpc/validation/index.js +2 -0
  42. package/dist/rpc/validation/rpcValidation.core.d.ts +86 -0
  43. package/dist/rpc/validation/rpcValidation.core.js +133 -0
  44. package/package.json +27 -16
  45. package/dist/.tsbuildinfo +0 -1
  46. package/dist/index.d.ts.map +0 -1
  47. package/dist/index.js.map +0 -1
  48. package/dist/rpc/client/index.d.ts.map +0 -1
  49. package/dist/rpc/client/index.js.map +0 -1
  50. package/dist/rpc/client/rpcClient.core.d.ts.map +0 -1
  51. package/dist/rpc/client/rpcClient.core.js.map +0 -1
  52. package/dist/rpc/constants/index.d.ts.map +0 -1
  53. package/dist/rpc/constants/index.js.map +0 -1
  54. package/dist/rpc/constants/rpcConstants.core.d.ts.map +0 -1
  55. package/dist/rpc/constants/rpcConstants.core.js.map +0 -1
  56. package/dist/rpc/context/index.d.ts.map +0 -1
  57. package/dist/rpc/context/index.js.map +0 -1
  58. package/dist/rpc/context/rpcContext.type.d.ts.map +0 -1
  59. package/dist/rpc/context/rpcContext.type.js.map +0 -1
  60. package/dist/rpc/dispatcher/index.d.ts.map +0 -1
  61. package/dist/rpc/dispatcher/index.js.map +0 -1
  62. package/dist/rpc/dispatcher/rpcDispatcher.core.d.ts.map +0 -1
  63. package/dist/rpc/dispatcher/rpcDispatcher.core.js.map +0 -1
  64. package/dist/rpc/errors/index.d.ts.map +0 -1
  65. package/dist/rpc/errors/index.js.map +0 -1
  66. package/dist/rpc/errors/rpc.errors.d.ts.map +0 -1
  67. package/dist/rpc/errors/rpc.errors.js.map +0 -1
  68. package/dist/rpc/interceptor/index.d.ts.map +0 -1
  69. package/dist/rpc/interceptor/index.js.map +0 -1
  70. package/dist/rpc/interceptor/rpcInterceptor.type.d.ts.map +0 -1
  71. package/dist/rpc/interceptor/rpcInterceptor.type.js.map +0 -1
  72. package/dist/rpc/middleware/index.d.ts.map +0 -1
  73. package/dist/rpc/middleware/index.js.map +0 -1
  74. package/dist/rpc/middleware/rpcMiddleware.core.d.ts.map +0 -1
  75. package/dist/rpc/middleware/rpcMiddleware.core.js.map +0 -1
  76. package/dist/rpc/procedure/index.d.ts.map +0 -1
  77. package/dist/rpc/procedure/index.js.map +0 -1
  78. package/dist/rpc/procedure/rpcProcedure.type.d.ts.map +0 -1
  79. package/dist/rpc/procedure/rpcProcedure.type.js.map +0 -1
  80. package/dist/rpc/procedure/rpcProcedureRegistry.core.d.ts.map +0 -1
  81. package/dist/rpc/procedure/rpcProcedureRegistry.core.js.map +0 -1
  82. package/dist/rpc/procedure/rpcProcedureRouter.core.d.ts.map +0 -1
  83. package/dist/rpc/procedure/rpcProcedureRouter.core.js.map +0 -1
  84. package/dist/rpc/reliability/cancellation/rpcCancellation.helper.d.ts.map +0 -1
  85. package/dist/rpc/reliability/cancellation/rpcCancellation.helper.js.map +0 -1
  86. package/dist/rpc/reliability/deadline/rpcDeadline.helper.d.ts.map +0 -1
  87. package/dist/rpc/reliability/deadline/rpcDeadline.helper.js.map +0 -1
  88. package/dist/rpc/reliability/index.d.ts.map +0 -1
  89. package/dist/rpc/reliability/index.js.map +0 -1
  90. package/dist/rpc/reliability/retry/rpcRetry.helper.d.ts.map +0 -1
  91. package/dist/rpc/reliability/retry/rpcRetry.helper.js.map +0 -1
  92. package/dist/rpc/reliability/timeout/rpcTimeout.helper.d.ts.map +0 -1
  93. package/dist/rpc/reliability/timeout/rpcTimeout.helper.js.map +0 -1
  94. package/dist/rpc/server/index.d.ts.map +0 -1
  95. package/dist/rpc/server/index.js.map +0 -1
  96. package/dist/rpc/server/rpcServer.core.d.ts.map +0 -1
  97. package/dist/rpc/server/rpcServer.core.js.map +0 -1
  98. package/dist/rpc/streaming/index.d.ts.map +0 -1
  99. package/dist/rpc/streaming/index.js.map +0 -1
  100. package/dist/rpc/streaming/rpcStreaming.type.d.ts.map +0 -1
  101. package/dist/rpc/streaming/rpcStreaming.type.js.map +0 -1
  102. package/dist/rpc/transport/index.d.ts.map +0 -1
  103. package/dist/rpc/transport/index.js.map +0 -1
  104. package/dist/rpc/transport/rpcTransport.type.d.ts.map +0 -1
  105. package/dist/rpc/transport/rpcTransport.type.js.map +0 -1
  106. package/dist/rpc/types/index.d.ts.map +0 -1
  107. package/dist/rpc/types/index.js.map +0 -1
  108. package/dist/rpc/types/rpcMetadata.type.d.ts.map +0 -1
  109. package/dist/rpc/types/rpcMetadata.type.js.map +0 -1
  110. package/dist/rpc/types/rpcProcedureName.type.d.ts.map +0 -1
  111. package/dist/rpc/types/rpcProcedureName.type.js.map +0 -1
  112. package/dist/rpc/types/rpcRequest.type.d.ts.map +0 -1
  113. package/dist/rpc/types/rpcRequest.type.js.map +0 -1
  114. package/dist/rpc/types/rpcResponse.type.d.ts.map +0 -1
  115. package/dist/rpc/types/rpcResponse.type.js.map +0 -1
  116. package/dist/testing/index.d.ts.map +0 -1
  117. package/dist/testing/index.js.map +0 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zudojs Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @zudojs/rpc
2
2
 
3
- Type-safe RPC — define procedures, apply middleware, dispatch calls, and serve over HTTP or your own transport.
3
+ Type-safe RPC — define procedures, apply middleware, dispatch calls, and serve them over your own transport.
4
4
 
5
5
  ## When to use
6
6
 
@@ -8,9 +8,14 @@ Import this when you need:
8
8
 
9
9
  - a typed RPC layer between services (gateway ↔ microservice, frontend ↔ backend)
10
10
  - procedure-level middleware (auth, tracing, rate limit)
11
- - a dispatcher that picks the right transport
11
+ - a server that validates frames and maps every failure to a wire code
12
12
  - structured RPC errors
13
13
 
14
+ The package is transport-agnostic: `RPCServer.handle` takes a request frame
15
+ and returns a response frame, and `RPCClient` sends through any object
16
+ implementing `RPCTransport`. It ships no HTTP or WebSocket transport of its
17
+ own.
18
+
14
19
  For request/response inside one process, prefer `@zudojs/api`.
15
20
 
16
21
  ## Installation
@@ -24,8 +29,11 @@ npm install @zudojs/rpc
24
29
  ```typescript
25
30
  import {
26
31
  createRPCProcedure,
27
- RPCDispatcher,
32
+ createRPCRequest,
28
33
  RPCServer,
34
+ RPCClient,
35
+ RPCDispatcher,
36
+ RPCProcedureRegistry,
29
37
  RPCMiddlewareStack,
30
38
  createRPCContext,
31
39
  type RPCContext,
@@ -33,24 +41,108 @@ import {
33
41
  type RPCProcedure,
34
42
  type RPCRequest,
35
43
  type RPCResponse,
44
+ type RPCTransport,
36
45
  type RPCErrorOptions,
37
46
  } from "@zudojs/rpc";
38
47
  ```
39
48
 
40
49
  ## Usage
41
50
 
51
+ A procedure is `createRPCProcedure(name, handler, options?)`. Names are
52
+ dot-separated identifiers (`"math.sum"`); the handler is positional,
53
+ `(input, context)`; `options.input` / `options.output` are `@zudojs/schema`
54
+ schemas (anything with `safeParse`) the payload and result are checked
55
+ against.
56
+
42
57
  ```typescript
43
- import { createRPCProcedure, RPCDispatcher } from "@zudojs/rpc";
58
+ import { createRPCProcedure, createRPCRequest, RPCServer } from "@zudojs/rpc";
59
+ import { schema } from "@zudojs/schema";
44
60
 
45
- const sum = createRPCProcedure({
46
- name: "sum",
47
- input: { a: "number", b: "number" },
48
- handler: ({ input }) => input.a + input.b,
49
- });
61
+ const sum = createRPCProcedure(
62
+ "math.sum",
63
+ async (input: { a: number; b: number }) => input.a + input.b,
64
+ { input: schema.object({ a: schema.number(), b: schema.number() }) },
65
+ );
66
+
67
+ const server = new RPCServer();
68
+ server.register(sum);
69
+
70
+ const response = await server.handle(
71
+ createRPCRequest({ id: "req-1", procedure: "math.sum", payload: { a: 1, b: 2 } }),
72
+ );
73
+ // { id: "req-1", success: true, result: 3 }
74
+ ```
50
75
 
51
- const dispatcher = new RPCDispatcher();
52
- dispatcher.register(sum);
53
- const result = await dispatcher.call("sum", { a: 1, b: 2 });
76
+ `handle` never throws for a bad request: a malformed frame, an unknown
77
+ procedure, a payload the input schema rejects, a timeout or a handler error
78
+ each come back as `{ success: false, error: { code, message, details? } }`.
79
+ The frame's `metadata` is optional.
80
+
81
+ ### Calling through a transport
82
+
83
+ `RPCClient` needs an `RPCTransport` — an object whose `send(request, options)`
84
+ delivers the frame and resolves with the response. `options.signal` aborts the
85
+ call and `options.timeout` is the deadline in milliseconds, so a transport can
86
+ set its own socket timeout. The in-process transport below is the smallest
87
+ possible one:
88
+
89
+ ```typescript
90
+ import { RPCClient, type RPCTransport } from "@zudojs/rpc";
91
+
92
+ const transport: RPCTransport = { send: (request) => server.handle(request) };
93
+ const client = new RPCClient(transport, { timeout: 5_000 });
94
+
95
+ const total = await client.call<{ a: number; b: number }, number>("math.sum", { a: 1, b: 2 });
96
+ // 3
97
+ ```
98
+
99
+ A failed call rejects with a typed error rebuilt from the wire code
100
+ (`RPCTimeoutError`, `RPCCancelledError`, `RPCUnavailableError`, or an
101
+ `RPCError` carrying the server's `code` and `details`).
102
+
103
+ ### Middleware
104
+
105
+ ```typescript
106
+ import { RPCMiddlewareStack, RPCAuthenticationError } from "@zudojs/rpc";
107
+
108
+ const stack = new RPCMiddlewareStack([
109
+ async (context, next) => {
110
+ if (context.metadata.userId === undefined) {
111
+ throw new RPCAuthenticationError("Sign in first.");
112
+ }
113
+ return next();
114
+ },
115
+ ]);
116
+
117
+ const server = new RPCServer(undefined, stack);
118
+ ```
119
+
120
+ Each middleware may call `next()` once. Input validation runs before the
121
+ stack, so middleware sees a payload the procedure's schema has accepted.
122
+
123
+ ## Errors
124
+
125
+ Every error class from `@zudojs/errors`' RPC family is re-exported. The
126
+ server maps them to wire codes (`RPC_PROCEDURE_NOT_FOUND`,
127
+ `RPC_VALIDATION_ERROR`, `RPC_UNAUTHENTICATED`, `RPC_FORBIDDEN`,
128
+ `RPC_RATE_LIMITED`, `RPC_TIMEOUT`, …). A custom `RPCError` subclass keeps its
129
+ own `code`.
130
+
131
+ What reaches the caller follows the error's `expose` flag. Anything thrown
132
+ with `expose: false` — an `RPCInternalError`, an `RPCSerializationError`, a
133
+ plain `new RPCError(...)` (whose default is `expose: false`), or any
134
+ non-RPC error — is answered with the fixed `INTERNAL_ERROR_MESSAGE`; the
135
+ original error is handed to `onInternalError(error, requestId)` so it can be
136
+ logged against the request id. A handler result that fails the procedure's
137
+ `output` schema is treated the same way: it is the server's fault, not the
138
+ caller's.
139
+
140
+ ```typescript
141
+ const server = new RPCServer(undefined, undefined, {
142
+ limits: { maxPayloadBytes: 256 * 1024 },
143
+ dispatch: { defaultTimeout: 10_000 },
144
+ onInternalError: (error, requestId) => logger.error({ requestId, error }),
145
+ });
54
146
  ```
55
147
 
56
148
  ## License
package/dist/index.d.ts CHANGED
@@ -21,7 +21,9 @@
21
21
  */
22
22
  export type { RPCProcedureName, RPCMetadata, RPCMetadataOptions, RPCRequest, RPCRequestOptions, RPCErrorPayload, RPCResponse, } from "./rpc/types/index.js";
23
23
  export { createRPCMetadata, createRPCRequest, createRPCResponse, createRPCErrorResponse, } from "./rpc/types/index.js";
24
- export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, PROCEDURE_NAME_PATTERN, } from "./rpc/constants/index.js";
24
+ export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, MAX_PROCEDURE_NAME_LENGTH, MAX_TIMER_DELAY, PROCEDURE_NAME_PATTERN, INTERNAL_ERROR_MESSAGE, } from "./rpc/constants/index.js";
25
+ export type { RPCRequestLimits, RPCSchema } from "./rpc/validation/index.js";
26
+ export { assertValidProcedureName, assertValidRequest, measurePayloadBytes, toValidationIssues, parseInput, parseOutput, } from "./rpc/validation/index.js";
25
27
  export type { RPCErrorOptions } from "./rpc/errors/index.js";
26
28
  export { RPCError, RPCProcedureNotFoundError, RPCInvalidRequestError, RPCValidationError, RPCAuthenticationError, RPCForbiddenError, RPCTimeoutError, RPCCancelledError, RPCInternalError, RPCTransportError, RPCSerializationError, RPCDeserializationError, RPCUnavailableError, RPCRateLimitedError, RPCDeadlineExceededError, RPCDuplicateProcedureError, createRPCError, isRPCError, } from "./rpc/errors/index.js";
27
29
  export type { RPCHandler, RPCProcedure, RPCProcedureOptions, } from "./rpc/procedure/index.js";
@@ -31,13 +33,15 @@ export type { RPCContext } from "./rpc/context/index.js";
31
33
  export { createRPCContext } from "./rpc/context/index.js";
32
34
  export type { RPCMiddleware } from "./rpc/middleware/index.js";
33
35
  export { RPCMiddlewareStack } from "./rpc/middleware/index.js";
36
+ export type { RPCDispatcherOptions } from "./rpc/dispatcher/index.js";
34
37
  export { RPCDispatcher } from "./rpc/dispatcher/index.js";
38
+ export type { RPCServerOptions } from "./rpc/server/index.js";
35
39
  export { RPCServer } from "./rpc/server/index.js";
36
40
  export type { RPCTransport, RPCTransportRequestOptions, } from "./rpc/transport/index.js";
37
- export type { RPCCallOptions } from "./rpc/client/index.js";
41
+ export type { RPCCallOptions, RPCClientOptions } from "./rpc/client/index.js";
38
42
  export { RPCClient } from "./rpc/client/index.js";
39
- export { createTimeout, withTimeout, getRemainingTime, isDeadlineExceeded, throwIfDeadlineExceeded, createCancellableSignal, cancelSignal, DEFAULT_RETRY_OPTIONS, calculateRetryDelay, retry, } from "./rpc/reliability/index.js";
40
- export type { RPCBackoff, RPCRetryOptions } from "./rpc/reliability/index.js";
43
+ export { createTimeout, withTimeout, runWithTimeout, getRemainingTime, isDeadlineExceeded, throwIfDeadlineExceeded, readDeadline, createCancellableSignal, cancelSignal, throwIfCancelled, combineSignals, DEFAULT_RETRY_OPTIONS, calculateRetryDelay, retry, } from "./rpc/reliability/index.js";
44
+ export type { RPCBackoff, RPCJitter, RPCRetryOptions, CancellableSignal, } from "./rpc/reliability/index.js";
41
45
  export type { RPCInterceptor } from "./rpc/interceptor/index.js";
42
46
  export { createNoopRPCInterceptor } from "./rpc/interceptor/index.js";
43
47
  export type { RPCStreamingHandler, RPCStreamingProcedure, } from "./rpc/streaming/index.js";
package/dist/index.js CHANGED
@@ -21,19 +21,18 @@
21
21
  */
22
22
  export { createRPCMetadata, createRPCRequest, createRPCResponse, createRPCErrorResponse, } from "./rpc/types/index.js";
23
23
  // Constants
24
- export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, PROCEDURE_NAME_PATTERN, } from "./rpc/constants/index.js";
24
+ export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, MAX_PROCEDURE_NAME_LENGTH, MAX_TIMER_DELAY, PROCEDURE_NAME_PATTERN, INTERNAL_ERROR_MESSAGE, } from "./rpc/constants/index.js";
25
+ export { assertValidProcedureName, assertValidRequest, measurePayloadBytes, toValidationIssues, parseInput, parseOutput, } from "./rpc/validation/index.js";
25
26
  export { RPCError, RPCProcedureNotFoundError, RPCInvalidRequestError, RPCValidationError, RPCAuthenticationError, RPCForbiddenError, RPCTimeoutError, RPCCancelledError, RPCInternalError, RPCTransportError, RPCSerializationError, RPCDeserializationError, RPCUnavailableError, RPCRateLimitedError, RPCDeadlineExceededError, RPCDuplicateProcedureError, createRPCError, isRPCError, } from "./rpc/errors/index.js";
26
27
  export { createRPCProcedure } from "./rpc/procedure/index.js";
27
28
  export { RPCProcedureRegistry, RPCProcedureRouter, } from "./rpc/procedure/index.js";
28
29
  export { createRPCContext } from "./rpc/context/index.js";
29
30
  export { RPCMiddlewareStack } from "./rpc/middleware/index.js";
30
- // Dispatcher
31
31
  export { RPCDispatcher } from "./rpc/dispatcher/index.js";
32
- // Server
33
32
  export { RPCServer } from "./rpc/server/index.js";
34
33
  export { RPCClient } from "./rpc/client/index.js";
35
34
  // Reliability
36
- export { createTimeout, withTimeout, getRemainingTime, isDeadlineExceeded, throwIfDeadlineExceeded, createCancellableSignal, cancelSignal, DEFAULT_RETRY_OPTIONS, calculateRetryDelay, retry, } from "./rpc/reliability/index.js";
35
+ export { createTimeout, withTimeout, runWithTimeout, getRemainingTime, isDeadlineExceeded, throwIfDeadlineExceeded, readDeadline, createCancellableSignal, cancelSignal, throwIfCancelled, combineSignals, DEFAULT_RETRY_OPTIONS, calculateRetryDelay, retry, } from "./rpc/reliability/index.js";
37
36
  export { createNoopRPCInterceptor } from "./rpc/interceptor/index.js";
38
37
  export { createRPCStreamingProcedure } from "./rpc/streaming/index.js";
39
38
  //# sourceMappingURL=index.js.map
@@ -1,3 +1,3 @@
1
- export type { RPCCallOptions } from "./rpcClient.core.js";
1
+ export type { RPCCallOptions, RPCClientOptions } from "./rpcClient.core.js";
2
2
  export { RPCClient } from "./rpcClient.core.js";
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -4,23 +4,77 @@ import type { RPCTransport } from "../transport/rpcTransport.type.js";
4
4
  */
5
5
  export interface RPCCallOptions {
6
6
  readonly timeout?: number;
7
+ /** Cancels the call. An already-aborted signal fails immediately. */
7
8
  readonly signal?: AbortSignal;
8
9
  readonly metadata?: Record<string, string | number | boolean | undefined>;
9
10
  }
11
+ /**
12
+ * Options for constructing an RPC client.
13
+ */
14
+ export interface RPCClientOptions {
15
+ /** Default timeout for calls that do not specify one. */
16
+ readonly timeout?: number;
17
+ /**
18
+ * Maximum number of calls in flight at once. Defaults to
19
+ * {@link MAX_PENDING_REQUESTS}. Additional calls fail fast rather than
20
+ * queueing without bound.
21
+ */
22
+ readonly maxPending?: number;
23
+ }
10
24
  /**
11
25
  * RPC client for invoking remote procedures.
26
+ *
27
+ * Every call is tracked while it is genuinely in flight and removed the
28
+ * moment it settles, so the concurrency limit reflects real in-flight
29
+ * work rather than accumulating entries from calls that already
30
+ * succeeded.
12
31
  */
13
32
  export declare class RPCClient {
14
33
  private readonly transport;
34
+ private readonly options;
15
35
  private readonly pending;
16
- constructor(transport: RPCTransport);
36
+ private closed;
37
+ constructor(transport: RPCTransport, options?: RPCClientOptions);
38
+ /**
39
+ * Number of calls currently in flight.
40
+ */
41
+ get pendingCount(): number;
17
42
  /**
18
43
  * Calls a remote procedure.
19
44
  */
20
45
  call<TInput = unknown, TOutput = unknown>(procedure: string, input: TInput, options?: RPCCallOptions): Promise<TOutput>;
21
46
  /**
22
- * Creates a timeout promise for an RPC call.
47
+ * Cancels every in-flight call and stops accepting new ones.
48
+ */
49
+ close(reason?: string): Promise<void>;
50
+ /**
51
+ * Describes the calls currently in flight, for diagnostics.
52
+ */
53
+ inspectPending(): readonly {
54
+ procedure: string;
55
+ elapsedMs: number;
56
+ }[];
57
+ /**
58
+ * A promise that rejects when the signal aborts.
59
+ */
60
+ private abortPromise;
61
+ private abortReason;
62
+ private cancellationError;
63
+ /**
64
+ * Normalises an abort reason into an `RPCCancelledError`.
65
+ *
66
+ * A bare `controller.abort()` yields a `DOMException`, so callers that
67
+ * branch on error type would never see a cancellation. The original
68
+ * reason is preserved as `cause`; a reason that is already an RPC
69
+ * error is passed through untouched.
70
+ */
71
+ private toCancellation;
72
+ /**
73
+ * Reconstructs a typed error from an error response.
74
+ *
75
+ * The wire code drives the type, so a caller can tell an
76
+ * authentication failure from a timeout without string matching.
23
77
  */
24
- private createTimeout;
78
+ private toError;
25
79
  }
26
80
  //# sourceMappingURL=rpcClient.core.d.ts.map
@@ -1,60 +1,217 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { createRPCRequest } from "../types/rpcRequest.type.js";
2
- import { RPCProcedureNotFoundError } from "../errors/rpc.errors.js";
3
+ import { isRPCError, RPCCancelledError, RPCError, RPCTimeoutError, RPCUnavailableError, } from "../errors/rpc.errors.js";
3
4
  import { DEFAULT_RPC_TIMEOUT, MAX_PENDING_REQUESTS, } from "../constants/rpcConstants.core.js";
5
+ import { createTimeout } from "../reliability/timeout/rpcTimeout.helper.js";
4
6
  /**
5
7
  * RPC client for invoking remote procedures.
8
+ *
9
+ * Every call is tracked while it is genuinely in flight and removed the
10
+ * moment it settles, so the concurrency limit reflects real in-flight
11
+ * work rather than accumulating entries from calls that already
12
+ * succeeded.
6
13
  */
7
14
  export class RPCClient {
8
15
  transport;
16
+ options;
9
17
  pending = new Map();
10
- constructor(transport) {
18
+ closed = false;
19
+ constructor(transport, options = {}) {
11
20
  this.transport = transport;
21
+ this.options = options;
22
+ }
23
+ /**
24
+ * Number of calls currently in flight.
25
+ */
26
+ get pendingCount() {
27
+ return this.pending.size;
12
28
  }
13
29
  /**
14
30
  * Calls a remote procedure.
15
31
  */
16
32
  async call(procedure, input, options = {}) {
17
- const id = crypto.randomUUID();
18
- const timeout = options.timeout ?? DEFAULT_RPC_TIMEOUT;
19
- if (this.pending.size >= MAX_PENDING_REQUESTS) {
20
- throw new Error("Too many pending RPC requests.");
33
+ if (this.closed) {
34
+ throw new RPCUnavailableError("RPC client has been closed.", procedure);
35
+ }
36
+ const maxPending = this.options.maxPending ?? MAX_PENDING_REQUESTS;
37
+ if (this.pending.size >= maxPending) {
38
+ throw new RPCUnavailableError(`Too many RPC requests in flight (${this.pending.size}/${maxPending}).`, procedure);
21
39
  }
22
- const response = await Promise.race([
23
- this.transport.send(createRPCRequest({
40
+ if (options.signal?.aborted) {
41
+ throw this.cancellationError(options.signal, procedure);
42
+ }
43
+ const id = randomUUID();
44
+ const timeoutMs = options.timeout ?? this.options.timeout ?? DEFAULT_RPC_TIMEOUT;
45
+ const controller = new AbortController();
46
+ const timeout = timeoutMs > 0 ? createTimeout(timeoutMs, procedure) : undefined;
47
+ const onCallerAbort = () => {
48
+ controller.abort(this.cancellationError(options.signal, procedure));
49
+ };
50
+ options.signal?.addEventListener("abort", onCallerAbort, { once: true });
51
+ this.pending.set(id, {
52
+ procedure,
53
+ startedAt: Date.now(),
54
+ cancel: (reason) => {
55
+ if (!controller.signal.aborted) {
56
+ controller.abort(reason);
57
+ }
58
+ },
59
+ });
60
+ try {
61
+ const request = createRPCRequest({
24
62
  id,
25
63
  procedure,
26
64
  payload: input,
27
65
  metadata: options.metadata,
28
- })),
29
- this.createTimeout(id, timeout),
30
- ]);
31
- if (!response.success) {
32
- const error = new Error(response.error?.message ?? "RPC call failed.");
33
- error.code = response.error?.code;
34
- throw error;
66
+ });
67
+ // The transport is told the deadline as well as given the signal:
68
+ // `RPCTransportRequestOptions.timeout` exists so a transport can set
69
+ // its own socket or request timeout, and it was never populated.
70
+ const races = [
71
+ this.transport.send(request, {
72
+ signal: controller.signal,
73
+ ...(timeoutMs > 0 ? { timeout: timeoutMs } : {}),
74
+ }),
75
+ this.abortPromise(controller.signal, procedure),
76
+ ];
77
+ if (timeout) {
78
+ races.push(timeout.promise.catch((error) => {
79
+ if (!controller.signal.aborted) {
80
+ controller.abort(error);
81
+ }
82
+ throw error;
83
+ }));
84
+ }
85
+ const response = await Promise.race(races);
86
+ if (!response.success) {
87
+ throw this.toError(response, procedure);
88
+ }
89
+ return response.result;
90
+ }
91
+ finally {
92
+ // Removing the entry here — rather than leaving it for a timer to
93
+ // reap — is what keeps `pendingCount` equal to the number of calls
94
+ // actually in flight.
95
+ this.pending.delete(id);
96
+ timeout?.cancel();
97
+ options.signal?.removeEventListener("abort", onCallerAbort);
98
+ if (!controller.signal.aborted) {
99
+ controller.abort(new RPCCancelledError("Call settled.", procedure));
100
+ }
35
101
  }
36
- return response.result;
37
102
  }
38
103
  /**
39
- * Creates a timeout promise for an RPC call.
104
+ * Cancels every in-flight call and stops accepting new ones.
105
+ */
106
+ async close(reason) {
107
+ this.closed = true;
108
+ const error = new RPCCancelledError(reason ?? "RPC client closed.");
109
+ for (const call of this.pending.values()) {
110
+ call.cancel(error);
111
+ }
112
+ this.pending.clear();
113
+ await this.transport.close?.();
114
+ }
115
+ /**
116
+ * Describes the calls currently in flight, for diagnostics.
117
+ */
118
+ inspectPending() {
119
+ const now = Date.now();
120
+ return [...this.pending.values()].map((call) => ({
121
+ procedure: call.procedure,
122
+ elapsedMs: now - call.startedAt,
123
+ }));
124
+ }
125
+ /**
126
+ * A promise that rejects when the signal aborts.
40
127
  */
41
- createTimeout(id, timeout) {
42
- return new Promise((resolve, reject) => {
43
- const timer = setTimeout(() => {
44
- this.pending.delete(id);
45
- reject(new Error(`RPC call timed out after ${timeout}ms.`));
46
- }, timeout);
47
- this.pending.set(id, {
48
- resolve: (response) => {
49
- clearTimeout(timer);
50
- resolve(response);
51
- },
52
- reject: (error) => {
53
- clearTimeout(timer);
54
- reject(error);
55
- },
128
+ abortPromise(signal, procedure) {
129
+ return new Promise((_, reject) => {
130
+ if (signal.aborted) {
131
+ reject(this.abortReason(signal, procedure));
132
+ return;
133
+ }
134
+ signal.addEventListener("abort", () => reject(this.abortReason(signal, procedure)), { once: true });
135
+ });
136
+ }
137
+ abortReason(signal, procedure) {
138
+ return this.toCancellation(signal.reason, procedure);
139
+ }
140
+ cancellationError(signal, procedure) {
141
+ return this.toCancellation(signal?.reason, procedure);
142
+ }
143
+ /**
144
+ * Normalises an abort reason into an `RPCCancelledError`.
145
+ *
146
+ * A bare `controller.abort()` yields a `DOMException`, so callers that
147
+ * branch on error type would never see a cancellation. The original
148
+ * reason is preserved as `cause`; a reason that is already an RPC
149
+ * error is passed through untouched.
150
+ */
151
+ toCancellation(reason, procedure) {
152
+ if (isRPCError(reason)) {
153
+ return reason;
154
+ }
155
+ const message = reason instanceof Error && reason.name !== "AbortError"
156
+ ? reason.message
157
+ : "Call cancelled by caller.";
158
+ const error = new RPCCancelledError(message, procedure);
159
+ if (reason !== undefined) {
160
+ Object.defineProperty(error, "cause", {
161
+ value: reason,
162
+ enumerable: false,
163
+ configurable: true,
164
+ writable: true,
56
165
  });
166
+ }
167
+ return error;
168
+ }
169
+ /**
170
+ * Reconstructs a typed error from an error response.
171
+ *
172
+ * The wire code drives the type, so a caller can tell an
173
+ * authentication failure from a timeout without string matching.
174
+ */
175
+ toError(response, procedure) {
176
+ const message = response.error?.message ?? "RPC call failed.";
177
+ const code = response.error?.code;
178
+ switch (code) {
179
+ case "RPC_TIMEOUT": {
180
+ // The constructor derives its message from a duration the wire
181
+ // does not carry; keep the type and restore the server's message
182
+ // rather than reporting "timed out after 0ms".
183
+ const timeout = new RPCTimeoutError(0, procedure);
184
+ Object.defineProperty(timeout, "message", {
185
+ value: message,
186
+ enumerable: false,
187
+ configurable: true,
188
+ writable: true,
189
+ });
190
+ return timeout;
191
+ }
192
+ case "RPC_CANCELLED":
193
+ return new RPCCancelledError(message, procedure);
194
+ case "RPC_UNAVAILABLE":
195
+ return new RPCUnavailableError(message, procedure);
196
+ default:
197
+ break;
198
+ }
199
+ const error = new RPCError(message, { procedureName: procedure });
200
+ // Preserve the server's code and any details for callers that
201
+ // branch on them.
202
+ Object.defineProperty(error, "code", {
203
+ value: code ?? error.code,
204
+ enumerable: true,
205
+ configurable: true,
57
206
  });
207
+ if (response.error?.details !== undefined) {
208
+ Object.defineProperty(error, "details", {
209
+ value: response.error.details,
210
+ enumerable: true,
211
+ configurable: true,
212
+ });
213
+ }
214
+ return error;
58
215
  }
59
216
  }
60
217
  //# sourceMappingURL=rpcClient.core.js.map
@@ -1,2 +1,2 @@
1
- export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, PROCEDURE_NAME_PATTERN, } from "./rpcConstants.core.js";
1
+ export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, MAX_PROCEDURE_NAME_LENGTH, MAX_TIMER_DELAY, PROCEDURE_NAME_PATTERN, INTERNAL_ERROR_MESSAGE, } from "./rpcConstants.core.js";
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1,2 @@
1
- export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, PROCEDURE_NAME_PATTERN, } from "./rpcConstants.core.js";
1
+ export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, MAX_PROCEDURE_NAME_LENGTH, MAX_TIMER_DELAY, PROCEDURE_NAME_PATTERN, INTERNAL_ERROR_MESSAGE, } from "./rpcConstants.core.js";
2
2
  //# sourceMappingURL=index.js.map
@@ -27,4 +27,22 @@ export declare const MAX_PROCEDURES = 4096;
27
27
  * Procedure name pattern: dot-separated lowercase identifiers.
28
28
  */
29
29
  export declare const PROCEDURE_NAME_PATTERN: RegExp;
30
+ /**
31
+ * Maximum length of a procedure name, checked before the pattern so a
32
+ * pathological name cannot drive regex backtracking.
33
+ */
34
+ export declare const MAX_PROCEDURE_NAME_LENGTH = 256;
35
+ /**
36
+ * Largest delay Node's timer subsystem accepts. A larger delay overflows
37
+ * a signed 32-bit integer and is silently clamped to `1`.
38
+ */
39
+ export declare const MAX_TIMER_DELAY = 2147483647;
40
+ /**
41
+ * Message returned to callers for an unexpected server-side failure.
42
+ *
43
+ * Internal exception text may name hosts, paths, credentials or queries,
44
+ * so it is logged rather than returned. Callers correlate with the
45
+ * request id instead.
46
+ */
47
+ export declare const INTERNAL_ERROR_MESSAGE = "The server encountered an internal error while handling this request.";
30
48
  //# sourceMappingURL=rpcConstants.core.d.ts.map
@@ -27,4 +27,22 @@ export const MAX_PROCEDURES = 4096;
27
27
  * Procedure name pattern: dot-separated lowercase identifiers.
28
28
  */
29
29
  export const PROCEDURE_NAME_PATTERN = /^[a-z][a-zA-Z0-9]*(\.[a-z][a-zA-Z0-9]*)+$/;
30
+ /**
31
+ * Maximum length of a procedure name, checked before the pattern so a
32
+ * pathological name cannot drive regex backtracking.
33
+ */
34
+ export const MAX_PROCEDURE_NAME_LENGTH = 256;
35
+ /**
36
+ * Largest delay Node's timer subsystem accepts. A larger delay overflows
37
+ * a signed 32-bit integer and is silently clamped to `1`.
38
+ */
39
+ export const MAX_TIMER_DELAY = 2_147_483_647;
40
+ /**
41
+ * Message returned to callers for an unexpected server-side failure.
42
+ *
43
+ * Internal exception text may name hosts, paths, credentials or queries,
44
+ * so it is logged rather than returned. Callers correlate with the
45
+ * request id instead.
46
+ */
47
+ export const INTERNAL_ERROR_MESSAGE = "The server encountered an internal error while handling this request.";
30
48
  //# sourceMappingURL=rpcConstants.core.js.map
@@ -5,7 +5,9 @@ export function createRPCContext(request, signal) {
5
5
  const state = new Map();
6
6
  const context = {
7
7
  request,
8
- metadata: request.metadata,
8
+ // A frame decoded from JSON may omit `metadata`; middleware reads
9
+ // `context.metadata.userId` and the like without guarding.
10
+ metadata: request.metadata ?? {},
9
11
  signal,
10
12
  state,
11
13
  get(key) {
@@ -1,2 +1,3 @@
1
+ export type { RPCDispatcherOptions } from "./rpcDispatcher.core.js";
1
2
  export { RPCDispatcher } from "./rpcDispatcher.core.js";
2
3
  //# sourceMappingURL=index.d.ts.map