@zudojs/rpc 1.3.0 → 1.4.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 (55) hide show
  1. package/README.md +120 -17
  2. package/dist/index.d.ts +12 -8
  3. package/dist/index.js +14 -5
  4. package/dist/rpc/client/index.d.ts +5 -0
  5. package/dist/rpc/client/index.js +5 -0
  6. package/dist/rpc/client/rpcClient.core.d.ts +6 -7
  7. package/dist/rpc/client/rpcClient.core.js +32 -46
  8. package/dist/rpc/client/rpcWireError.helper.d.ts +13 -0
  9. package/dist/rpc/client/rpcWireError.helper.js +71 -0
  10. package/dist/rpc/constants/index.d.ts +6 -1
  11. package/dist/rpc/constants/index.js +6 -1
  12. package/dist/rpc/constants/rpcConstants.core.d.ts +11 -0
  13. package/dist/rpc/constants/rpcConstants.core.js +11 -0
  14. package/dist/rpc/context/rpcContext.type.d.ts +7 -0
  15. package/dist/rpc/dispatcher/rpcDispatcher.core.js +14 -1
  16. package/dist/rpc/reliability/cancellation/rpcAbort.helper.d.ts +17 -0
  17. package/dist/rpc/reliability/cancellation/rpcAbort.helper.js +40 -0
  18. package/dist/rpc/reliability/retry/rpcRetry.helper.js +3 -1
  19. package/dist/rpc/reliability/timeout/rpcTimeout.helper.d.ts +5 -3
  20. package/dist/rpc/reliability/timeout/rpcTimeout.helper.js +5 -4
  21. package/dist/rpc/server/index.d.ts +6 -0
  22. package/dist/rpc/server/index.js +5 -0
  23. package/dist/rpc/server/rpcBaseErrorMapping.helper.d.ts +16 -0
  24. package/dist/rpc/server/rpcBaseErrorMapping.helper.js +57 -0
  25. package/dist/rpc/server/rpcErrorMapping.helper.d.ts +35 -0
  26. package/dist/rpc/server/rpcErrorMapping.helper.js +101 -0
  27. package/dist/rpc/server/rpcServer.core.d.ts +0 -7
  28. package/dist/rpc/server/rpcServer.core.js +7 -91
  29. package/dist/rpc/transport/codec/index.d.ts +10 -0
  30. package/dist/rpc/transport/codec/index.js +8 -0
  31. package/dist/rpc/transport/codec/rpcBody.helper.d.ts +36 -0
  32. package/dist/rpc/transport/codec/rpcBody.helper.js +64 -0
  33. package/dist/rpc/transport/codec/rpcCodec.helper.d.ts +41 -0
  34. package/dist/rpc/transport/codec/rpcCodec.helper.js +40 -0
  35. package/dist/rpc/transport/http/index.d.ts +12 -0
  36. package/dist/rpc/transport/http/index.js +10 -0
  37. package/dist/rpc/transport/http/rpcFetchHandler.core.d.ts +36 -0
  38. package/dist/rpc/transport/http/rpcFetchHandler.core.js +97 -0
  39. package/dist/rpc/transport/http/rpcHttpExchange.helper.d.ts +27 -0
  40. package/dist/rpc/transport/http/rpcHttpExchange.helper.js +78 -0
  41. package/dist/rpc/transport/http/rpcHttpStatus.helper.d.ts +16 -0
  42. package/dist/rpc/transport/http/rpcHttpStatus.helper.js +37 -0
  43. package/dist/rpc/transport/http/rpcHttpTransport.core.d.ts +36 -0
  44. package/dist/rpc/transport/http/rpcHttpTransport.core.js +45 -0
  45. package/dist/rpc/transport/index.d.ts +12 -0
  46. package/dist/rpc/transport/index.js +9 -1
  47. package/dist/rpc/transport/memory/index.d.ts +7 -0
  48. package/dist/rpc/transport/memory/index.js +6 -0
  49. package/dist/rpc/transport/memory/rpcMemoryTransport.core.d.ts +42 -0
  50. package/dist/rpc/transport/memory/rpcMemoryTransport.core.js +73 -0
  51. package/dist/rpc/validation/rpcUnsafeKey.helper.d.ts +18 -0
  52. package/dist/rpc/validation/rpcUnsafeKey.helper.js +60 -0
  53. package/dist/rpc/validation/rpcValidation.core.d.ts +11 -2
  54. package/dist/rpc/validation/rpcValidation.core.js +11 -2
  55. package/package.json +9 -7
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 them over your own transport.
3
+ Type-safe RPC — define procedures, apply middleware, dispatch calls, and serve them in-process or over HTTP with the built-in transports, or over your own.
4
4
 
5
5
  <!-- zudo-docs:start -->
6
6
 
@@ -19,10 +19,13 @@ Import this when you need:
19
19
 
20
20
  The package is transport-agnostic: `RPCServer.handle` takes a request frame
21
21
  and returns a response frame, and `RPCClient` sends through any object
22
- implementing `RPCTransport`. It ships no HTTP or WebSocket transport of its
23
- own.
22
+ implementing `RPCTransport`. Two transports ship with it: an in-memory one
23
+ (`createRPCMemoryTransport`) and HTTP on the web-standard Fetch API
24
+ (`createRPCHttpTransport` for the client, `createRPCFetchHandler` for the
25
+ server). There is no WebSocket transport.
24
26
 
25
- For request/response inside one process, prefer `@zudojs/api`.
27
+ To expose `@zudojs/api` operations as procedures, use
28
+ `registerApiRpcProcedures` from `@zudojs/api`.
26
29
 
27
30
  ## Installation
28
31
 
@@ -49,6 +52,20 @@ import {
49
52
  type RPCResponse,
50
53
  type RPCTransport,
51
54
  type RPCErrorOptions,
55
+ // transports
56
+ createRPCMemoryTransport,
57
+ createRPCHttpTransport,
58
+ createRPCFetchHandler,
59
+ createRPCJsonSerializer,
60
+ readBoundedBody,
61
+ isRPCResponseFrame,
62
+ RPC_HTTP_STATUS,
63
+ rpcHttpStatus,
64
+ // errors on the wire
65
+ mapRPCError,
66
+ rpcErrorFromWire,
67
+ DEFAULT_RPC_HTTP_MAX_BODY_BYTES,
68
+ MAX_RPC_FRAME_DEPTH,
52
69
  } from "@zudojs/rpc";
53
70
  ```
54
71
 
@@ -90,17 +107,19 @@ The frame's `metadata` is optional.
90
107
 
91
108
  ### Calling through a transport
92
109
 
93
- `RPCClient` needs an `RPCTransport` — an object whose `send(request, options)`
110
+ `RPCClient` sends through an `RPCTransport`: an object whose `send(request, options)`
94
111
  delivers the frame and resolves with the response. `options.signal` aborts the
95
- call and `options.timeout` is the deadline in milliseconds, so a transport can
96
- set its own socket timeout. The in-process transport below is the smallest
97
- possible one:
112
+ call and `options.timeout` is the deadline in milliseconds.
113
+
114
+ **In memory**, for tests and modular monoliths:
98
115
 
99
116
  ```typescript
100
- import { RPCClient, type RPCTransport } from "@zudojs/rpc";
117
+ import { RPCClient, createRPCMemoryTransport } from "@zudojs/rpc";
101
118
 
102
- const transport: RPCTransport = { send: (request) => server.handle(request) };
103
- const client = new RPCClient(transport, { timeout: 5_000 });
119
+ const client = new RPCClient(
120
+ createRPCMemoryTransport(server, { auth: { userId: "u1" } }),
121
+ { timeout: 5_000 },
122
+ );
104
123
 
105
124
  const total = await client.call<{ a: number; b: number }, number>("math.sum", {
106
125
  a: 1,
@@ -109,9 +128,83 @@ const total = await client.call<{ a: number; b: number }, number>("math.sum", {
109
128
  // 3
110
129
  ```
111
130
 
131
+ Frames are round-tripped through JSON by default, so the server never shares
132
+ objects with the caller, and a value that could not cross a network (a
133
+ `BigInt` result, a cycle) fails in memory too. Pass `serializer: false` to hand
134
+ frames over by reference. `auth` (a value, or a function of the request) is
135
+ handed to the server as the trusted `context.auth`.
136
+
137
+ **Over HTTP.** Mount the server's fetch handler on any Fetch API server at one
138
+ POST endpoint, and point the client transport at it:
139
+
140
+ ```typescript
141
+ import { createRPCFetchHandler, createRPCHttpTransport } from "@zudojs/rpc";
142
+
143
+ // Server: (request: Request) => Promise<Response>
144
+ const handle = createRPCFetchHandler(server, {
145
+ auth: async (request) => verifyBearer(request.headers.get("authorization")),
146
+ onInternalError: (error, requestId) => logger.error({ requestId, error }),
147
+ });
148
+ Bun.serve({ port: 3000, fetch: handle }); // or Deno.serve, an edge runtime, …
149
+ // On @zudojs/http, no glue code: mountFetchHandler(router, "/rpc", handle);
150
+
151
+ // Client: uses the global fetch.
152
+ const remote = new RPCClient(
153
+ createRPCHttpTransport({
154
+ url: "https://math.internal/rpc",
155
+ headers: () => ({ authorization: `Bearer ${currentToken()}` }),
156
+ }),
157
+ { timeout: 5_000 },
158
+ );
159
+ await remote.call("math.sum", { a: 1, b: 2 });
160
+ ```
161
+
162
+ On `@zudojs/http`, mount it with `mountFetchHandler(router, "/rpc", handle)`. The handler answers every path it is mounted on, so the prefix that `mountFetchHandler` strips does not matter. It reads at most `maxBodyBytes` (default
163
+ `DEFAULT_RPC_HTTP_MAX_BODY_BYTES`, 1 MiB plus envelope headroom) without
164
+ buffering more, and decodes with a size- and depth-limited
165
+ `@zudojs/serialization` JSON serializer. Every reply, including one to a bad
166
+ HTTP request (wrong method, wrong content type, oversized or invalid body), is
167
+ an RPC frame with a status from `RPC_HTTP_STATUS` (404 unknown procedure, 422
168
+ validation, 401, 403, 429, 504 timeout, 500 internal, and so on). The status is
169
+ advisory; `error.code` is authoritative. An `auth` hook that throws an
170
+ `RPCAuthenticationError` refuses the call. Any other error it throws is
171
+ answered as an internal error. The call runs under `request.signal`, so a
172
+ client that disconnects cancels the procedure (`context.signal` aborts); the
173
+ memory transport passes the caller's signal to the server the same way.
174
+
175
+ The server refuses (`RPC_INVALID_REQUEST`) a frame whose `payload` or
176
+ `metadata` holds a `__proto__`, `constructor` or `prototype` key at any
177
+ depth: `JSON.parse` keeps such a key as an own property, and a handler that
178
+ merges its input into another object would have that object's prototype
179
+ replaced. `limits: { allowUnsafeKeys: true }` turns the check off, and
180
+ `findUnsafeKey(value)` runs it on anything else you decode.
181
+
182
+ The client transport aborts the underlying `fetch` when the call's signal or
183
+ deadline fires. It reports a network failure, or a reply that is not an RPC
184
+ frame for this request (a proxy's HTML page, a truncated or oversized body), as
185
+ an `RPCTransportError`. An expired deadline is an `RPCTimeoutError`, and a caller
186
+ abort is an `RPCCancelledError`. Use the same serializer on both ends:
187
+ `createRPCJsonSerializer({ preserveTypes: true })` carries `Date`, `BigInt`,
188
+ `Map` and `Set`.
189
+
112
190
  A failed call rejects with a typed error rebuilt from the wire code
113
- (`RPCTimeoutError`, `RPCCancelledError`, `RPCUnavailableError`, or an
114
- `RPCError` carrying the server's `code` and `details`).
191
+ (`rpcErrorFromWire`): `RPCProcedureNotFoundError`, `RPCValidationError` (with
192
+ `issues`), `RPCInvalidRequestError`, `RPCAuthenticationError`,
193
+ `RPCForbiddenError`, `RPCRateLimitedError`, `RPCTimeoutError`,
194
+ `RPCCancelledError` or `RPCUnavailableError`, or else an `RPCError`. The
195
+ server's `details` are kept on the error as `error.details`.
196
+
197
+ `error.code` is always the wire code — `"RPC_TIMEOUT"`, `"RPC_CANCELLED"`,
198
+ `"RPC_UNAVAILABLE"`, `"RPC_VALIDATION_ERROR"`, `"RPC_NOT_FOUND"` … — whether
199
+ the server reported the failure or the client raised it itself (its own
200
+ deadline, a cancelled signal, a closed client, `"RPC_TRANSPORT_ERROR"` for a
201
+ network failure). Branch on `instanceof` or on those strings; the class
202
+ codes (`ErrorCode.RPC_TIMEOUT`, `"ERR_RPC_TIMEOUT"`) are not what a client
203
+ error carries.
204
+
205
+ A pending call's deadline and a `retry()` backoff hold a normal (ref'd)
206
+ timer, cleared as soon as the call settles, so a plain script awaiting a call
207
+ stays alive until it resolves or times out.
115
208
 
116
209
  ### Middleware and trusted identity
117
210
 
@@ -154,10 +247,20 @@ server maps them to wire codes (`RPC_PROCEDURE_NOT_FOUND`,
154
247
  `RPC_RATE_LIMITED`, `RPC_TIMEOUT`, …). A custom `RPCError` subclass keeps its
155
248
  own `code`.
156
249
 
157
- What reaches the caller follows the error's `expose` flag. Anything thrown
158
- with `expose: false` — an `RPCInternalError`, an `RPCSerializationError`, a
159
- plain `new RPCError(...)` (whose default is `expose: false`), or any
160
- non-RPC error — is answered with the fixed `INTERNAL_ERROR_MESSAGE`; the
250
+ `mapRPCError(error)` is the mapping the server and the fetch handler share. Use it in a custom transport to produce the same wire payloads.
251
+
252
+ What reaches the caller follows the error's `expose` flag. A
253
+ `@zudojs/errors` error built with `expose: true` keeps its message and maps
254
+ to the matching code by status: `NotFoundError` → `RPC_NOT_FOUND`,
255
+ `ConflictError` → `RPC_CONFLICT`, `ValidationError` → `RPC_VALIDATION_ERROR`
256
+ (with its issues, minus the received values, as `details`),
257
+ `AuthenticationError` → `RPC_UNAUTHENTICATED`, `AuthorizationError` →
258
+ `RPC_FORBIDDEN`, `RateLimitError` → `RPC_RATE_LIMITED` (with
259
+ `{ retryAfter }`); an unlisted status keeps the error's own code. Anything
260
+ thrown with `expose: false` — an `RPCInternalError`, an
261
+ `RPCSerializationError`, a plain `new RPCError(...)` (whose default is
262
+ `expose: false`), a non-exposed `BaseError`, or any other error — is
263
+ answered with the fixed `INTERNAL_ERROR_MESSAGE`; the
161
264
  original error is handed to `onInternalError(error, requestId)` so it can be
162
265
  logged against the request id. A handler result that fails the procedure's
163
266
  `output` schema is treated the same way: it is the server's fault, not the
package/dist/index.d.ts CHANGED
@@ -8,20 +8,26 @@
8
8
  *
9
9
  * @example
10
10
  * ```ts
11
- * import { RPCServer, RPCClient, createRPCProcedure, RPCMiddlewareStack } from "@zudojs/rpc";
11
+ * import {
12
+ * RPCServer,
13
+ * RPCClient,
14
+ * createRPCProcedure,
15
+ * createRPCMemoryTransport,
16
+ * } from "@zudojs/rpc";
12
17
  *
13
18
  * const server = new RPCServer();
14
19
  * server.register(createRPCProcedure("users.getUser", async (input) => {
15
20
  * return userService.findById(input.id);
16
21
  * }));
17
22
  *
18
- * const client = new RPCClient(memoryTransport);
23
+ * // In-process; swap for createRPCHttpTransport({ url }) across the network.
24
+ * const client = new RPCClient(createRPCMemoryTransport(server));
19
25
  * const user = await client.call("users.getUser", { id: "123" });
20
26
  * ```
21
27
  */
22
28
  export type { RPCProcedureName, RPCMetadata, RPCMetadataOptions, RPCRequest, RPCRequestOptions, RPCErrorPayload, RPCResponse, } from "./rpc/types/index.js";
23
29
  export { createRPCMetadata, createRPCRequest, createRPCResponse, createRPCErrorResponse, } from "./rpc/types/index.js";
24
- export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_RPC_REQUEST_ID_LENGTH, 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";
30
+ export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_RPC_REQUEST_ID_LENGTH, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, MAX_PROCEDURE_NAME_LENGTH, MAX_TIMER_DELAY, PROCEDURE_NAME_PATTERN, INTERNAL_ERROR_MESSAGE, DEFAULT_RPC_HTTP_MAX_BODY_BYTES, MAX_RPC_FRAME_DEPTH, } from "./rpc/constants/index.js";
25
31
  export type { RPCRequestLimits, RPCSchema } from "./rpc/validation/index.js";
26
32
  export { assertValidProcedureName, assertValidRequest, measurePayloadBytes, toValidationIssues, parseInput, parseOutput, } from "./rpc/validation/index.js";
27
33
  export type { RPCErrorOptions } from "./rpc/errors/index.js";
@@ -35,11 +41,9 @@ export type { RPCMiddleware } from "./rpc/middleware/index.js";
35
41
  export { RPCMiddlewareStack } from "./rpc/middleware/index.js";
36
42
  export type { RPCDispatcherOptions } from "./rpc/dispatcher/index.js";
37
43
  export { RPCDispatcher } from "./rpc/dispatcher/index.js";
38
- export type { RPCServerOptions } from "./rpc/server/index.js";
39
- export { RPCServer } from "./rpc/server/index.js";
40
- export type { RPCTransport, RPCTransportRequestOptions, } from "./rpc/transport/index.js";
41
- export type { RPCCallOptions, RPCClientOptions } from "./rpc/client/index.js";
42
- export { RPCClient } from "./rpc/client/index.js";
44
+ export * from "./rpc/server/index.js";
45
+ export * from "./rpc/transport/index.js";
46
+ export * from "./rpc/client/index.js";
43
47
  export { createTimeout, withTimeout, runWithTimeout, getRemainingTime, isDeadlineExceeded, throwIfDeadlineExceeded, readDeadline, createCancellableSignal, cancelSignal, throwIfCancelled, combineSignals, DEFAULT_RETRY_OPTIONS, calculateRetryDelay, retry, } from "./rpc/reliability/index.js";
44
48
  export type { RPCBackoff, RPCJitter, RPCRetryOptions, CancellableSignal, } from "./rpc/reliability/index.js";
45
49
  export type { RPCInterceptor } from "./rpc/interceptor/index.js";
package/dist/index.js CHANGED
@@ -8,20 +8,26 @@
8
8
  *
9
9
  * @example
10
10
  * ```ts
11
- * import { RPCServer, RPCClient, createRPCProcedure, RPCMiddlewareStack } from "@zudojs/rpc";
11
+ * import {
12
+ * RPCServer,
13
+ * RPCClient,
14
+ * createRPCProcedure,
15
+ * createRPCMemoryTransport,
16
+ * } from "@zudojs/rpc";
12
17
  *
13
18
  * const server = new RPCServer();
14
19
  * server.register(createRPCProcedure("users.getUser", async (input) => {
15
20
  * return userService.findById(input.id);
16
21
  * }));
17
22
  *
18
- * const client = new RPCClient(memoryTransport);
23
+ * // In-process; swap for createRPCHttpTransport({ url }) across the network.
24
+ * const client = new RPCClient(createRPCMemoryTransport(server));
19
25
  * const user = await client.call("users.getUser", { id: "123" });
20
26
  * ```
21
27
  */
22
28
  export { createRPCMetadata, createRPCRequest, createRPCResponse, createRPCErrorResponse, } from "./rpc/types/index.js";
23
29
  // Constants
24
- export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_RPC_REQUEST_ID_LENGTH, 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";
30
+ export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_RPC_REQUEST_ID_LENGTH, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, MAX_PROCEDURE_NAME_LENGTH, MAX_TIMER_DELAY, PROCEDURE_NAME_PATTERN, INTERNAL_ERROR_MESSAGE, DEFAULT_RPC_HTTP_MAX_BODY_BYTES, MAX_RPC_FRAME_DEPTH, } from "./rpc/constants/index.js";
25
31
  export { assertValidProcedureName, assertValidRequest, measurePayloadBytes, toValidationIssues, parseInput, parseOutput, } from "./rpc/validation/index.js";
26
32
  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
33
  export { createRPCProcedure } from "./rpc/procedure/index.js";
@@ -29,8 +35,11 @@ export { RPCProcedureRegistry, RPCProcedureRouter, } from "./rpc/procedure/index
29
35
  export { createRPCContext } from "./rpc/context/index.js";
30
36
  export { RPCMiddlewareStack } from "./rpc/middleware/index.js";
31
37
  export { RPCDispatcher } from "./rpc/dispatcher/index.js";
32
- export { RPCServer } from "./rpc/server/index.js";
33
- export { RPCClient } from "./rpc/client/index.js";
38
+ // Server, transports (in-memory and HTTP) and client — every export of
39
+ // these barrels is public.
40
+ export * from "./rpc/server/index.js";
41
+ export * from "./rpc/transport/index.js";
42
+ export * from "./rpc/client/index.js";
34
43
  // Reliability
35
44
  export { createTimeout, withTimeout, runWithTimeout, getRemainingTime, isDeadlineExceeded, throwIfDeadlineExceeded, readDeadline, createCancellableSignal, cancelSignal, throwIfCancelled, combineSignals, DEFAULT_RETRY_OPTIONS, calculateRetryDelay, retry, } from "./rpc/reliability/index.js";
36
45
  export { createNoopRPCInterceptor } from "./rpc/interceptor/index.js";
@@ -1,3 +1,8 @@
1
+ /**
2
+ * RPC client: builds request frames, sends them through an
3
+ * {@link RPCTransport}, and rebuilds typed errors from error responses.
4
+ */
1
5
  export type { RPCCallOptions, RPCClientOptions } from "./rpcClient.core.js";
2
6
  export { RPCClient } from "./rpcClient.core.js";
7
+ export { rpcErrorFromWire } from "./rpcWireError.helper.js";
3
8
  //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1,7 @@
1
+ /**
2
+ * RPC client: builds request frames, sends them through an
3
+ * {@link RPCTransport}, and rebuilds typed errors from error responses.
4
+ */
1
5
  export { RPCClient } from "./rpcClient.core.js";
6
+ export { rpcErrorFromWire } from "./rpcWireError.helper.js";
2
7
  //# sourceMappingURL=index.js.map
@@ -41,8 +41,14 @@ export declare class RPCClient {
41
41
  get pendingCount(): number;
42
42
  /**
43
43
  * Calls a remote procedure.
44
+ *
45
+ * Rejects with a typed RPC error whose `code` is the wire code
46
+ * (`"RPC_TIMEOUT"`, `"RPC_CANCELLED"`, `"RPC_VALIDATION_ERROR"` …)
47
+ * whether the server reported the failure or the client raised it (its
48
+ * own deadline, a cancelled signal, a closed client).
44
49
  */
45
50
  call<TInput = unknown, TOutput = unknown>(procedure: string, input: TInput, options?: RPCCallOptions): Promise<TOutput>;
51
+ private invoke;
46
52
  /**
47
53
  * Cancels every in-flight call and stops accepting new ones.
48
54
  */
@@ -69,12 +75,5 @@ export declare class RPCClient {
69
75
  * error is passed through untouched.
70
76
  */
71
77
  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.
77
- */
78
- private toError;
79
78
  }
80
79
  //# sourceMappingURL=rpcClient.core.d.ts.map
@@ -1,6 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { createRPCRequest } from "../types/rpcRequest.type.js";
3
- import { isRPCError, RPCCancelledError, RPCError, RPCTimeoutError, RPCUnavailableError, } from "../errors/rpc.errors.js";
3
+ import { isRPCError, RPCCancelledError, RPCUnavailableError, } from "../errors/rpc.errors.js";
4
+ import { rpcErrorFromWire } from "./rpcWireError.helper.js";
5
+ import { rpcWireCodeOf } from "../server/rpcErrorMapping.helper.js";
4
6
  import { DEFAULT_RPC_TIMEOUT, MAX_PENDING_REQUESTS, } from "../constants/rpcConstants.core.js";
5
7
  import { createTimeout } from "../reliability/timeout/rpcTimeout.helper.js";
6
8
  /**
@@ -28,8 +30,21 @@ export class RPCClient {
28
30
  }
29
31
  /**
30
32
  * Calls a remote procedure.
33
+ *
34
+ * Rejects with a typed RPC error whose `code` is the wire code
35
+ * (`"RPC_TIMEOUT"`, `"RPC_CANCELLED"`, `"RPC_VALIDATION_ERROR"` …)
36
+ * whether the server reported the failure or the client raised it (its
37
+ * own deadline, a cancelled signal, a closed client).
31
38
  */
32
39
  async call(procedure, input, options = {}) {
40
+ try {
41
+ return await this.invoke(procedure, input, options);
42
+ }
43
+ catch (error) {
44
+ throw withWireCode(error);
45
+ }
46
+ }
47
+ async invoke(procedure, input, options) {
33
48
  if (this.closed) {
34
49
  throw new RPCUnavailableError("RPC client has been closed.", procedure);
35
50
  }
@@ -84,7 +99,7 @@ export class RPCClient {
84
99
  }
85
100
  const response = await Promise.race(races);
86
101
  if (!response.success) {
87
- throw this.toError(response, procedure);
102
+ throw rpcErrorFromWire(response.error, procedure);
88
103
  }
89
104
  return response.result;
90
105
  }
@@ -166,52 +181,23 @@ export class RPCClient {
166
181
  }
167
182
  return error;
168
183
  }
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;
184
+ }
185
+ /**
186
+ * Gives a typed RPC error its wire code. An error rebuilt from a response
187
+ * already has it; one raised locally still carries its class code
188
+ * (`ERR_RPC_TIMEOUT`), which a caller comparing against `"RPC_TIMEOUT"`
189
+ * would never match.
190
+ */
191
+ function withWireCode(error) {
192
+ const code = isRPCError(error) ? rpcWireCodeOf(error) : undefined;
193
+ if (code !== undefined && error instanceof Error && error.code !== code) {
194
+ try {
195
+ Object.defineProperty(error, "code", { value: code, enumerable: true, configurable: true });
198
196
  }
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,
206
- });
207
- if (response.error?.details !== undefined) {
208
- Object.defineProperty(error, "details", {
209
- value: response.error.details,
210
- enumerable: true,
211
- configurable: true,
212
- });
197
+ catch {
198
+ // A frozen error keeps the code it has.
213
199
  }
214
- return error;
215
200
  }
201
+ return error;
216
202
  }
217
203
  //# sourceMappingURL=rpcClient.core.js.map
@@ -0,0 +1,13 @@
1
+ import type { RPCErrorPayload } from "../types/rpcResponse.type.js";
2
+ import { RPCError } from "../errors/rpc.errors.js";
3
+ /**
4
+ * Rebuilds a typed error from an error response's payload.
5
+ *
6
+ * The wire code drives the class, so a caller can tell a validation
7
+ * failure from an authentication failure or a timeout with `instanceof`
8
+ * rather than string matching. Every rebuilt error carries the server's
9
+ * wire `code` (`error.code === "RPC_TIMEOUT"`, `"RPC_VALIDATION_ERROR"`,
10
+ * `"RPC_NOT_FOUND"` …) and its `details`.
11
+ */
12
+ export declare function rpcErrorFromWire(payload: RPCErrorPayload | undefined, procedure: string): RPCError;
13
+ //# sourceMappingURL=rpcWireError.helper.d.ts.map
@@ -0,0 +1,71 @@
1
+ import { RPCAuthenticationError, RPCCancelledError, RPCError, RPCForbiddenError, RPCInvalidRequestError, RPCProcedureNotFoundError, RPCRateLimitedError, RPCTimeoutError, RPCUnavailableError, RPCValidationError, } from "../errors/rpc.errors.js";
2
+ /**
3
+ * Rebuilds a typed error from an error response's payload.
4
+ *
5
+ * The wire code drives the class, so a caller can tell a validation
6
+ * failure from an authentication failure or a timeout with `instanceof`
7
+ * rather than string matching. Every rebuilt error carries the server's
8
+ * wire `code` (`error.code === "RPC_TIMEOUT"`, `"RPC_VALIDATION_ERROR"`,
9
+ * `"RPC_NOT_FOUND"` …) and its `details`.
10
+ */
11
+ export function rpcErrorFromWire(payload, procedure) {
12
+ const message = payload?.message ?? "RPC call failed.";
13
+ const code = payload?.code;
14
+ const details = payload?.details;
15
+ switch (code) {
16
+ case "RPC_TIMEOUT":
17
+ return withWire(withMessage(new RPCTimeoutError(0, procedure), message), code, details);
18
+ case "RPC_CANCELLED":
19
+ return withWire(new RPCCancelledError(message, procedure), code, details);
20
+ case "RPC_UNAVAILABLE":
21
+ return withWire(new RPCUnavailableError(message, procedure), code, details);
22
+ case "RPC_PROCEDURE_NOT_FOUND":
23
+ return withWire(withMessage(new RPCProcedureNotFoundError(procedure), message), code, details);
24
+ case "RPC_VALIDATION_ERROR":
25
+ return withWire(new RPCValidationError(message, (Array.isArray(details) ? details : []), procedure), code, details);
26
+ case "RPC_INVALID_REQUEST":
27
+ return withWire(new RPCInvalidRequestError(message, procedure), code, details);
28
+ case "RPC_UNAUTHENTICATED":
29
+ return withWire(new RPCAuthenticationError(message, procedure), code, details);
30
+ case "RPC_FORBIDDEN":
31
+ return withWire(new RPCForbiddenError(message, procedure), code, details);
32
+ case "RPC_RATE_LIMITED":
33
+ return withWire(new RPCRateLimitedError(message, retryAfterOf(details), procedure), code, details);
34
+ default:
35
+ return withWire(new RPCError(message, { procedureName: procedure }), code, details);
36
+ }
37
+ }
38
+ function retryAfterOf(details) {
39
+ if (typeof details !== "object" || details === null) {
40
+ return undefined;
41
+ }
42
+ const value = details.retryAfter;
43
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
44
+ ? value
45
+ : undefined;
46
+ }
47
+ function withMessage(error, message) {
48
+ Object.defineProperty(error, "message", {
49
+ value: message,
50
+ enumerable: false,
51
+ configurable: true,
52
+ writable: true,
53
+ });
54
+ return error;
55
+ }
56
+ function withWire(error, code, details) {
57
+ Object.defineProperty(error, "code", {
58
+ value: code ?? error.code,
59
+ enumerable: true,
60
+ configurable: true,
61
+ });
62
+ if (details !== undefined) {
63
+ Object.defineProperty(error, "details", {
64
+ value: details,
65
+ enumerable: true,
66
+ configurable: true,
67
+ });
68
+ }
69
+ return error;
70
+ }
71
+ //# sourceMappingURL=rpcWireError.helper.js.map
@@ -1,2 +1,7 @@
1
- export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_RPC_REQUEST_ID_LENGTH, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, MAX_PROCEDURE_NAME_LENGTH, MAX_TIMER_DELAY, PROCEDURE_NAME_PATTERN, INTERNAL_ERROR_MESSAGE, } from "./rpcConstants.core.js";
1
+ /**
2
+ * Limits and defaults shared across the RPC package: timeouts, payload
3
+ * and frame sizes, procedure-name rules, and the fixed internal-error
4
+ * message returned to remote callers.
5
+ */
6
+ export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_RPC_REQUEST_ID_LENGTH, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, MAX_PROCEDURE_NAME_LENGTH, MAX_TIMER_DELAY, PROCEDURE_NAME_PATTERN, INTERNAL_ERROR_MESSAGE, DEFAULT_RPC_HTTP_MAX_BODY_BYTES, MAX_RPC_FRAME_DEPTH, } from "./rpcConstants.core.js";
2
7
  //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1,7 @@
1
- export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_RPC_REQUEST_ID_LENGTH, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, MAX_PROCEDURE_NAME_LENGTH, MAX_TIMER_DELAY, PROCEDURE_NAME_PATTERN, INTERNAL_ERROR_MESSAGE, } from "./rpcConstants.core.js";
1
+ /**
2
+ * Limits and defaults shared across the RPC package: timeouts, payload
3
+ * and frame sizes, procedure-name rules, and the fixed internal-error
4
+ * message returned to remote callers.
5
+ */
6
+ export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_RPC_REQUEST_ID_LENGTH, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, MAX_PROCEDURE_NAME_LENGTH, MAX_TIMER_DELAY, PROCEDURE_NAME_PATTERN, INTERNAL_ERROR_MESSAGE, DEFAULT_RPC_HTTP_MAX_BODY_BYTES, MAX_RPC_FRAME_DEPTH, } from "./rpcConstants.core.js";
2
7
  //# sourceMappingURL=index.js.map
@@ -54,4 +54,15 @@ export declare const MAX_TIMER_DELAY = 2147483647;
54
54
  * request id instead.
55
55
  */
56
56
  export declare const INTERNAL_ERROR_MESSAGE = "The server encountered an internal error while handling this request.";
57
+ /**
58
+ * Default limit, in bytes, on an HTTP request or response body carrying
59
+ * one RPC frame: the payload limit plus headroom for the frame envelope
60
+ * (`id`, `procedure`, `metadata`, `timestamp`).
61
+ */
62
+ export declare const DEFAULT_RPC_HTTP_MAX_BODY_BYTES: number;
63
+ /**
64
+ * Maximum nesting depth accepted when decoding a frame from the wire.
65
+ * Bounds the work a hostile peer can force with deeply nested JSON.
66
+ */
67
+ export declare const MAX_RPC_FRAME_DEPTH = 128;
57
68
  //# sourceMappingURL=rpcConstants.core.d.ts.map
@@ -54,4 +54,15 @@ export const MAX_TIMER_DELAY = 2_147_483_647;
54
54
  * request id instead.
55
55
  */
56
56
  export const INTERNAL_ERROR_MESSAGE = "The server encountered an internal error while handling this request.";
57
+ /**
58
+ * Default limit, in bytes, on an HTTP request or response body carrying
59
+ * one RPC frame: the payload limit plus headroom for the frame envelope
60
+ * (`id`, `procedure`, `metadata`, `timestamp`).
61
+ */
62
+ export const DEFAULT_RPC_HTTP_MAX_BODY_BYTES = MAX_RPC_PAYLOAD_SIZE + 64 * 1024;
63
+ /**
64
+ * Maximum nesting depth accepted when decoding a frame from the wire.
65
+ * Bounds the work a hostile peer can force with deeply nested JSON.
66
+ */
67
+ export const MAX_RPC_FRAME_DEPTH = 128;
57
68
  //# sourceMappingURL=rpcConstants.core.js.map
@@ -15,6 +15,13 @@ export type RPCAuthContext = Readonly<Record<string, unknown>>;
15
15
  export interface RPCContextOptions {
16
16
  /** Trusted, transport-derived identity. See {@link RPCAuthContext}. */
17
17
  readonly auth?: RPCAuthContext;
18
+ /**
19
+ * Aborts when the caller is gone — the HTTP client disconnected, the
20
+ * in-process caller cancelled. The dispatch then fails with
21
+ * `RPC_CANCELLED` and the handler's `context.signal` aborts, so the
22
+ * server stops work nobody will read.
23
+ */
24
+ readonly signal?: AbortSignal;
18
25
  }
19
26
  /**
20
27
  * Context passed through the RPC execution pipeline.
@@ -5,6 +5,7 @@ import { DEFAULT_RPC_TIMEOUT } from "../constants/rpcConstants.core.js";
5
5
  import { parseInput, parseOutput } from "../validation/rpcValidation.core.js";
6
6
  import { readDeadline, throwIfDeadlineExceeded, } from "../reliability/deadline/rpcDeadline.helper.js";
7
7
  import { createTimeout } from "../reliability/timeout/rpcTimeout.helper.js";
8
+ import { abortReasonToRPCError, raceAbort, } from "../reliability/cancellation/rpcAbort.helper.js";
8
9
  /**
9
10
  * Dispatches RPC requests to registered procedures.
10
11
  *
@@ -37,6 +38,10 @@ export class RPCDispatcher {
37
38
  // — deadline reading, the context's `metadata` — reads it as an object.
38
39
  const request = input.metadata === undefined ? { ...input, metadata: {} } : input;
39
40
  const procedure = this.registry.require(request.procedure);
41
+ const caller = trusted.signal;
42
+ if (caller?.aborted) {
43
+ throw abortReasonToRPCError(caller, request.procedure);
44
+ }
40
45
  const controller = new AbortController();
41
46
  const context = createRPCContext(request, controller.signal, trusted);
42
47
  const timeoutMs = this.resolveTimeout(request, procedure);
@@ -48,6 +53,13 @@ export class RPCDispatcher {
48
53
  }
49
54
  }
50
55
  const timeout = timeoutMs > 0 ? createTimeout(timeoutMs, request.procedure) : undefined;
56
+ // The caller going away cancels the work, not just the wait for it.
57
+ const onCallerAbort = () => {
58
+ if (!controller.signal.aborted && caller !== undefined) {
59
+ controller.abort(abortReasonToRPCError(caller, request.procedure));
60
+ }
61
+ };
62
+ caller?.addEventListener("abort", onCallerAbort, { once: true });
51
63
  try {
52
64
  const run = async () => {
53
65
  const input = procedure.options?.input
@@ -61,7 +73,7 @@ export class RPCDispatcher {
61
73
  ? parseOutput(procedure.options.output, result, request.procedure)
62
74
  : result;
63
75
  };
64
- const invoke = () => this.applyInterceptors(context, run);
76
+ const invoke = () => raceAbort(this.applyInterceptors(context, run), caller, request.procedure);
65
77
  const result = timeout === undefined
66
78
  ? await invoke()
67
79
  : await Promise.race([
@@ -82,6 +94,7 @@ export class RPCDispatcher {
82
94
  // an auth failure or a rate limit into a generic internal fault;
83
95
  // the server owns the mapping from error type to response code.
84
96
  timeout?.cancel();
97
+ caller?.removeEventListener("abort", onCallerAbort);
85
98
  if (!controller.signal.aborted) {
86
99
  // Release anything still listening on the request signal.
87
100
  controller.abort(new RPCCancelledError("Request completed.", request.procedure));