@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
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/rpc/reliability/cancellation
|
|
3
|
+
*
|
|
4
|
+
* Abort helpers used by transports to turn a signal into a typed error.
|
|
5
|
+
*/
|
|
6
|
+
import type { RPCError } from "../../errors/rpc.errors.js";
|
|
7
|
+
/**
|
|
8
|
+
* Converts an aborted signal's reason into a typed RPC error: an RPC
|
|
9
|
+
* error reason (such as the client's `RPCTimeoutError`) passes through,
|
|
10
|
+
* anything else becomes an `RPCCancelledError`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function abortReasonToRPCError(signal: AbortSignal, procedureName?: string): RPCError;
|
|
13
|
+
/**
|
|
14
|
+
* Settles with `promise`, or rejects as soon as `signal` aborts.
|
|
15
|
+
*/
|
|
16
|
+
export declare function raceAbort<T>(promise: Promise<T>, signal: AbortSignal | undefined, procedureName?: string): Promise<T>;
|
|
17
|
+
//# sourceMappingURL=rpcAbort.helper.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/rpc/reliability/cancellation
|
|
3
|
+
*
|
|
4
|
+
* Abort helpers used by transports to turn a signal into a typed error.
|
|
5
|
+
*/
|
|
6
|
+
import { isRPCError, RPCCancelledError } from "../../errors/rpc.errors.js";
|
|
7
|
+
/**
|
|
8
|
+
* Converts an aborted signal's reason into a typed RPC error: an RPC
|
|
9
|
+
* error reason (such as the client's `RPCTimeoutError`) passes through,
|
|
10
|
+
* anything else becomes an `RPCCancelledError`.
|
|
11
|
+
*/
|
|
12
|
+
export function abortReasonToRPCError(signal, procedureName) {
|
|
13
|
+
const reason = signal.reason;
|
|
14
|
+
return isRPCError(reason)
|
|
15
|
+
? reason
|
|
16
|
+
: new RPCCancelledError("Call cancelled by caller.", procedureName);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Settles with `promise`, or rejects as soon as `signal` aborts.
|
|
20
|
+
*/
|
|
21
|
+
export function raceAbort(promise, signal, procedureName) {
|
|
22
|
+
if (signal === undefined) {
|
|
23
|
+
return promise;
|
|
24
|
+
}
|
|
25
|
+
if (signal.aborted) {
|
|
26
|
+
return Promise.reject(abortReasonToRPCError(signal, procedureName));
|
|
27
|
+
}
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const onAbort = () => reject(abortReasonToRPCError(signal, procedureName));
|
|
30
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
31
|
+
promise.then((value) => {
|
|
32
|
+
signal.removeEventListener("abort", onAbort);
|
|
33
|
+
resolve(value);
|
|
34
|
+
}, (error) => {
|
|
35
|
+
signal.removeEventListener("abort", onAbort);
|
|
36
|
+
reject(error);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=rpcAbort.helper.js.map
|
|
@@ -54,6 +54,9 @@ export function calculateRetryDelay(attempt, options) {
|
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
56
|
* Sleeps for a duration, rejecting early if the signal aborts.
|
|
57
|
+
*
|
|
58
|
+
* The timer stays ref'd: a pending retry is work the caller is awaiting,
|
|
59
|
+
* and an unref'd backoff let a script exit before the next attempt.
|
|
57
60
|
*/
|
|
58
61
|
function sleep(ms, signal) {
|
|
59
62
|
if (ms <= 0) {
|
|
@@ -64,7 +67,6 @@ function sleep(ms, signal) {
|
|
|
64
67
|
signal?.removeEventListener("abort", onAbort);
|
|
65
68
|
resolve();
|
|
66
69
|
}, ms);
|
|
67
|
-
timer.unref?.();
|
|
68
70
|
function onAbort() {
|
|
69
71
|
clearTimeout(timer);
|
|
70
72
|
reject(signal?.reason instanceof Error
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
/**
|
|
7
7
|
* Creates a timeout promise that rejects after the given duration.
|
|
8
8
|
*
|
|
9
|
-
* The returned `cancel` clears the underlying timer
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* The returned `cancel` clears the underlying timer, and every caller in
|
|
10
|
+
* this package cancels it as soon as the guarded work settles. The timer
|
|
11
|
+
* is deliberately *not* unref'd: it guards pending work, and in a script
|
|
12
|
+
* with no other handle an unref'd deadline let Node exit (code 13,
|
|
13
|
+
* "unsettled top-level await") before the timeout could reject.
|
|
12
14
|
*/
|
|
13
15
|
export declare function createTimeout(duration: number, procedureName?: string): {
|
|
14
16
|
readonly promise: Promise<never>;
|
|
@@ -8,9 +8,11 @@ import { MAX_TIMER_DELAY } from "../../constants/rpcConstants.core.js";
|
|
|
8
8
|
/**
|
|
9
9
|
* Creates a timeout promise that rejects after the given duration.
|
|
10
10
|
*
|
|
11
|
-
* The returned `cancel` clears the underlying timer
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The returned `cancel` clears the underlying timer, and every caller in
|
|
12
|
+
* this package cancels it as soon as the guarded work settles. The timer
|
|
13
|
+
* is deliberately *not* unref'd: it guards pending work, and in a script
|
|
14
|
+
* with no other handle an unref'd deadline let Node exit (code 13,
|
|
15
|
+
* "unsettled top-level await") before the timeout could reject.
|
|
14
16
|
*/
|
|
15
17
|
export function createTimeout(duration, procedureName) {
|
|
16
18
|
const delay = Math.min(Math.max(0, duration), MAX_TIMER_DELAY);
|
|
@@ -19,7 +21,6 @@ export function createTimeout(duration, procedureName) {
|
|
|
19
21
|
timer = setTimeout(() => {
|
|
20
22
|
reject(new RPCTimeoutError(duration, procedureName));
|
|
21
23
|
}, delay);
|
|
22
|
-
timer.unref?.();
|
|
23
24
|
});
|
|
24
25
|
return {
|
|
25
26
|
promise,
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC server: validates incoming frames, dispatches them to registered
|
|
3
|
+
* procedures, and maps every failure onto a wire-safe error payload.
|
|
4
|
+
*/
|
|
1
5
|
export type { RPCServerOptions } from "./rpcServer.core.js";
|
|
2
6
|
export { RPCServer } from "./rpcServer.core.js";
|
|
7
|
+
export type { RPCMappedError } from "./rpcErrorMapping.helper.js";
|
|
8
|
+
export { mapRPCError } from "./rpcErrorMapping.helper.js";
|
|
3
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/rpc/server/index.js
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC server: validates incoming frames, dispatches them to registered
|
|
3
|
+
* procedures, and maps every failure onto a wire-safe error payload.
|
|
4
|
+
*/
|
|
1
5
|
export { RPCServer } from "./rpcServer.core.js";
|
|
6
|
+
export { mapRPCError } from "./rpcErrorMapping.helper.js";
|
|
2
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire mapping for exposable `@zudojs/errors` errors that are not RPC
|
|
3
|
+
* errors — a `NotFoundError` or `ConflictError` thrown by domain code a
|
|
4
|
+
* procedure calls.
|
|
5
|
+
*/
|
|
6
|
+
import type { RPCErrorPayload } from "../types/rpcResponse.type.js";
|
|
7
|
+
/**
|
|
8
|
+
* Maps a `@zudojs/errors` error built with `expose: true` onto a wire
|
|
9
|
+
* payload: its status picks the RPC code (404 → `RPC_NOT_FOUND`, 409 →
|
|
10
|
+
* `RPC_CONFLICT`, 401, 403, 422, 429 …; a `ValidationError` is always
|
|
11
|
+
* `RPC_VALIDATION_ERROR`), and an unlisted status keeps the error's own
|
|
12
|
+
* code. The message is the error's own, which `expose: true` declares
|
|
13
|
+
* safe. Returns `undefined` for anything else, which stays internal.
|
|
14
|
+
*/
|
|
15
|
+
export declare function mapExposedBaseError(error: unknown): RPCErrorPayload | undefined;
|
|
16
|
+
//# sourceMappingURL=rpcBaseErrorMapping.helper.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire mapping for exposable `@zudojs/errors` errors that are not RPC
|
|
3
|
+
* errors — a `NotFoundError` or `ConflictError` thrown by domain code a
|
|
4
|
+
* procedure calls.
|
|
5
|
+
*/
|
|
6
|
+
import { isBaseError, RateLimitError, ValidationError } from "@zudojs/errors";
|
|
7
|
+
/** Wire code for each HTTP-style status an exposable error can carry. */
|
|
8
|
+
const STATUS_WIRE_CODES = new Map([
|
|
9
|
+
[400, "RPC_VALIDATION_ERROR"],
|
|
10
|
+
[401, "RPC_UNAUTHENTICATED"],
|
|
11
|
+
[403, "RPC_FORBIDDEN"],
|
|
12
|
+
[404, "RPC_NOT_FOUND"],
|
|
13
|
+
[408, "RPC_TIMEOUT"],
|
|
14
|
+
[409, "RPC_CONFLICT"],
|
|
15
|
+
[422, "RPC_VALIDATION_ERROR"],
|
|
16
|
+
[429, "RPC_RATE_LIMITED"],
|
|
17
|
+
[503, "RPC_UNAVAILABLE"],
|
|
18
|
+
[504, "RPC_TIMEOUT"],
|
|
19
|
+
]);
|
|
20
|
+
function validationDetails(error) {
|
|
21
|
+
if (error.issues.length === 0) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
// The received `value` is dropped: a response never echoes caller input.
|
|
25
|
+
return error.issues.map((issue) => ({
|
|
26
|
+
path: (issue.path ?? (issue.field !== undefined ? [issue.field] : [])).map(String).join("."),
|
|
27
|
+
code: issue.code ?? "invalid",
|
|
28
|
+
message: issue.message,
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Maps a `@zudojs/errors` error built with `expose: true` onto a wire
|
|
33
|
+
* payload: its status picks the RPC code (404 → `RPC_NOT_FOUND`, 409 →
|
|
34
|
+
* `RPC_CONFLICT`, 401, 403, 422, 429 …; a `ValidationError` is always
|
|
35
|
+
* `RPC_VALIDATION_ERROR`), and an unlisted status keeps the error's own
|
|
36
|
+
* code. The message is the error's own, which `expose: true` declares
|
|
37
|
+
* safe. Returns `undefined` for anything else, which stays internal.
|
|
38
|
+
*/
|
|
39
|
+
export function mapExposedBaseError(error) {
|
|
40
|
+
if (!isBaseError(error) || error.expose !== true) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const code = error instanceof ValidationError
|
|
44
|
+
? "RPC_VALIDATION_ERROR"
|
|
45
|
+
: (STATUS_WIRE_CODES.get(error.statusCode) ?? String(error.code));
|
|
46
|
+
const details = error instanceof ValidationError
|
|
47
|
+
? validationDetails(error)
|
|
48
|
+
: error instanceof RateLimitError && error.retryAfterSeconds !== undefined
|
|
49
|
+
? { retryAfter: error.retryAfterSeconds }
|
|
50
|
+
: undefined;
|
|
51
|
+
return {
|
|
52
|
+
code,
|
|
53
|
+
message: error.message,
|
|
54
|
+
...(details !== undefined ? { details } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=rpcBaseErrorMapping.helper.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { RPCErrorPayload } from "../types/rpcResponse.type.js";
|
|
2
|
+
/**
|
|
3
|
+
* An error mapped onto the wire.
|
|
4
|
+
*
|
|
5
|
+
* `internal` is `true` when the original error carried server detail that
|
|
6
|
+
* was withheld from `payload`; transports hand the original error to their
|
|
7
|
+
* `onInternalError` hook in that case so the failure stays diagnosable.
|
|
8
|
+
*/
|
|
9
|
+
export interface RPCMappedError {
|
|
10
|
+
readonly payload: RPCErrorPayload;
|
|
11
|
+
readonly internal: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Maps any thrown value onto a wire-safe error payload.
|
|
15
|
+
*
|
|
16
|
+
* Typed RPC errors keep their wire code (and, for validation and rate
|
|
17
|
+
* limiting, their `details`). A `@zudojs/errors` error built with
|
|
18
|
+
* `expose: true` — `NotFoundError`, `ConflictError`, `ValidationError` … —
|
|
19
|
+
* travels with its own message under the matching code (`RPC_NOT_FOUND`,
|
|
20
|
+
* `RPC_CONFLICT`, `RPC_VALIDATION_ERROR` …). Anything not built to be
|
|
21
|
+
* exposed — an `RPCInternalError`, a non-exposed custom `RPCError` or
|
|
22
|
+
* `BaseError`, or any other error — is answered with
|
|
23
|
+
* {@link INTERNAL_ERROR_MESSAGE}, so exception text, stack traces and
|
|
24
|
+
* causes never reach the remote side.
|
|
25
|
+
*/
|
|
26
|
+
export declare function mapRPCError(error: unknown): RPCMappedError;
|
|
27
|
+
/**
|
|
28
|
+
* The wire code of a typed RPC error — `RPC_TIMEOUT` for an
|
|
29
|
+
* `RPCTimeoutError`, and so on — or `undefined` for anything without one.
|
|
30
|
+
* The client stamps it on every error it rejects with, so a caller
|
|
31
|
+
* compares `error.code` against the same strings whether the failure was
|
|
32
|
+
* reported by the server or raised locally (its own deadline, a cancel).
|
|
33
|
+
*/
|
|
34
|
+
export declare function rpcWireCodeOf(error: unknown): string | undefined;
|
|
35
|
+
//# sourceMappingURL=rpcErrorMapping.helper.d.ts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { isRPCError, RPCAuthenticationError, RPCCancelledError, RPCDeadlineExceededError, RPCDeserializationError, RPCForbiddenError, RPCInternalError, RPCInvalidRequestError, RPCProcedureNotFoundError, RPCRateLimitedError, RPCSerializationError, RPCTimeoutError, RPCTransportError, RPCUnavailableError, RPCValidationError, } from "../errors/rpc.errors.js";
|
|
2
|
+
import { INTERNAL_ERROR_MESSAGE } from "../constants/rpcConstants.core.js";
|
|
3
|
+
import { mapExposedBaseError } from "./rpcBaseErrorMapping.helper.js";
|
|
4
|
+
/**
|
|
5
|
+
* Wire codes for the error types the server maps.
|
|
6
|
+
*
|
|
7
|
+
* Ordered most specific first; every entry is a type a caller can act
|
|
8
|
+
* on, which is why they must not be collapsed into a generic internal
|
|
9
|
+
* error. The third element marks a typed error whose message is server
|
|
10
|
+
* detail: its code travels, its message goes to `onInternalError`.
|
|
11
|
+
*/
|
|
12
|
+
const ERROR_CODES = [
|
|
13
|
+
[RPCProcedureNotFoundError, "RPC_PROCEDURE_NOT_FOUND"],
|
|
14
|
+
[RPCValidationError, "RPC_VALIDATION_ERROR"],
|
|
15
|
+
[RPCInvalidRequestError, "RPC_INVALID_REQUEST"],
|
|
16
|
+
[RPCAuthenticationError, "RPC_UNAUTHENTICATED"],
|
|
17
|
+
[RPCForbiddenError, "RPC_FORBIDDEN"],
|
|
18
|
+
[RPCRateLimitedError, "RPC_RATE_LIMITED"],
|
|
19
|
+
[RPCDeadlineExceededError, "RPC_DEADLINE_EXCEEDED"],
|
|
20
|
+
[RPCTimeoutError, "RPC_TIMEOUT"],
|
|
21
|
+
[RPCCancelledError, "RPC_CANCELLED"],
|
|
22
|
+
[RPCUnavailableError, "RPC_UNAVAILABLE"],
|
|
23
|
+
[RPCSerializationError, "RPC_SERIALIZATION_ERROR", true],
|
|
24
|
+
[RPCDeserializationError, "RPC_DESERIALIZATION_ERROR"],
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Maps any thrown value onto a wire-safe error payload.
|
|
28
|
+
*
|
|
29
|
+
* Typed RPC errors keep their wire code (and, for validation and rate
|
|
30
|
+
* limiting, their `details`). A `@zudojs/errors` error built with
|
|
31
|
+
* `expose: true` — `NotFoundError`, `ConflictError`, `ValidationError` … —
|
|
32
|
+
* travels with its own message under the matching code (`RPC_NOT_FOUND`,
|
|
33
|
+
* `RPC_CONFLICT`, `RPC_VALIDATION_ERROR` …). Anything not built to be
|
|
34
|
+
* exposed — an `RPCInternalError`, a non-exposed custom `RPCError` or
|
|
35
|
+
* `BaseError`, or any other error — is answered with
|
|
36
|
+
* {@link INTERNAL_ERROR_MESSAGE}, so exception text, stack traces and
|
|
37
|
+
* causes never reach the remote side.
|
|
38
|
+
*/
|
|
39
|
+
export function mapRPCError(error) {
|
|
40
|
+
if (error instanceof RPCInternalError || !(error instanceof Error)) {
|
|
41
|
+
return internalFailure("RPC_INTERNAL_ERROR");
|
|
42
|
+
}
|
|
43
|
+
for (const [type, code, internal] of ERROR_CODES) {
|
|
44
|
+
if (error instanceof type) {
|
|
45
|
+
return internal === true
|
|
46
|
+
? internalFailure(code)
|
|
47
|
+
: exposed(code, error.message, detailsOf(error));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (isRPCError(error)) {
|
|
51
|
+
return error.expose === false
|
|
52
|
+
? internalFailure(error.code)
|
|
53
|
+
: exposed(error.code, error.message, undefined);
|
|
54
|
+
}
|
|
55
|
+
const base = mapExposedBaseError(error);
|
|
56
|
+
if (base !== undefined) {
|
|
57
|
+
return { payload: base, internal: false };
|
|
58
|
+
}
|
|
59
|
+
return internalFailure("RPC_INTERNAL_ERROR");
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The wire code of a typed RPC error — `RPC_TIMEOUT` for an
|
|
63
|
+
* `RPCTimeoutError`, and so on — or `undefined` for anything without one.
|
|
64
|
+
* The client stamps it on every error it rejects with, so a caller
|
|
65
|
+
* compares `error.code` against the same strings whether the failure was
|
|
66
|
+
* reported by the server or raised locally (its own deadline, a cancel).
|
|
67
|
+
*/
|
|
68
|
+
export function rpcWireCodeOf(error) {
|
|
69
|
+
if (error instanceof RPCInternalError) {
|
|
70
|
+
return "RPC_INTERNAL_ERROR";
|
|
71
|
+
}
|
|
72
|
+
if (error instanceof RPCTransportError) {
|
|
73
|
+
return "RPC_TRANSPORT_ERROR";
|
|
74
|
+
}
|
|
75
|
+
return ERROR_CODES.find(([type]) => error instanceof type)?.[1];
|
|
76
|
+
}
|
|
77
|
+
function detailsOf(error) {
|
|
78
|
+
if (error instanceof RPCValidationError &&
|
|
79
|
+
error.issues !== undefined &&
|
|
80
|
+
error.issues.length > 0) {
|
|
81
|
+
return error.issues;
|
|
82
|
+
}
|
|
83
|
+
if (error instanceof RPCRateLimitedError && error.retryAfter !== undefined) {
|
|
84
|
+
return { retryAfter: error.retryAfter };
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
function exposed(code, message, details) {
|
|
89
|
+
return {
|
|
90
|
+
payload: {
|
|
91
|
+
code,
|
|
92
|
+
message,
|
|
93
|
+
...(details !== undefined ? { details } : {}),
|
|
94
|
+
},
|
|
95
|
+
internal: false,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function internalFailure(code) {
|
|
99
|
+
return { payload: { code, message: INTERNAL_ERROR_MESSAGE }, internal: true };
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=rpcErrorMapping.helper.js.map
|
|
@@ -61,12 +61,5 @@ export declare class RPCServer {
|
|
|
61
61
|
* Returns the procedure registry.
|
|
62
62
|
*/
|
|
63
63
|
getRegistry(): RPCProcedureRegistry;
|
|
64
|
-
/**
|
|
65
|
-
* Maps a known error onto a wire payload.
|
|
66
|
-
*
|
|
67
|
-
* Returns `undefined` for anything unrecognised, which the caller
|
|
68
|
-
* answers with a generic internal error.
|
|
69
|
-
*/
|
|
70
|
-
private mapError;
|
|
71
64
|
}
|
|
72
65
|
//# sourceMappingURL=rpcServer.core.d.ts.map
|
|
@@ -2,33 +2,9 @@ import { RPCProcedureRegistry } from "../procedure/rpcProcedureRegistry.core.js"
|
|
|
2
2
|
import { RPCMiddlewareStack } from "../middleware/rpcMiddleware.core.js";
|
|
3
3
|
import { RPCDispatcher } from "../dispatcher/rpcDispatcher.core.js";
|
|
4
4
|
import { createRPCErrorResponse } from "../types/rpcResponse.type.js";
|
|
5
|
-
import {
|
|
6
|
-
import { INTERNAL_ERROR_MESSAGE, MAX_RPC_REQUEST_ID_LENGTH, } from "../constants/rpcConstants.core.js";
|
|
5
|
+
import { MAX_RPC_REQUEST_ID_LENGTH } from "../constants/rpcConstants.core.js";
|
|
7
6
|
import { assertValidRequest } from "../validation/rpcValidation.core.js";
|
|
8
|
-
|
|
9
|
-
* Wire codes for the error types the server maps.
|
|
10
|
-
*
|
|
11
|
-
* Ordered most specific first; every entry is a type a caller can act
|
|
12
|
-
* on, which is why they must not be collapsed into a generic internal
|
|
13
|
-
* error.
|
|
14
|
-
*/
|
|
15
|
-
const ERROR_CODES = [
|
|
16
|
-
[RPCProcedureNotFoundError, "RPC_PROCEDURE_NOT_FOUND"],
|
|
17
|
-
[RPCValidationError, "RPC_VALIDATION_ERROR"],
|
|
18
|
-
[RPCInvalidRequestError, "RPC_INVALID_REQUEST"],
|
|
19
|
-
[RPCAuthenticationError, "RPC_UNAUTHENTICATED"],
|
|
20
|
-
[RPCForbiddenError, "RPC_FORBIDDEN"],
|
|
21
|
-
[RPCRateLimitedError, "RPC_RATE_LIMITED"],
|
|
22
|
-
[RPCDeadlineExceededError, "RPC_DEADLINE_EXCEEDED"],
|
|
23
|
-
[RPCTimeoutError, "RPC_TIMEOUT"],
|
|
24
|
-
[RPCCancelledError, "RPC_CANCELLED"],
|
|
25
|
-
[RPCUnavailableError, "RPC_UNAVAILABLE"],
|
|
26
|
-
// A serialization failure is a server fault (500, `expose: false`) whose
|
|
27
|
-
// message names what could not be encoded; the code travels, the
|
|
28
|
-
// message goes to `onInternalError`.
|
|
29
|
-
[RPCSerializationError, "RPC_SERIALIZATION_ERROR", true],
|
|
30
|
-
[RPCDeserializationError, "RPC_DESERIALIZATION_ERROR"],
|
|
31
|
-
];
|
|
7
|
+
import { mapRPCError } from "./rpcErrorMapping.helper.js";
|
|
32
8
|
/**
|
|
33
9
|
* RPC server that receives and dispatches requests.
|
|
34
10
|
*/
|
|
@@ -91,29 +67,14 @@ export class RPCServer {
|
|
|
91
67
|
return await this.dispatcher.dispatch(frame, trusted);
|
|
92
68
|
}
|
|
93
69
|
catch (error) {
|
|
94
|
-
const mapped =
|
|
95
|
-
if (mapped === undefined) {
|
|
96
|
-
this.options.onInternalError?.(error, requestId);
|
|
97
|
-
return createRPCErrorResponse(requestId, {
|
|
98
|
-
code: "RPC_INTERNAL_ERROR",
|
|
99
|
-
message: INTERNAL_ERROR_MESSAGE,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
70
|
+
const mapped = mapRPCError(error);
|
|
102
71
|
if (mapped.internal) {
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
72
|
+
// Internal detail goes to the log, never the wire: the caller is
|
|
73
|
+
// an untrusted peer and exception text can name hosts, paths,
|
|
74
|
+
// credentials or queries.
|
|
106
75
|
this.options.onInternalError?.(error, requestId);
|
|
107
|
-
return createRPCErrorResponse(requestId, {
|
|
108
|
-
code: mapped.code,
|
|
109
|
-
message: INTERNAL_ERROR_MESSAGE,
|
|
110
|
-
});
|
|
111
76
|
}
|
|
112
|
-
return createRPCErrorResponse(requestId,
|
|
113
|
-
code: mapped.code,
|
|
114
|
-
message: mapped.message,
|
|
115
|
-
...(mapped.details !== undefined ? { details: mapped.details } : {}),
|
|
116
|
-
});
|
|
77
|
+
return createRPCErrorResponse(requestId, mapped.payload);
|
|
117
78
|
}
|
|
118
79
|
}
|
|
119
80
|
/**
|
|
@@ -122,50 +83,5 @@ export class RPCServer {
|
|
|
122
83
|
getRegistry() {
|
|
123
84
|
return this.registry;
|
|
124
85
|
}
|
|
125
|
-
/**
|
|
126
|
-
* Maps a known error onto a wire payload.
|
|
127
|
-
*
|
|
128
|
-
* Returns `undefined` for anything unrecognised, which the caller
|
|
129
|
-
* answers with a generic internal error.
|
|
130
|
-
*/
|
|
131
|
-
mapError(error) {
|
|
132
|
-
// An RPCInternalError *is* the generic internal failure: it carries
|
|
133
|
-
// `expose: false` and a message written for the log. Mapping it like
|
|
134
|
-
// any other RPCError put that message on the wire.
|
|
135
|
-
if (error instanceof RPCInternalError) {
|
|
136
|
-
return undefined;
|
|
137
|
-
}
|
|
138
|
-
for (const [type, code, internal] of ERROR_CODES) {
|
|
139
|
-
if (error instanceof type) {
|
|
140
|
-
const payload = {
|
|
141
|
-
code,
|
|
142
|
-
message: error.message,
|
|
143
|
-
...(internal ? { internal: true } : {}),
|
|
144
|
-
};
|
|
145
|
-
if (error instanceof RPCValidationError &&
|
|
146
|
-
error.issues !== undefined &&
|
|
147
|
-
error.issues.length > 0) {
|
|
148
|
-
payload.details = error.issues;
|
|
149
|
-
}
|
|
150
|
-
if (error instanceof RPCRateLimitedError &&
|
|
151
|
-
error.retryAfter !== undefined) {
|
|
152
|
-
payload.details = { retryAfter: error.retryAfter };
|
|
153
|
-
}
|
|
154
|
-
return payload;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
// A custom RPCError subclass is still a deliberate, caller-facing
|
|
158
|
-
// error; honour its own code rather than hiding it. Its message only
|
|
159
|
-
// travels when the error was built to be exposed — `RPCError`
|
|
160
|
-
// defaults to `expose: false`.
|
|
161
|
-
if (isRPCError(error)) {
|
|
162
|
-
return {
|
|
163
|
-
code: error.code,
|
|
164
|
-
message: error.message,
|
|
165
|
-
internal: error.expose === false,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
return undefined;
|
|
169
|
-
}
|
|
170
86
|
}
|
|
171
87
|
//# sourceMappingURL=rpcServer.core.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire encoding shared by the built-in transports: the frame serializer
|
|
3
|
+
* (`@zudojs/serialization` JSON with size and depth limits), a response
|
|
4
|
+
* frame shape guard, and a bounded Fetch API body reader.
|
|
5
|
+
*/
|
|
6
|
+
export type { RPCFrameSerializer, RPCJsonSerializerOptions, } from "./rpcCodec.helper.js";
|
|
7
|
+
export { createRPCJsonSerializer, isRPCResponseFrame } from "./rpcCodec.helper.js";
|
|
8
|
+
export type { RPCBodyReadResult, RPCBodySource } from "./rpcBody.helper.js";
|
|
9
|
+
export { readBoundedBody } from "./rpcBody.helper.js";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire encoding shared by the built-in transports: the frame serializer
|
|
3
|
+
* (`@zudojs/serialization` JSON with size and depth limits), a response
|
|
4
|
+
* frame shape guard, and a bounded Fetch API body reader.
|
|
5
|
+
*/
|
|
6
|
+
export { createRPCJsonSerializer, isRPCResponseFrame } from "./rpcCodec.helper.js";
|
|
7
|
+
export { readBoundedBody } from "./rpcBody.helper.js";
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outcome of {@link readBoundedBody}.
|
|
3
|
+
*
|
|
4
|
+
* `too-large` means the body exceeded the limit (by its declared
|
|
5
|
+
* `Content-Length` or while streaming); `unreadable` means the stream
|
|
6
|
+
* failed, was aborted, or was not valid UTF-8.
|
|
7
|
+
*/
|
|
8
|
+
export type RPCBodyReadResult = {
|
|
9
|
+
readonly ok: true;
|
|
10
|
+
readonly text: string;
|
|
11
|
+
} | {
|
|
12
|
+
readonly ok: false;
|
|
13
|
+
readonly reason: "too-large" | "unreadable";
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The part of a Fetch API `Request` or `Response` a body read needs.
|
|
17
|
+
*/
|
|
18
|
+
export interface RPCBodySource {
|
|
19
|
+
readonly body: ReadableStream<Uint8Array> | null;
|
|
20
|
+
readonly headers: Headers;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reads a Fetch API body as UTF-8 text without ever buffering more than
|
|
24
|
+
* `maxBytes`.
|
|
25
|
+
*
|
|
26
|
+
* `request.text()` buffers the whole body before anything can check its
|
|
27
|
+
* size, so a peer could make the server hold an arbitrarily large body in
|
|
28
|
+
* memory. This checks the declared `Content-Length` first, then counts
|
|
29
|
+
* bytes as they stream and cancels the stream the moment the limit is
|
|
30
|
+
* crossed. Transport-neutral: `@zudojs/api`'s fetch binding reads request
|
|
31
|
+
* bodies through it too.
|
|
32
|
+
*
|
|
33
|
+
* @param maxBytes Limit in bytes; `0` or less disables the limit.
|
|
34
|
+
*/
|
|
35
|
+
export declare function readBoundedBody(source: RPCBodySource, maxBytes: number): Promise<RPCBodyReadResult>;
|
|
36
|
+
//# sourceMappingURL=rpcBody.helper.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a Fetch API body as UTF-8 text without ever buffering more than
|
|
3
|
+
* `maxBytes`.
|
|
4
|
+
*
|
|
5
|
+
* `request.text()` buffers the whole body before anything can check its
|
|
6
|
+
* size, so a peer could make the server hold an arbitrarily large body in
|
|
7
|
+
* memory. This checks the declared `Content-Length` first, then counts
|
|
8
|
+
* bytes as they stream and cancels the stream the moment the limit is
|
|
9
|
+
* crossed. Transport-neutral: `@zudojs/api`'s fetch binding reads request
|
|
10
|
+
* bodies through it too.
|
|
11
|
+
*
|
|
12
|
+
* @param maxBytes Limit in bytes; `0` or less disables the limit.
|
|
13
|
+
*/
|
|
14
|
+
export async function readBoundedBody(source, maxBytes) {
|
|
15
|
+
const limited = maxBytes > 0;
|
|
16
|
+
const declared = Number(source.headers.get("content-length") ?? Number.NaN);
|
|
17
|
+
if (limited && Number.isFinite(declared) && declared > maxBytes) {
|
|
18
|
+
await source.body?.cancel().catch(() => undefined);
|
|
19
|
+
return { ok: false, reason: "too-large" };
|
|
20
|
+
}
|
|
21
|
+
if (source.body === null) {
|
|
22
|
+
return { ok: true, text: "" };
|
|
23
|
+
}
|
|
24
|
+
const reader = source.body.getReader();
|
|
25
|
+
const chunks = [];
|
|
26
|
+
let total = 0;
|
|
27
|
+
try {
|
|
28
|
+
for (;;) {
|
|
29
|
+
const { done, value } = await reader.read();
|
|
30
|
+
if (done) {
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
total += value.byteLength;
|
|
34
|
+
if (limited && total > maxBytes) {
|
|
35
|
+
await reader.cancel().catch(() => undefined);
|
|
36
|
+
return { ok: false, reason: "too-large" };
|
|
37
|
+
}
|
|
38
|
+
chunks.push(value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return { ok: false, reason: "unreadable" };
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
reader.releaseLock();
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(concat(chunks, total));
|
|
49
|
+
return { ok: true, text };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return { ok: false, reason: "unreadable" };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function concat(chunks, total) {
|
|
56
|
+
const joined = new Uint8Array(total);
|
|
57
|
+
let offset = 0;
|
|
58
|
+
for (const chunk of chunks) {
|
|
59
|
+
joined.set(chunk, offset);
|
|
60
|
+
offset += chunk.byteLength;
|
|
61
|
+
}
|
|
62
|
+
return joined;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=rpcBody.helper.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Serializer } from "@zudojs/serialization";
|
|
2
|
+
import type { RPCResponse } from "../../types/rpcResponse.type.js";
|
|
3
|
+
/**
|
|
4
|
+
* Serializer that turns RPC frames into wire text and back.
|
|
5
|
+
*
|
|
6
|
+
* Any `@zudojs/serialization` string serializer fits. Both ends of a
|
|
7
|
+
* connection must use compatible serializers: a server created with
|
|
8
|
+
* `preserveTypes: true` should be called by a client that uses it too.
|
|
9
|
+
*/
|
|
10
|
+
export type RPCFrameSerializer = Serializer<unknown, string>;
|
|
11
|
+
/**
|
|
12
|
+
* Options for {@link createRPCJsonSerializer}.
|
|
13
|
+
*/
|
|
14
|
+
export interface RPCJsonSerializerOptions {
|
|
15
|
+
/** Largest accepted encoded frame, in bytes. */
|
|
16
|
+
readonly maxSize?: number;
|
|
17
|
+
/** Deepest accepted nesting. Defaults to {@link MAX_RPC_FRAME_DEPTH}. */
|
|
18
|
+
readonly maxDepth?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Tag `Date`, `BigInt`, `Map`, `Set` and friends so they survive the
|
|
21
|
+
* trip. Defaults to `false` (plain JSON), which any HTTP client can
|
|
22
|
+
* speak.
|
|
23
|
+
*/
|
|
24
|
+
readonly preserveTypes?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Creates the default frame serializer: `@zudojs/serialization` JSON with
|
|
28
|
+
* size and depth limits, so decoding an untrusted frame is bounded.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createRPCJsonSerializer(options?: RPCJsonSerializerOptions): RPCFrameSerializer;
|
|
31
|
+
/**
|
|
32
|
+
* Determines whether a decoded value has the shape of an
|
|
33
|
+
* {@link RPCResponse}: a string `id`, a boolean `success`, and — when
|
|
34
|
+
* present — an `error` with string `code` and `message`.
|
|
35
|
+
*
|
|
36
|
+
* Transports check this before handing a peer's reply to the client, so
|
|
37
|
+
* a proxy's HTML error page or a truncated body is reported as a
|
|
38
|
+
* transport failure instead of being read as a result.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isRPCResponseFrame(value: unknown): value is RPCResponse;
|
|
41
|
+
//# sourceMappingURL=rpcCodec.helper.d.ts.map
|