@zudojs/rpc 1.3.0 → 1.4.1

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 +131 -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 +11 -0
  13. package/dist/rpc/constants/rpcConstants.core.js +11 -0
  14. package/dist/rpc/context/rpcContext.type.d.ts +7 -0
  15. package/dist/rpc/dispatcher/rpcDispatcher.core.js +14 -1
  16. package/dist/rpc/reliability/cancellation/rpcAbort.helper.d.ts +17 -0
  17. package/dist/rpc/reliability/cancellation/rpcAbort.helper.js +40 -0
  18. package/dist/rpc/reliability/retry/rpcRetry.helper.js +3 -1
  19. package/dist/rpc/reliability/timeout/rpcTimeout.helper.d.ts +5 -3
  20. package/dist/rpc/reliability/timeout/rpcTimeout.helper.js +5 -4
  21. package/dist/rpc/server/index.d.ts +6 -0
  22. package/dist/rpc/server/index.js +5 -0
  23. package/dist/rpc/server/rpcBaseErrorMapping.helper.d.ts +26 -0
  24. package/dist/rpc/server/rpcBaseErrorMapping.helper.js +70 -0
  25. package/dist/rpc/server/rpcErrorMapping.helper.d.ts +36 -0
  26. package/dist/rpc/server/rpcErrorMapping.helper.js +102 -0
  27. package/dist/rpc/server/rpcServer.core.d.ts +0 -7
  28. package/dist/rpc/server/rpcServer.core.js +7 -91
  29. package/dist/rpc/transport/codec/index.d.ts +10 -0
  30. package/dist/rpc/transport/codec/index.js +8 -0
  31. package/dist/rpc/transport/codec/rpcBody.helper.d.ts +36 -0
  32. package/dist/rpc/transport/codec/rpcBody.helper.js +64 -0
  33. package/dist/rpc/transport/codec/rpcCodec.helper.d.ts +41 -0
  34. package/dist/rpc/transport/codec/rpcCodec.helper.js +40 -0
  35. package/dist/rpc/transport/http/index.d.ts +12 -0
  36. package/dist/rpc/transport/http/index.js +10 -0
  37. package/dist/rpc/transport/http/rpcFetchHandler.core.d.ts +36 -0
  38. package/dist/rpc/transport/http/rpcFetchHandler.core.js +97 -0
  39. package/dist/rpc/transport/http/rpcHttpExchange.helper.d.ts +27 -0
  40. package/dist/rpc/transport/http/rpcHttpExchange.helper.js +78 -0
  41. package/dist/rpc/transport/http/rpcHttpStatus.helper.d.ts +16 -0
  42. package/dist/rpc/transport/http/rpcHttpStatus.helper.js +37 -0
  43. package/dist/rpc/transport/http/rpcHttpTransport.core.d.ts +36 -0
  44. package/dist/rpc/transport/http/rpcHttpTransport.core.js +45 -0
  45. package/dist/rpc/transport/index.d.ts +12 -0
  46. package/dist/rpc/transport/index.js +9 -1
  47. package/dist/rpc/transport/memory/index.d.ts +7 -0
  48. package/dist/rpc/transport/memory/index.js +6 -0
  49. package/dist/rpc/transport/memory/rpcMemoryTransport.core.d.ts +42 -0
  50. package/dist/rpc/transport/memory/rpcMemoryTransport.core.js +73 -0
  51. package/dist/rpc/validation/rpcUnsafeKey.helper.d.ts +18 -0
  52. package/dist/rpc/validation/rpcUnsafeKey.helper.js +60 -0
  53. package/dist/rpc/validation/rpcValidation.core.d.ts +11 -2
  54. package/dist/rpc/validation/rpcValidation.core.js +11 -2
  55. package/package.json +9 -7
@@ -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. A timeout promise
10
- * whose timer is never cleared keeps the event loop alive for its full
11
- * duration even after the work it guarded has finished.
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. A timeout promise
12
- * whose timer is never cleared keeps the event loop alive for its full
13
- * duration even after the work it guarded has finished.
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
@@ -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,26 @@
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
+ /**
17
+ * The wire code for an `RPCError` that was not built to be exposed.
18
+ *
19
+ * A standard wire code (a key of `RPC_HTTP_STATUS`, such as
20
+ * `RPC_UNAVAILABLE`) is public vocabulary and callers act on it, so it
21
+ * travels. Any other code — `new RPCError("…", { code: "TASK_SECRET" })` —
22
+ * is server detail just as the message is, and goes out as
23
+ * `RPC_INTERNAL_ERROR`.
24
+ */
25
+ export declare function withheldRPCErrorCode(code: string): string;
26
+ //# sourceMappingURL=rpcBaseErrorMapping.helper.d.ts.map
@@ -0,0 +1,70 @@
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
+ import { RPC_HTTP_STATUS } from "../transport/http/rpcHttpStatus.helper.js";
8
+ /** Wire code for each HTTP-style status an exposable error can carry. */
9
+ const STATUS_WIRE_CODES = new Map([
10
+ [400, "RPC_VALIDATION_ERROR"],
11
+ [401, "RPC_UNAUTHENTICATED"],
12
+ [403, "RPC_FORBIDDEN"],
13
+ [404, "RPC_NOT_FOUND"],
14
+ [408, "RPC_TIMEOUT"],
15
+ [409, "RPC_CONFLICT"],
16
+ [422, "RPC_VALIDATION_ERROR"],
17
+ [429, "RPC_RATE_LIMITED"],
18
+ [503, "RPC_UNAVAILABLE"],
19
+ [504, "RPC_TIMEOUT"],
20
+ ]);
21
+ function validationDetails(error) {
22
+ if (error.issues.length === 0) {
23
+ return undefined;
24
+ }
25
+ // The received `value` is dropped: a response never echoes caller input.
26
+ return error.issues.map((issue) => ({
27
+ path: (issue.path ?? (issue.field !== undefined ? [issue.field] : [])).map(String).join("."),
28
+ code: issue.code ?? "invalid",
29
+ message: issue.message,
30
+ }));
31
+ }
32
+ /**
33
+ * Maps a `@zudojs/errors` error built with `expose: true` onto a wire
34
+ * payload: its status picks the RPC code (404 → `RPC_NOT_FOUND`, 409 →
35
+ * `RPC_CONFLICT`, 401, 403, 422, 429 …; a `ValidationError` is always
36
+ * `RPC_VALIDATION_ERROR`), and an unlisted status keeps the error's own
37
+ * code. The message is the error's own, which `expose: true` declares
38
+ * safe. Returns `undefined` for anything else, which stays internal.
39
+ */
40
+ export function mapExposedBaseError(error) {
41
+ if (!isBaseError(error) || error.expose !== true) {
42
+ return undefined;
43
+ }
44
+ const code = error instanceof ValidationError
45
+ ? "RPC_VALIDATION_ERROR"
46
+ : (STATUS_WIRE_CODES.get(error.statusCode) ?? String(error.code));
47
+ const details = error instanceof ValidationError
48
+ ? validationDetails(error)
49
+ : error instanceof RateLimitError && error.retryAfterSeconds !== undefined
50
+ ? { retryAfter: error.retryAfterSeconds }
51
+ : undefined;
52
+ return {
53
+ code,
54
+ message: error.message,
55
+ ...(details !== undefined ? { details } : {}),
56
+ };
57
+ }
58
+ /**
59
+ * The wire code for an `RPCError` that was not built to be exposed.
60
+ *
61
+ * A standard wire code (a key of `RPC_HTTP_STATUS`, such as
62
+ * `RPC_UNAVAILABLE`) is public vocabulary and callers act on it, so it
63
+ * travels. Any other code — `new RPCError("…", { code: "TASK_SECRET" })` —
64
+ * is server detail just as the message is, and goes out as
65
+ * `RPC_INTERNAL_ERROR`.
66
+ */
67
+ export function withheldRPCErrorCode(code) {
68
+ return Object.hasOwn(RPC_HTTP_STATUS, code) ? code : "RPC_INTERNAL_ERROR";
69
+ }
70
+ //# sourceMappingURL=rpcBaseErrorMapping.helper.js.map
@@ -0,0 +1,36 @@
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 — nor does a non-exposed custom
25
+ * code ({@link withheldRPCErrorCode}).
26
+ */
27
+ export declare function mapRPCError(error: unknown): RPCMappedError;
28
+ /**
29
+ * The wire code of a typed RPC error — `RPC_TIMEOUT` for an
30
+ * `RPCTimeoutError`, and so on — or `undefined` for anything without one.
31
+ * The client stamps it on every error it rejects with, so a caller
32
+ * compares `error.code` against the same strings whether the failure was
33
+ * reported by the server or raised locally (its own deadline, a cancel).
34
+ */
35
+ export declare function rpcWireCodeOf(error: unknown): string | undefined;
36
+ //# sourceMappingURL=rpcErrorMapping.helper.d.ts.map
@@ -0,0 +1,102 @@
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, withheldRPCErrorCode } 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 — nor does a non-exposed custom
38
+ * code ({@link withheldRPCErrorCode}).
39
+ */
40
+ export function mapRPCError(error) {
41
+ if (error instanceof RPCInternalError || !(error instanceof Error)) {
42
+ return internalFailure("RPC_INTERNAL_ERROR");
43
+ }
44
+ for (const [type, code, internal] of ERROR_CODES) {
45
+ if (error instanceof type) {
46
+ return internal === true
47
+ ? internalFailure(code)
48
+ : exposed(code, error.message, detailsOf(error));
49
+ }
50
+ }
51
+ if (isRPCError(error)) {
52
+ return error.expose === true
53
+ ? exposed(error.code, error.message, undefined)
54
+ : internalFailure(withheldRPCErrorCode(error.code));
55
+ }
56
+ const base = mapExposedBaseError(error);
57
+ if (base !== undefined) {
58
+ return { payload: base, internal: false };
59
+ }
60
+ return internalFailure("RPC_INTERNAL_ERROR");
61
+ }
62
+ /**
63
+ * The wire code of a typed RPC error — `RPC_TIMEOUT` for an
64
+ * `RPCTimeoutError`, and so on — or `undefined` for anything without one.
65
+ * The client stamps it on every error it rejects with, so a caller
66
+ * compares `error.code` against the same strings whether the failure was
67
+ * reported by the server or raised locally (its own deadline, a cancel).
68
+ */
69
+ export function rpcWireCodeOf(error) {
70
+ if (error instanceof RPCInternalError) {
71
+ return "RPC_INTERNAL_ERROR";
72
+ }
73
+ if (error instanceof RPCTransportError) {
74
+ return "RPC_TRANSPORT_ERROR";
75
+ }
76
+ return ERROR_CODES.find(([type]) => error instanceof type)?.[1];
77
+ }
78
+ function detailsOf(error) {
79
+ if (error instanceof RPCValidationError &&
80
+ error.issues !== undefined &&
81
+ error.issues.length > 0) {
82
+ return error.issues;
83
+ }
84
+ if (error instanceof RPCRateLimitedError && error.retryAfter !== undefined) {
85
+ return { retryAfter: error.retryAfter };
86
+ }
87
+ return undefined;
88
+ }
89
+ function exposed(code, message, details) {
90
+ return {
91
+ payload: {
92
+ code,
93
+ message,
94
+ ...(details !== undefined ? { details } : {}),
95
+ },
96
+ internal: false,
97
+ };
98
+ }
99
+ function internalFailure(code) {
100
+ return { payload: { code, message: INTERNAL_ERROR_MESSAGE }, internal: true };
101
+ }
102
+ //# 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 { isRPCError, RPCAuthenticationError, RPCCancelledError, RPCDeadlineExceededError, RPCDeserializationError, RPCForbiddenError, RPCInternalError, RPCInvalidRequestError, RPCProcedureNotFoundError, RPCRateLimitedError, RPCSerializationError, RPCTimeoutError, RPCUnavailableError, RPCValidationError, } from "../errors/rpc.errors.js";
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 = this.mapError(error);
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
- // The error is typed, so the caller keeps its code, but it was
104
- // constructed with `expose: false`: its message is server detail
105
- // and goes to the log, not the wire.
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