@zudojs/rpc 0.0.1 → 1.0.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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +8 -4
  3. package/dist/index.js +3 -4
  4. package/dist/rpc/client/index.d.ts +1 -1
  5. package/dist/rpc/client/rpcClient.core.d.ts +57 -3
  6. package/dist/rpc/client/rpcClient.core.js +172 -32
  7. package/dist/rpc/constants/index.d.ts +1 -1
  8. package/dist/rpc/constants/index.js +1 -1
  9. package/dist/rpc/constants/rpcConstants.core.d.ts +18 -0
  10. package/dist/rpc/constants/rpcConstants.core.js +18 -0
  11. package/dist/rpc/dispatcher/index.d.ts +1 -0
  12. package/dist/rpc/dispatcher/rpcDispatcher.core.d.ts +48 -1
  13. package/dist/rpc/dispatcher/rpcDispatcher.core.js +104 -13
  14. package/dist/rpc/middleware/rpcMiddleware.core.d.ts +12 -0
  15. package/dist/rpc/middleware/rpcMiddleware.core.js +29 -6
  16. package/dist/rpc/procedure/rpcProcedure.type.d.ts +25 -3
  17. package/dist/rpc/procedure/rpcProcedure.type.js +9 -0
  18. package/dist/rpc/procedure/rpcProcedureRegistry.core.d.ts +15 -1
  19. package/dist/rpc/procedure/rpcProcedureRegistry.core.js +26 -1
  20. package/dist/rpc/procedure/rpcProcedureRouter.core.d.ts +1 -1
  21. package/dist/rpc/procedure/rpcProcedureRouter.core.js +5 -0
  22. package/dist/rpc/reliability/cancellation/rpcCancellation.helper.d.ts +30 -3
  23. package/dist/rpc/reliability/cancellation/rpcCancellation.helper.js +60 -4
  24. package/dist/rpc/reliability/deadline/rpcDeadline.helper.d.ts +14 -1
  25. package/dist/rpc/reliability/deadline/rpcDeadline.helper.js +23 -2
  26. package/dist/rpc/reliability/index.d.ts +5 -4
  27. package/dist/rpc/reliability/index.js +3 -3
  28. package/dist/rpc/reliability/retry/rpcRetry.helper.d.ts +22 -0
  29. package/dist/rpc/reliability/retry/rpcRetry.helper.js +80 -8
  30. package/dist/rpc/reliability/timeout/rpcTimeout.helper.d.ts +18 -2
  31. package/dist/rpc/reliability/timeout/rpcTimeout.helper.js +71 -7
  32. package/dist/rpc/server/index.d.ts +1 -0
  33. package/dist/rpc/server/rpcServer.core.d.ts +41 -2
  34. package/dist/rpc/server/rpcServer.core.js +82 -18
  35. package/dist/rpc/streaming/rpcStreaming.type.d.ts +9 -0
  36. package/dist/rpc/streaming/rpcStreaming.type.js +9 -0
  37. package/dist/rpc/transport/rpcTransport.type.d.ts +9 -1
  38. package/dist/rpc/validation/index.d.ts +3 -0
  39. package/dist/rpc/validation/index.js +2 -0
  40. package/dist/rpc/validation/rpcValidation.core.d.ts +81 -0
  41. package/dist/rpc/validation/rpcValidation.core.js +125 -0
  42. package/package.json +23 -16
  43. package/dist/.tsbuildinfo +0 -1
  44. package/dist/index.d.ts.map +0 -1
  45. package/dist/index.js.map +0 -1
  46. package/dist/rpc/client/index.d.ts.map +0 -1
  47. package/dist/rpc/client/index.js.map +0 -1
  48. package/dist/rpc/client/rpcClient.core.d.ts.map +0 -1
  49. package/dist/rpc/client/rpcClient.core.js.map +0 -1
  50. package/dist/rpc/constants/index.d.ts.map +0 -1
  51. package/dist/rpc/constants/index.js.map +0 -1
  52. package/dist/rpc/constants/rpcConstants.core.d.ts.map +0 -1
  53. package/dist/rpc/constants/rpcConstants.core.js.map +0 -1
  54. package/dist/rpc/context/index.d.ts.map +0 -1
  55. package/dist/rpc/context/index.js.map +0 -1
  56. package/dist/rpc/context/rpcContext.type.d.ts.map +0 -1
  57. package/dist/rpc/context/rpcContext.type.js.map +0 -1
  58. package/dist/rpc/dispatcher/index.d.ts.map +0 -1
  59. package/dist/rpc/dispatcher/index.js.map +0 -1
  60. package/dist/rpc/dispatcher/rpcDispatcher.core.d.ts.map +0 -1
  61. package/dist/rpc/dispatcher/rpcDispatcher.core.js.map +0 -1
  62. package/dist/rpc/errors/index.d.ts.map +0 -1
  63. package/dist/rpc/errors/index.js.map +0 -1
  64. package/dist/rpc/errors/rpc.errors.d.ts.map +0 -1
  65. package/dist/rpc/errors/rpc.errors.js.map +0 -1
  66. package/dist/rpc/interceptor/index.d.ts.map +0 -1
  67. package/dist/rpc/interceptor/index.js.map +0 -1
  68. package/dist/rpc/interceptor/rpcInterceptor.type.d.ts.map +0 -1
  69. package/dist/rpc/interceptor/rpcInterceptor.type.js.map +0 -1
  70. package/dist/rpc/middleware/index.d.ts.map +0 -1
  71. package/dist/rpc/middleware/index.js.map +0 -1
  72. package/dist/rpc/middleware/rpcMiddleware.core.d.ts.map +0 -1
  73. package/dist/rpc/middleware/rpcMiddleware.core.js.map +0 -1
  74. package/dist/rpc/procedure/index.d.ts.map +0 -1
  75. package/dist/rpc/procedure/index.js.map +0 -1
  76. package/dist/rpc/procedure/rpcProcedure.type.d.ts.map +0 -1
  77. package/dist/rpc/procedure/rpcProcedure.type.js.map +0 -1
  78. package/dist/rpc/procedure/rpcProcedureRegistry.core.d.ts.map +0 -1
  79. package/dist/rpc/procedure/rpcProcedureRegistry.core.js.map +0 -1
  80. package/dist/rpc/procedure/rpcProcedureRouter.core.d.ts.map +0 -1
  81. package/dist/rpc/procedure/rpcProcedureRouter.core.js.map +0 -1
  82. package/dist/rpc/reliability/cancellation/rpcCancellation.helper.d.ts.map +0 -1
  83. package/dist/rpc/reliability/cancellation/rpcCancellation.helper.js.map +0 -1
  84. package/dist/rpc/reliability/deadline/rpcDeadline.helper.d.ts.map +0 -1
  85. package/dist/rpc/reliability/deadline/rpcDeadline.helper.js.map +0 -1
  86. package/dist/rpc/reliability/index.d.ts.map +0 -1
  87. package/dist/rpc/reliability/index.js.map +0 -1
  88. package/dist/rpc/reliability/retry/rpcRetry.helper.d.ts.map +0 -1
  89. package/dist/rpc/reliability/retry/rpcRetry.helper.js.map +0 -1
  90. package/dist/rpc/reliability/timeout/rpcTimeout.helper.d.ts.map +0 -1
  91. package/dist/rpc/reliability/timeout/rpcTimeout.helper.js.map +0 -1
  92. package/dist/rpc/server/index.d.ts.map +0 -1
  93. package/dist/rpc/server/index.js.map +0 -1
  94. package/dist/rpc/server/rpcServer.core.d.ts.map +0 -1
  95. package/dist/rpc/server/rpcServer.core.js.map +0 -1
  96. package/dist/rpc/streaming/index.d.ts.map +0 -1
  97. package/dist/rpc/streaming/index.js.map +0 -1
  98. package/dist/rpc/streaming/rpcStreaming.type.d.ts.map +0 -1
  99. package/dist/rpc/streaming/rpcStreaming.type.js.map +0 -1
  100. package/dist/rpc/transport/index.d.ts.map +0 -1
  101. package/dist/rpc/transport/index.js.map +0 -1
  102. package/dist/rpc/transport/rpcTransport.type.d.ts.map +0 -1
  103. package/dist/rpc/transport/rpcTransport.type.js.map +0 -1
  104. package/dist/rpc/types/index.d.ts.map +0 -1
  105. package/dist/rpc/types/index.js.map +0 -1
  106. package/dist/rpc/types/rpcMetadata.type.d.ts.map +0 -1
  107. package/dist/rpc/types/rpcMetadata.type.js.map +0 -1
  108. package/dist/rpc/types/rpcProcedureName.type.d.ts.map +0 -1
  109. package/dist/rpc/types/rpcProcedureName.type.js.map +0 -1
  110. package/dist/rpc/types/rpcRequest.type.d.ts.map +0 -1
  111. package/dist/rpc/types/rpcRequest.type.js.map +0 -1
  112. package/dist/rpc/types/rpcResponse.type.d.ts.map +0 -1
  113. package/dist/rpc/types/rpcResponse.type.js.map +0 -1
  114. package/dist/testing/index.d.ts.map +0 -1
  115. package/dist/testing/index.js.map +0 -1
@@ -3,15 +3,38 @@
3
3
  *
4
4
  * Retry utilities for RPC operations.
5
5
  */
6
+ import { RPCCancelledError } from "../../errors/rpc.errors.js";
7
+ import { MAX_TIMER_DELAY } from "../../constants/rpcConstants.core.js";
6
8
  /**
7
9
  * Default retry options.
8
10
  */
9
- export const DEFAULT_RETRY_OPTIONS = {
11
+ export const DEFAULT_RETRY_OPTIONS = Object.freeze({
10
12
  attempts: 1,
11
13
  delay: 0,
12
- };
14
+ });
15
+ /**
16
+ * Applies the configured jitter strategy to a delay.
17
+ */
18
+ function applyJitter(delay, jitter) {
19
+ if (delay <= 0) {
20
+ return 0;
21
+ }
22
+ switch (jitter) {
23
+ case "full":
24
+ return Math.round(Math.random() * delay);
25
+ case "equal":
26
+ return Math.round(delay / 2 + Math.random() * (delay / 2));
27
+ default:
28
+ return delay;
29
+ }
30
+ }
13
31
  /**
14
32
  * Calculates the delay for a retry attempt.
33
+ *
34
+ * The result is finite, non-negative, and no greater than a timer can
35
+ * represent. Jitter defaults to `"none"` here so the calculation stays
36
+ * predictable for callers that compose it; {@link retry} opts into
37
+ * `"full"` jitter by default.
15
38
  */
16
39
  export function calculateRetryDelay(attempt, options) {
17
40
  const backoff = options.backoff ?? "fixed";
@@ -21,30 +44,79 @@ export function calculateRetryDelay(attempt, options) {
21
44
  delay = options.delay * attempt;
22
45
  }
23
46
  else if (backoff === "exponential") {
24
- delay = options.delay * 2 ** (attempt - 1);
47
+ delay = options.delay * 2 ** Math.max(0, attempt - 1);
48
+ }
49
+ if (!Number.isFinite(delay) || delay < 0) {
50
+ delay = 0;
51
+ }
52
+ const capped = Math.min(delay, maxDelay, MAX_TIMER_DELAY);
53
+ return Math.min(applyJitter(capped, options.jitter ?? "none"), MAX_TIMER_DELAY);
54
+ }
55
+ /**
56
+ * Sleeps for a duration, rejecting early if the signal aborts.
57
+ */
58
+ function sleep(ms, signal) {
59
+ if (ms <= 0) {
60
+ return Promise.resolve();
25
61
  }
26
- return Math.min(delay, maxDelay);
62
+ return new Promise((resolve, reject) => {
63
+ const timer = setTimeout(() => {
64
+ signal?.removeEventListener("abort", onAbort);
65
+ resolve();
66
+ }, ms);
67
+ timer.unref?.();
68
+ function onAbort() {
69
+ clearTimeout(timer);
70
+ reject(signal?.reason instanceof Error
71
+ ? signal.reason
72
+ : new RPCCancelledError("Retry cancelled."));
73
+ }
74
+ if (signal) {
75
+ if (signal.aborted) {
76
+ onAbort();
77
+ return;
78
+ }
79
+ signal.addEventListener("abort", onAbort, { once: true });
80
+ }
81
+ });
27
82
  }
28
83
  /**
29
84
  * Retries an asynchronous operation.
85
+ *
86
+ * Jitter defaults to `"full"` so concurrent callers retrying the same
87
+ * failing dependency do not synchronise into a retry wave.
30
88
  */
31
89
  export async function retry(operation, options) {
32
- const attempts = Math.max(1, options.attempts);
90
+ const attempts = Math.max(1, Math.floor(options.attempts));
91
+ const jittered = {
92
+ ...options,
93
+ jitter: options.jitter ?? "full",
94
+ };
95
+ let lastError;
33
96
  for (let attempt = 0; attempt < attempts; attempt += 1) {
97
+ if (options.signal?.aborted) {
98
+ throw options.signal.reason instanceof Error
99
+ ? options.signal.reason
100
+ : new RPCCancelledError("Retry cancelled.");
101
+ }
34
102
  try {
35
103
  return await operation();
36
104
  }
37
105
  catch (error) {
106
+ lastError = error;
38
107
  if (attempt >= attempts - 1) {
39
108
  throw error;
40
109
  }
41
110
  if (options.retryIf && !options.retryIf(error)) {
42
111
  throw error;
43
112
  }
44
- const delay = calculateRetryDelay(attempt + 1, options);
45
- await new Promise((resolve) => setTimeout(resolve, delay));
113
+ const delay = calculateRetryDelay(attempt + 1, jittered);
114
+ options.onRetry?.(error, attempt + 1, delay);
115
+ await sleep(delay, options.signal);
46
116
  }
47
117
  }
48
- throw new Error("Retry loop exited unexpectedly.");
118
+ // Unreachable while `attempts >= 1`, but throwing the last observed
119
+ // error beats inventing a new one if that ever changes.
120
+ throw lastError ?? new Error("Retry loop exited without running.");
49
121
  }
50
122
  //# sourceMappingURL=rpcRetry.helper.js.map
@@ -5,10 +5,26 @@
5
5
  */
6
6
  /**
7
7
  * Creates a timeout promise that rejects after the given duration.
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.
8
12
  */
9
- export declare function createTimeout(duration: number): Promise<never>;
13
+ export declare function createTimeout(duration: number, procedureName?: string): {
14
+ readonly promise: Promise<never>;
15
+ cancel(): void;
16
+ };
10
17
  /**
11
18
  * Wraps a promise with a timeout.
19
+ *
20
+ * The timer is always cleared once the race settles, whichever side won.
21
+ */
22
+ export declare function withTimeout<T>(promise: Promise<T>, duration: number, procedureName?: string): Promise<T>;
23
+ /**
24
+ * Runs an operation under a timeout, aborting it when the timeout wins.
25
+ *
26
+ * Unlike {@link withTimeout}, this gives the operation a signal so it can
27
+ * stop its own work rather than continuing unobserved.
12
28
  */
13
- export declare function withTimeout<T>(promise: Promise<T>, duration: number): Promise<T>;
29
+ export declare function runWithTimeout<T>(operation: (signal: AbortSignal) => Promise<T>, duration: number, procedureName?: string, parentSignal?: AbortSignal): Promise<T>;
14
30
  //# sourceMappingURL=rpcTimeout.helper.d.ts.map
@@ -3,20 +3,84 @@
3
3
  *
4
4
  * Timeout utilities for RPC operations.
5
5
  */
6
+ import { RPCTimeoutError } from "../../errors/rpc.errors.js";
7
+ import { MAX_TIMER_DELAY } from "../../constants/rpcConstants.core.js";
6
8
  /**
7
9
  * Creates a timeout promise that rejects after the given duration.
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.
8
14
  */
9
- export function createTimeout(duration) {
10
- return new Promise((_, reject) => {
11
- setTimeout(() => {
12
- reject(new Error(`Operation timed out after ${duration}ms.`));
13
- }, duration);
15
+ export function createTimeout(duration, procedureName) {
16
+ const delay = Math.min(Math.max(0, duration), MAX_TIMER_DELAY);
17
+ let timer;
18
+ const promise = new Promise((_, reject) => {
19
+ timer = setTimeout(() => {
20
+ reject(new RPCTimeoutError(duration, procedureName));
21
+ }, delay);
22
+ timer.unref?.();
14
23
  });
24
+ return {
25
+ promise,
26
+ cancel() {
27
+ if (timer !== undefined) {
28
+ clearTimeout(timer);
29
+ timer = undefined;
30
+ }
31
+ },
32
+ };
15
33
  }
16
34
  /**
17
35
  * Wraps a promise with a timeout.
36
+ *
37
+ * The timer is always cleared once the race settles, whichever side won.
38
+ */
39
+ export async function withTimeout(promise, duration, procedureName) {
40
+ const timeout = createTimeout(duration, procedureName);
41
+ try {
42
+ return await Promise.race([promise, timeout.promise]);
43
+ }
44
+ finally {
45
+ timeout.cancel();
46
+ }
47
+ }
48
+ /**
49
+ * Runs an operation under a timeout, aborting it when the timeout wins.
50
+ *
51
+ * Unlike {@link withTimeout}, this gives the operation a signal so it can
52
+ * stop its own work rather than continuing unobserved.
18
53
  */
19
- export async function withTimeout(promise, duration) {
20
- return Promise.race([promise, createTimeout(duration)]);
54
+ export async function runWithTimeout(operation, duration, procedureName, parentSignal) {
55
+ const controller = new AbortController();
56
+ const timeout = createTimeout(duration, procedureName);
57
+ const onParentAbort = () => {
58
+ if (!controller.signal.aborted) {
59
+ controller.abort(parentSignal?.reason);
60
+ }
61
+ };
62
+ if (parentSignal) {
63
+ if (parentSignal.aborted) {
64
+ onParentAbort();
65
+ }
66
+ else {
67
+ parentSignal.addEventListener("abort", onParentAbort, { once: true });
68
+ }
69
+ }
70
+ try {
71
+ return await Promise.race([
72
+ operation(controller.signal),
73
+ timeout.promise.catch((error) => {
74
+ if (!controller.signal.aborted) {
75
+ controller.abort(error);
76
+ }
77
+ throw error;
78
+ }),
79
+ ]);
80
+ }
81
+ finally {
82
+ timeout.cancel();
83
+ parentSignal?.removeEventListener("abort", onParentAbort);
84
+ }
21
85
  }
22
86
  //# sourceMappingURL=rpcTimeout.helper.js.map
@@ -1,2 +1,3 @@
1
+ export type { RPCServerOptions } from "./rpcServer.core.js";
1
2
  export { RPCServer } from "./rpcServer.core.js";
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -3,6 +3,24 @@ import type { RPCResponse } from "../types/rpcResponse.type.js";
3
3
  import type { RPCProcedure } from "../procedure/rpcProcedure.type.js";
4
4
  import { RPCProcedureRegistry } from "../procedure/rpcProcedureRegistry.core.js";
5
5
  import { RPCMiddlewareStack } from "../middleware/rpcMiddleware.core.js";
6
+ import type { RPCDispatcherOptions } from "../dispatcher/rpcDispatcher.core.js";
7
+ import type { RPCRequestLimits } from "../validation/rpcValidation.core.js";
8
+ /**
9
+ * Options for an RPC server.
10
+ */
11
+ export interface RPCServerOptions {
12
+ /** Limits applied to every incoming request before dispatch. */
13
+ readonly limits?: RPCRequestLimits;
14
+ /** Dispatch behaviour: default timeout and deadline handling. */
15
+ readonly dispatch?: RPCDispatcherOptions;
16
+ /**
17
+ * Receives errors that produced an internal error response, together
18
+ * with the request id the caller was given. Internal error detail is
19
+ * never returned over the wire, so this is where it must be recorded
20
+ * for the failure to be diagnosable.
21
+ */
22
+ readonly onInternalError?: (error: unknown, requestId: string) => void;
23
+ }
6
24
  /**
7
25
  * RPC server that receives and dispatches requests.
8
26
  */
@@ -10,18 +28,39 @@ export declare class RPCServer {
10
28
  private readonly registry;
11
29
  private readonly middleware;
12
30
  private readonly dispatcher;
13
- constructor(registry?: RPCProcedureRegistry, middleware?: RPCMiddlewareStack);
31
+ private readonly options;
32
+ constructor(registry?: RPCProcedureRegistry, middleware?: RPCMiddlewareStack, options?: RPCServerOptions);
14
33
  /**
15
34
  * Registers a procedure.
35
+ *
36
+ * Generic over the procedure's input and output so a procedure built
37
+ * with `createRPCProcedure<TInput, TOutput>` can be registered
38
+ * directly. `RPCProcedure` is contravariant in `TInput` (its handler
39
+ * accepts that input), so a typed procedure is NOT assignable to
40
+ * `RPCProcedure<unknown>` and a non-generic signature would reject
41
+ * every typed procedure. Decoding stays a runtime concern.
16
42
  */
17
- register(procedure: RPCProcedure): this;
43
+ register<TInput = unknown, TOutput = unknown>(procedure: RPCProcedure<TInput, TOutput>): this;
18
44
  /**
19
45
  * Handles an incoming RPC request.
46
+ *
47
+ * The frame is validated before dispatch, and every failure is mapped
48
+ * to a typed wire code. Unexpected errors are reported to
49
+ * `onInternalError` and answered with a fixed message: internal
50
+ * exception text can name hosts, paths, credentials or queries, and
51
+ * the caller is an untrusted peer.
20
52
  */
21
53
  handle(request: RPCRequest): Promise<RPCResponse>;
22
54
  /**
23
55
  * Returns the procedure registry.
24
56
  */
25
57
  getRegistry(): RPCProcedureRegistry;
58
+ /**
59
+ * Maps a known error onto a wire payload.
60
+ *
61
+ * Returns `undefined` for anything unrecognised, which the caller
62
+ * answers with a generic internal error.
63
+ */
64
+ private mapError;
26
65
  }
27
66
  //# sourceMappingURL=rpcServer.core.d.ts.map
@@ -2,7 +2,30 @@ 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 { RPCInternalError, RPCProcedureNotFoundError, RPCValidationError, } from "../errors/rpc.errors.js";
5
+ import { isRPCError, RPCAuthenticationError, RPCCancelledError, RPCDeadlineExceededError, RPCDeserializationError, RPCForbiddenError, RPCInvalidRequestError, RPCProcedureNotFoundError, RPCRateLimitedError, RPCSerializationError, RPCTimeoutError, RPCUnavailableError, RPCValidationError, } from "../errors/rpc.errors.js";
6
+ import { INTERNAL_ERROR_MESSAGE } from "../constants/rpcConstants.core.js";
7
+ 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
+ [RPCSerializationError, "RPC_SERIALIZATION_ERROR"],
27
+ [RPCDeserializationError, "RPC_DESERIALIZATION_ERROR"],
28
+ ];
6
29
  /**
7
30
  * RPC server that receives and dispatches requests.
8
31
  */
@@ -10,13 +33,22 @@ export class RPCServer {
10
33
  registry;
11
34
  middleware;
12
35
  dispatcher;
13
- constructor(registry, middleware) {
36
+ options;
37
+ constructor(registry, middleware, options = {}) {
14
38
  this.registry = registry ?? new RPCProcedureRegistry();
15
39
  this.middleware = middleware ?? new RPCMiddlewareStack();
16
- this.dispatcher = new RPCDispatcher(this.registry, this.middleware);
40
+ this.options = options;
41
+ this.dispatcher = new RPCDispatcher(this.registry, this.middleware, options.dispatch ?? {});
17
42
  }
18
43
  /**
19
44
  * Registers a procedure.
45
+ *
46
+ * Generic over the procedure's input and output so a procedure built
47
+ * with `createRPCProcedure<TInput, TOutput>` can be registered
48
+ * directly. `RPCProcedure` is contravariant in `TInput` (its handler
49
+ * accepts that input), so a typed procedure is NOT assignable to
50
+ * `RPCProcedure<unknown>` and a non-generic signature would reject
51
+ * every typed procedure. Decoding stays a runtime concern.
20
52
  */
21
53
  register(procedure) {
22
54
  this.registry.register(procedure);
@@ -24,29 +56,31 @@ export class RPCServer {
24
56
  }
25
57
  /**
26
58
  * Handles an incoming RPC request.
59
+ *
60
+ * The frame is validated before dispatch, and every failure is mapped
61
+ * to a typed wire code. Unexpected errors are reported to
62
+ * `onInternalError` and answered with a fixed message: internal
63
+ * exception text can name hosts, paths, credentials or queries, and
64
+ * the caller is an untrusted peer.
27
65
  */
28
66
  async handle(request) {
67
+ const requestId = typeof request?.id === "string"
68
+ ? request.id
69
+ : "";
29
70
  try {
71
+ assertValidRequest(request, this.options.limits);
30
72
  return await this.dispatcher.dispatch(request);
31
73
  }
32
74
  catch (error) {
33
- if (error instanceof RPCProcedureNotFoundError) {
34
- return createRPCErrorResponse(request.id, {
35
- code: "RPC_PROCEDURE_NOT_FOUND",
36
- message: error.message,
75
+ const mapped = this.mapError(error);
76
+ if (mapped === undefined) {
77
+ this.options.onInternalError?.(error, requestId);
78
+ return createRPCErrorResponse(requestId, {
79
+ code: "RPC_INTERNAL_ERROR",
80
+ message: INTERNAL_ERROR_MESSAGE,
37
81
  });
38
82
  }
39
- if (error instanceof RPCValidationError) {
40
- return createRPCErrorResponse(request.id, {
41
- code: "RPC_VALIDATION_ERROR",
42
- message: error.message,
43
- details: error.issues,
44
- });
45
- }
46
- return createRPCErrorResponse(request.id, {
47
- code: "RPC_INTERNAL_ERROR",
48
- message: error instanceof Error ? error.message : "Unknown error",
49
- });
83
+ return createRPCErrorResponse(requestId, mapped);
50
84
  }
51
85
  }
52
86
  /**
@@ -55,5 +89,35 @@ export class RPCServer {
55
89
  getRegistry() {
56
90
  return this.registry;
57
91
  }
92
+ /**
93
+ * Maps a known error onto a wire payload.
94
+ *
95
+ * Returns `undefined` for anything unrecognised, which the caller
96
+ * answers with a generic internal error.
97
+ */
98
+ mapError(error) {
99
+ for (const [type, code] of ERROR_CODES) {
100
+ if (error instanceof type) {
101
+ const payload = {
102
+ code,
103
+ message: error.message,
104
+ };
105
+ if (error instanceof RPCValidationError && error.issues !== undefined) {
106
+ payload.details = error.issues;
107
+ }
108
+ if (error instanceof RPCRateLimitedError &&
109
+ error.retryAfter !== undefined) {
110
+ payload.details = { retryAfter: error.retryAfter };
111
+ }
112
+ return payload;
113
+ }
114
+ }
115
+ // A custom RPCError subclass is still a deliberate, caller-facing
116
+ // error; honour its own code rather than hiding it.
117
+ if (isRPCError(error)) {
118
+ return { code: error.code, message: error.message };
119
+ }
120
+ return undefined;
121
+ }
58
122
  }
59
123
  //# sourceMappingURL=rpcServer.core.js.map
@@ -2,6 +2,15 @@
2
2
  * @zudojs/rpc/streaming
3
3
  *
4
4
  * Streaming types for RPC operations.
5
+ *
6
+ * @remarks
7
+ * These types describe a streaming contract but are **not yet
8
+ * dispatchable**: `RPCProcedureRegistry` and `RPCServer` accept only
9
+ * `RPCProcedure`, and `RPCTransport.send` resolves with a single
10
+ * `RPCResponse` rather than a stream. A procedure built with
11
+ * {@link createRPCStreamingProcedure} therefore cannot be registered or
12
+ * called over a transport today. Use it to shape a handler ahead of
13
+ * transport support; do not rely on it to serve traffic.
5
14
  */
6
15
  /**
7
16
  * A streaming RPC procedure handler.
@@ -2,6 +2,15 @@
2
2
  * @zudojs/rpc/streaming
3
3
  *
4
4
  * Streaming types for RPC operations.
5
+ *
6
+ * @remarks
7
+ * These types describe a streaming contract but are **not yet
8
+ * dispatchable**: `RPCProcedureRegistry` and `RPCServer` accept only
9
+ * `RPCProcedure`, and `RPCTransport.send` resolves with a single
10
+ * `RPCResponse` rather than a stream. A procedure built with
11
+ * {@link createRPCStreamingProcedure} therefore cannot be registered or
12
+ * called over a transport today. Use it to shape a handler ahead of
13
+ * transport support; do not rely on it to serve traffic.
5
14
  */
6
15
  /**
7
16
  * Creates a streaming RPC procedure.
@@ -11,7 +11,15 @@ export interface RPCTransportRequestOptions {
11
11
  * Transport interface for sending and receiving RPC messages.
12
12
  */
13
13
  export interface RPCTransport {
14
- send(request: RPCRequest): Promise<RPCResponse>;
14
+ /**
15
+ * Sends a request and resolves with the peer's response.
16
+ *
17
+ * `options.signal` aborts the in-flight send. A transport that ignores
18
+ * it still works — the client races the signal itself — but the
19
+ * underlying connection is only released by a transport that honours
20
+ * it.
21
+ */
22
+ send(request: RPCRequest, options?: RPCTransportRequestOptions): Promise<RPCResponse>;
15
23
  close?(): Promise<void>;
16
24
  }
17
25
  //# sourceMappingURL=rpcTransport.type.d.ts.map
@@ -0,0 +1,3 @@
1
+ export type { RPCRequestLimits, RPCSchema } from "./rpcValidation.core.js";
2
+ export { assertValidProcedureName, assertValidRequest, measurePayloadBytes, toValidationIssues, parseInput, parseOutput, } from "./rpcValidation.core.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ export { assertValidProcedureName, assertValidRequest, measurePayloadBytes, toValidationIssues, parseInput, parseOutput, } from "./rpcValidation.core.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @zudojs/rpc/validation
3
+ *
4
+ * Request-level checks applied before a payload reaches a handler.
5
+ *
6
+ * Requests arrive from an untrusted peer over a transport, so these are
7
+ * the first checks the server runs, not the last.
8
+ */
9
+ import type { Schema, SchemaIssue } from "@zudojs/schema";
10
+ import type { RPCRequest } from "../types/rpcRequest.type.js";
11
+ /**
12
+ * Limits applied to an incoming request.
13
+ */
14
+ export interface RPCRequestLimits {
15
+ /**
16
+ * Maximum encoded payload size in bytes. Defaults to
17
+ * {@link MAX_RPC_PAYLOAD_SIZE}. Set to `0` to skip the check when the
18
+ * transport already enforces a frame limit.
19
+ */
20
+ readonly maxPayloadBytes?: number;
21
+ /**
22
+ * Whether procedure names must match {@link PROCEDURE_NAME_PATTERN}.
23
+ * Defaults to `true`.
24
+ */
25
+ readonly enforceProcedureNamePattern?: boolean;
26
+ }
27
+ /**
28
+ * Validates a procedure name.
29
+ *
30
+ * Length is checked before the pattern so a pathological name cannot
31
+ * drive regex backtracking.
32
+ */
33
+ export declare function assertValidProcedureName(name: unknown): asserts name is string;
34
+ /**
35
+ * Measures the encoded size of a payload in bytes.
36
+ *
37
+ * Returns `undefined` for a payload that cannot be encoded, which the
38
+ * caller reports as an invalid request rather than passing on.
39
+ */
40
+ export declare function measurePayloadBytes(payload: unknown): number | undefined;
41
+ /**
42
+ * Validates the shape and size of an incoming request.
43
+ *
44
+ * @throws {RPCInvalidRequestError} when the frame is malformed or the
45
+ * payload exceeds the configured limit.
46
+ */
47
+ export declare function assertValidRequest(request: unknown, limits?: RPCRequestLimits): asserts request is RPCRequest;
48
+ /**
49
+ * Converts schema issues into metadata safe to return to a caller.
50
+ *
51
+ * Only the path, code and message travel; the received input is dropped
52
+ * so a validation response cannot echo back data the caller sent.
53
+ */
54
+ export declare function toValidationIssues(issues: readonly SchemaIssue[]): readonly {
55
+ path: string;
56
+ code: string;
57
+ message: string;
58
+ }[];
59
+ /**
60
+ * The part of a `@zudojs/schema` Schema this package depends on.
61
+ *
62
+ * Structural so any schema-like validator can be supplied, and so the
63
+ * dependency stays to the parse contract rather than the class.
64
+ */
65
+ export type RPCSchema<TOutput = unknown> = Pick<Schema<TOutput, never>, "safeParse">;
66
+ /**
67
+ * Parses caller input against a schema.
68
+ *
69
+ * @throws {RPCValidationError} carrying the issues, which are safe to
70
+ * return: they describe the caller's own input.
71
+ */
72
+ export declare function parseInput<T>(schema: RPCSchema<T>, value: unknown, procedureName: string): T;
73
+ /**
74
+ * Parses a handler's result against its output schema.
75
+ *
76
+ * An invalid output is a server defect rather than a caller mistake, so
77
+ * the issues describe internal shape and stay server-side: the thrown
78
+ * error carries no issue detail.
79
+ */
80
+ export declare function parseOutput<T>(schema: RPCSchema<T>, value: unknown, procedureName: string): T;
81
+ //# sourceMappingURL=rpcValidation.core.d.ts.map