@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.
- package/README.md +120 -17
- package/dist/index.d.ts +12 -8
- package/dist/index.js +14 -5
- package/dist/rpc/client/index.d.ts +5 -0
- package/dist/rpc/client/index.js +5 -0
- package/dist/rpc/client/rpcClient.core.d.ts +6 -7
- package/dist/rpc/client/rpcClient.core.js +32 -46
- package/dist/rpc/client/rpcWireError.helper.d.ts +13 -0
- package/dist/rpc/client/rpcWireError.helper.js +71 -0
- package/dist/rpc/constants/index.d.ts +6 -1
- package/dist/rpc/constants/index.js +6 -1
- package/dist/rpc/constants/rpcConstants.core.d.ts +11 -0
- package/dist/rpc/constants/rpcConstants.core.js +11 -0
- package/dist/rpc/context/rpcContext.type.d.ts +7 -0
- package/dist/rpc/dispatcher/rpcDispatcher.core.js +14 -1
- package/dist/rpc/reliability/cancellation/rpcAbort.helper.d.ts +17 -0
- package/dist/rpc/reliability/cancellation/rpcAbort.helper.js +40 -0
- package/dist/rpc/reliability/retry/rpcRetry.helper.js +3 -1
- package/dist/rpc/reliability/timeout/rpcTimeout.helper.d.ts +5 -3
- package/dist/rpc/reliability/timeout/rpcTimeout.helper.js +5 -4
- package/dist/rpc/server/index.d.ts +6 -0
- package/dist/rpc/server/index.js +5 -0
- package/dist/rpc/server/rpcBaseErrorMapping.helper.d.ts +16 -0
- package/dist/rpc/server/rpcBaseErrorMapping.helper.js +57 -0
- package/dist/rpc/server/rpcErrorMapping.helper.d.ts +35 -0
- package/dist/rpc/server/rpcErrorMapping.helper.js +101 -0
- package/dist/rpc/server/rpcServer.core.d.ts +0 -7
- package/dist/rpc/server/rpcServer.core.js +7 -91
- package/dist/rpc/transport/codec/index.d.ts +10 -0
- package/dist/rpc/transport/codec/index.js +8 -0
- package/dist/rpc/transport/codec/rpcBody.helper.d.ts +36 -0
- package/dist/rpc/transport/codec/rpcBody.helper.js +64 -0
- package/dist/rpc/transport/codec/rpcCodec.helper.d.ts +41 -0
- package/dist/rpc/transport/codec/rpcCodec.helper.js +40 -0
- package/dist/rpc/transport/http/index.d.ts +12 -0
- package/dist/rpc/transport/http/index.js +10 -0
- package/dist/rpc/transport/http/rpcFetchHandler.core.d.ts +36 -0
- package/dist/rpc/transport/http/rpcFetchHandler.core.js +97 -0
- package/dist/rpc/transport/http/rpcHttpExchange.helper.d.ts +27 -0
- package/dist/rpc/transport/http/rpcHttpExchange.helper.js +78 -0
- package/dist/rpc/transport/http/rpcHttpStatus.helper.d.ts +16 -0
- package/dist/rpc/transport/http/rpcHttpStatus.helper.js +37 -0
- package/dist/rpc/transport/http/rpcHttpTransport.core.d.ts +36 -0
- package/dist/rpc/transport/http/rpcHttpTransport.core.js +45 -0
- package/dist/rpc/transport/index.d.ts +12 -0
- package/dist/rpc/transport/index.js +9 -1
- package/dist/rpc/transport/memory/index.d.ts +7 -0
- package/dist/rpc/transport/memory/index.js +6 -0
- package/dist/rpc/transport/memory/rpcMemoryTransport.core.d.ts +42 -0
- package/dist/rpc/transport/memory/rpcMemoryTransport.core.js +73 -0
- package/dist/rpc/validation/rpcUnsafeKey.helper.d.ts +18 -0
- package/dist/rpc/validation/rpcUnsafeKey.helper.js +60 -0
- package/dist/rpc/validation/rpcValidation.core.d.ts +11 -2
- package/dist/rpc/validation/rpcValidation.core.js +11 -2
- 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
|
|
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`.
|
|
23
|
-
|
|
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
|
-
|
|
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`
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
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,
|
|
117
|
+
import { RPCClient, createRPCMemoryTransport } from "@zudojs/rpc";
|
|
101
118
|
|
|
102
|
-
const
|
|
103
|
-
|
|
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
|
-
(`
|
|
114
|
-
`
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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 {
|
|
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
|
-
*
|
|
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
|
|
39
|
-
export
|
|
40
|
-
export
|
|
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 {
|
|
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
|
-
*
|
|
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
|
-
|
|
33
|
-
|
|
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
|
package/dist/rpc/client/index.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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));
|