@zudojs/rpc 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +120 -17
  2. package/dist/index.d.ts +12 -8
  3. package/dist/index.js +14 -5
  4. package/dist/rpc/client/index.d.ts +5 -0
  5. package/dist/rpc/client/index.js +5 -0
  6. package/dist/rpc/client/rpcClient.core.d.ts +6 -7
  7. package/dist/rpc/client/rpcClient.core.js +32 -46
  8. package/dist/rpc/client/rpcWireError.helper.d.ts +13 -0
  9. package/dist/rpc/client/rpcWireError.helper.js +71 -0
  10. package/dist/rpc/constants/index.d.ts +6 -1
  11. package/dist/rpc/constants/index.js +6 -1
  12. package/dist/rpc/constants/rpcConstants.core.d.ts +11 -0
  13. package/dist/rpc/constants/rpcConstants.core.js +11 -0
  14. package/dist/rpc/context/rpcContext.type.d.ts +7 -0
  15. package/dist/rpc/dispatcher/rpcDispatcher.core.js +14 -1
  16. package/dist/rpc/reliability/cancellation/rpcAbort.helper.d.ts +17 -0
  17. package/dist/rpc/reliability/cancellation/rpcAbort.helper.js +40 -0
  18. package/dist/rpc/reliability/retry/rpcRetry.helper.js +3 -1
  19. package/dist/rpc/reliability/timeout/rpcTimeout.helper.d.ts +5 -3
  20. package/dist/rpc/reliability/timeout/rpcTimeout.helper.js +5 -4
  21. package/dist/rpc/server/index.d.ts +6 -0
  22. package/dist/rpc/server/index.js +5 -0
  23. package/dist/rpc/server/rpcBaseErrorMapping.helper.d.ts +16 -0
  24. package/dist/rpc/server/rpcBaseErrorMapping.helper.js +57 -0
  25. package/dist/rpc/server/rpcErrorMapping.helper.d.ts +35 -0
  26. package/dist/rpc/server/rpcErrorMapping.helper.js +101 -0
  27. package/dist/rpc/server/rpcServer.core.d.ts +0 -7
  28. package/dist/rpc/server/rpcServer.core.js +7 -91
  29. package/dist/rpc/transport/codec/index.d.ts +10 -0
  30. package/dist/rpc/transport/codec/index.js +8 -0
  31. package/dist/rpc/transport/codec/rpcBody.helper.d.ts +36 -0
  32. package/dist/rpc/transport/codec/rpcBody.helper.js +64 -0
  33. package/dist/rpc/transport/codec/rpcCodec.helper.d.ts +41 -0
  34. package/dist/rpc/transport/codec/rpcCodec.helper.js +40 -0
  35. package/dist/rpc/transport/http/index.d.ts +12 -0
  36. package/dist/rpc/transport/http/index.js +10 -0
  37. package/dist/rpc/transport/http/rpcFetchHandler.core.d.ts +36 -0
  38. package/dist/rpc/transport/http/rpcFetchHandler.core.js +97 -0
  39. package/dist/rpc/transport/http/rpcHttpExchange.helper.d.ts +27 -0
  40. package/dist/rpc/transport/http/rpcHttpExchange.helper.js +78 -0
  41. package/dist/rpc/transport/http/rpcHttpStatus.helper.d.ts +16 -0
  42. package/dist/rpc/transport/http/rpcHttpStatus.helper.js +37 -0
  43. package/dist/rpc/transport/http/rpcHttpTransport.core.d.ts +36 -0
  44. package/dist/rpc/transport/http/rpcHttpTransport.core.js +45 -0
  45. package/dist/rpc/transport/index.d.ts +12 -0
  46. package/dist/rpc/transport/index.js +9 -1
  47. package/dist/rpc/transport/memory/index.d.ts +7 -0
  48. package/dist/rpc/transport/memory/index.js +6 -0
  49. package/dist/rpc/transport/memory/rpcMemoryTransport.core.d.ts +42 -0
  50. package/dist/rpc/transport/memory/rpcMemoryTransport.core.js +73 -0
  51. package/dist/rpc/validation/rpcUnsafeKey.helper.d.ts +18 -0
  52. package/dist/rpc/validation/rpcUnsafeKey.helper.js +60 -0
  53. package/dist/rpc/validation/rpcValidation.core.d.ts +11 -2
  54. package/dist/rpc/validation/rpcValidation.core.js +11 -2
  55. package/package.json +9 -7
@@ -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,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 { 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
@@ -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