@zudojs/rpc 1.2.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 +137 -20
  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 +20 -0
  13. package/dist/rpc/constants/rpcConstants.core.js +20 -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 +15 -93
  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 +26 -3
  54. package/dist/rpc/validation/rpcValidation.core.js +27 -5
  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
 
@@ -74,7 +91,11 @@ const server = new RPCServer();
74
91
  server.register(sum);
75
92
 
76
93
  const response = await server.handle(
77
- createRPCRequest({ id: "req-1", procedure: "math.sum", payload: { a: 1, b: 2 } }),
94
+ createRPCRequest({
95
+ id: "req-1",
96
+ procedure: "math.sum",
97
+ payload: { a: 1, b: 2 },
98
+ }),
78
99
  );
79
100
  // { id: "req-1", success: true, result: 3 }
80
101
  ```
@@ -86,25 +107,104 @@ The frame's `metadata` is optional.
86
107
 
87
108
  ### Calling through a transport
88
109
 
89
- `RPCClient` needs an `RPCTransport` — an object whose `send(request, options)`
110
+ `RPCClient` sends through an `RPCTransport`: an object whose `send(request, options)`
90
111
  delivers the frame and resolves with the response. `options.signal` aborts the
91
- call and `options.timeout` is the deadline in milliseconds, so a transport can
92
- set its own socket timeout. The in-process transport below is the smallest
93
- possible one:
112
+ call and `options.timeout` is the deadline in milliseconds.
113
+
114
+ **In memory**, for tests and modular monoliths:
94
115
 
95
116
  ```typescript
96
- import { RPCClient, type RPCTransport } from "@zudojs/rpc";
117
+ import { RPCClient, createRPCMemoryTransport } from "@zudojs/rpc";
97
118
 
98
- const transport: RPCTransport = { send: (request) => server.handle(request) };
99
- const client = new RPCClient(transport, { timeout: 5_000 });
119
+ const client = new RPCClient(
120
+ createRPCMemoryTransport(server, { auth: { userId: "u1" } }),
121
+ { timeout: 5_000 },
122
+ );
100
123
 
101
- const total = await client.call<{ a: number; b: number }, number>("math.sum", { a: 1, b: 2 });
124
+ const total = await client.call<{ a: number; b: number }, number>("math.sum", {
125
+ a: 1,
126
+ b: 2,
127
+ });
102
128
  // 3
103
129
  ```
104
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
+
105
190
  A failed call rejects with a typed error rebuilt from the wire code
106
- (`RPCTimeoutError`, `RPCCancelledError`, `RPCUnavailableError`, or an
107
- `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.
108
208
 
109
209
  ### Middleware and trusted identity
110
210
 
@@ -147,10 +247,20 @@ server maps them to wire codes (`RPC_PROCEDURE_NOT_FOUND`,
147
247
  `RPC_RATE_LIMITED`, `RPC_TIMEOUT`, …). A custom `RPCError` subclass keeps its
148
248
  own `code`.
149
249
 
150
- What reaches the caller follows the error's `expose` flag. Anything thrown
151
- with `expose: false` — an `RPCInternalError`, an `RPCSerializationError`, a
152
- plain `new RPCError(...)` (whose default is `expose: false`), or any
153
- 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
154
264
  original error is handed to `onInternalError(error, requestId)` so it can be
155
265
  logged against the request id. A handler result that fails the procedure's
156
266
  `output` schema is treated the same way: it is the server's fault, not the
@@ -158,12 +268,19 @@ caller's.
158
268
 
159
269
  ```typescript
160
270
  const server = new RPCServer(undefined, undefined, {
161
- limits: { maxPayloadBytes: 256 * 1024 },
271
+ limits: { maxPayloadBytes: 256 * 1024, maxRequestIdLength: 128 },
162
272
  dispatch: { defaultTimeout: 10_000 },
163
273
  onInternalError: (error, requestId) => logger.error({ requestId, error }),
164
274
  });
165
275
  ```
166
276
 
277
+ `maxPayloadBytes` bounds `payload` and `metadata` together, because both are
278
+ caller-controlled and both reach the handler. `maxRequestIdLength` bounds
279
+ `request.id`, which every response echoes back; an id over the limit is
280
+ refused before a response is built, so it is never reflected. Both default to
281
+ `MAX_RPC_PAYLOAD_SIZE` (1 MiB) and `MAX_RPC_REQUEST_ID_LENGTH` (128), and
282
+ either can be set to `0` when the transport already enforces the limit.
283
+
167
284
  ## License
168
285
 
169
286
  MIT
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_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_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_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_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
@@ -11,6 +11,15 @@ export declare const DEFAULT_RPC_TIMEOUT = 30000;
11
11
  * Maximum payload size for RPC messages (1MB).
12
12
  */
13
13
  export declare const MAX_RPC_PAYLOAD_SIZE: number;
14
+ /**
15
+ * Maximum length of a request id accepted from a peer.
16
+ *
17
+ * The id is echoed verbatim into every success and error response, so an
18
+ * unbounded id is a reflection amplifier: the server writes back whatever
19
+ * the caller sent, on both paths. 128 characters holds a UUID, a ULID or a
20
+ * W3C trace id with room to spare.
21
+ */
22
+ export declare const MAX_RPC_REQUEST_ID_LENGTH = 128;
14
23
  /**
15
24
  * Maximum number of pending requests allowed in the client.
16
25
  */
@@ -45,4 +54,15 @@ export declare const MAX_TIMER_DELAY = 2147483647;
45
54
  * request id instead.
46
55
  */
47
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;
48
68
  //# sourceMappingURL=rpcConstants.core.d.ts.map
@@ -11,6 +11,15 @@ export const DEFAULT_RPC_TIMEOUT = 30_000;
11
11
  * Maximum payload size for RPC messages (1MB).
12
12
  */
13
13
  export const MAX_RPC_PAYLOAD_SIZE = 1024 * 1024;
14
+ /**
15
+ * Maximum length of a request id accepted from a peer.
16
+ *
17
+ * The id is echoed verbatim into every success and error response, so an
18
+ * unbounded id is a reflection amplifier: the server writes back whatever
19
+ * the caller sent, on both paths. 128 characters holds a UUID, a ULID or a
20
+ * W3C trace id with room to spare.
21
+ */
22
+ export const MAX_RPC_REQUEST_ID_LENGTH = 128;
14
23
  /**
15
24
  * Maximum number of pending requests allowed in the client.
16
25
  */
@@ -45,4 +54,15 @@ export const MAX_TIMER_DELAY = 2_147_483_647;
45
54
  * request id instead.
46
55
  */
47
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;
48
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.