@zudojs/rpc 0.1.0 → 1.1.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 (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +104 -12
  3. package/dist/index.d.ts +8 -4
  4. package/dist/index.js +3 -4
  5. package/dist/rpc/client/index.d.ts +1 -1
  6. package/dist/rpc/client/rpcClient.core.d.ts +57 -3
  7. package/dist/rpc/client/rpcClient.core.js +189 -32
  8. package/dist/rpc/constants/index.d.ts +1 -1
  9. package/dist/rpc/constants/index.js +1 -1
  10. package/dist/rpc/constants/rpcConstants.core.d.ts +18 -0
  11. package/dist/rpc/constants/rpcConstants.core.js +18 -0
  12. package/dist/rpc/context/rpcContext.type.js +3 -1
  13. package/dist/rpc/dispatcher/index.d.ts +1 -0
  14. package/dist/rpc/dispatcher/rpcDispatcher.core.d.ts +49 -2
  15. package/dist/rpc/dispatcher/rpcDispatcher.core.js +109 -14
  16. package/dist/rpc/middleware/rpcMiddleware.core.d.ts +12 -0
  17. package/dist/rpc/middleware/rpcMiddleware.core.js +29 -6
  18. package/dist/rpc/procedure/rpcProcedure.type.d.ts +25 -3
  19. package/dist/rpc/procedure/rpcProcedure.type.js +9 -0
  20. package/dist/rpc/procedure/rpcProcedureRegistry.core.d.ts +15 -1
  21. package/dist/rpc/procedure/rpcProcedureRegistry.core.js +26 -1
  22. package/dist/rpc/procedure/rpcProcedureRouter.core.d.ts +1 -1
  23. package/dist/rpc/procedure/rpcProcedureRouter.core.js +5 -0
  24. package/dist/rpc/reliability/cancellation/rpcCancellation.helper.d.ts +30 -3
  25. package/dist/rpc/reliability/cancellation/rpcCancellation.helper.js +60 -4
  26. package/dist/rpc/reliability/deadline/rpcDeadline.helper.d.ts +14 -1
  27. package/dist/rpc/reliability/deadline/rpcDeadline.helper.js +23 -2
  28. package/dist/rpc/reliability/index.d.ts +5 -4
  29. package/dist/rpc/reliability/index.js +3 -3
  30. package/dist/rpc/reliability/retry/rpcRetry.helper.d.ts +22 -0
  31. package/dist/rpc/reliability/retry/rpcRetry.helper.js +80 -8
  32. package/dist/rpc/reliability/timeout/rpcTimeout.helper.d.ts +18 -2
  33. package/dist/rpc/reliability/timeout/rpcTimeout.helper.js +71 -7
  34. package/dist/rpc/server/index.d.ts +1 -0
  35. package/dist/rpc/server/rpcServer.core.d.ts +41 -2
  36. package/dist/rpc/server/rpcServer.core.js +117 -16
  37. package/dist/rpc/streaming/rpcStreaming.type.d.ts +9 -0
  38. package/dist/rpc/streaming/rpcStreaming.type.js +9 -0
  39. package/dist/rpc/transport/rpcTransport.type.d.ts +9 -1
  40. package/dist/rpc/validation/index.d.ts +3 -0
  41. package/dist/rpc/validation/index.js +2 -0
  42. package/dist/rpc/validation/rpcValidation.core.d.ts +86 -0
  43. package/dist/rpc/validation/rpcValidation.core.js +133 -0
  44. package/package.json +27 -16
  45. package/dist/.tsbuildinfo +0 -1
  46. package/dist/index.d.ts.map +0 -1
  47. package/dist/index.js.map +0 -1
  48. package/dist/rpc/client/index.d.ts.map +0 -1
  49. package/dist/rpc/client/index.js.map +0 -1
  50. package/dist/rpc/client/rpcClient.core.d.ts.map +0 -1
  51. package/dist/rpc/client/rpcClient.core.js.map +0 -1
  52. package/dist/rpc/constants/index.d.ts.map +0 -1
  53. package/dist/rpc/constants/index.js.map +0 -1
  54. package/dist/rpc/constants/rpcConstants.core.d.ts.map +0 -1
  55. package/dist/rpc/constants/rpcConstants.core.js.map +0 -1
  56. package/dist/rpc/context/index.d.ts.map +0 -1
  57. package/dist/rpc/context/index.js.map +0 -1
  58. package/dist/rpc/context/rpcContext.type.d.ts.map +0 -1
  59. package/dist/rpc/context/rpcContext.type.js.map +0 -1
  60. package/dist/rpc/dispatcher/index.d.ts.map +0 -1
  61. package/dist/rpc/dispatcher/index.js.map +0 -1
  62. package/dist/rpc/dispatcher/rpcDispatcher.core.d.ts.map +0 -1
  63. package/dist/rpc/dispatcher/rpcDispatcher.core.js.map +0 -1
  64. package/dist/rpc/errors/index.d.ts.map +0 -1
  65. package/dist/rpc/errors/index.js.map +0 -1
  66. package/dist/rpc/errors/rpc.errors.d.ts.map +0 -1
  67. package/dist/rpc/errors/rpc.errors.js.map +0 -1
  68. package/dist/rpc/interceptor/index.d.ts.map +0 -1
  69. package/dist/rpc/interceptor/index.js.map +0 -1
  70. package/dist/rpc/interceptor/rpcInterceptor.type.d.ts.map +0 -1
  71. package/dist/rpc/interceptor/rpcInterceptor.type.js.map +0 -1
  72. package/dist/rpc/middleware/index.d.ts.map +0 -1
  73. package/dist/rpc/middleware/index.js.map +0 -1
  74. package/dist/rpc/middleware/rpcMiddleware.core.d.ts.map +0 -1
  75. package/dist/rpc/middleware/rpcMiddleware.core.js.map +0 -1
  76. package/dist/rpc/procedure/index.d.ts.map +0 -1
  77. package/dist/rpc/procedure/index.js.map +0 -1
  78. package/dist/rpc/procedure/rpcProcedure.type.d.ts.map +0 -1
  79. package/dist/rpc/procedure/rpcProcedure.type.js.map +0 -1
  80. package/dist/rpc/procedure/rpcProcedureRegistry.core.d.ts.map +0 -1
  81. package/dist/rpc/procedure/rpcProcedureRegistry.core.js.map +0 -1
  82. package/dist/rpc/procedure/rpcProcedureRouter.core.d.ts.map +0 -1
  83. package/dist/rpc/procedure/rpcProcedureRouter.core.js.map +0 -1
  84. package/dist/rpc/reliability/cancellation/rpcCancellation.helper.d.ts.map +0 -1
  85. package/dist/rpc/reliability/cancellation/rpcCancellation.helper.js.map +0 -1
  86. package/dist/rpc/reliability/deadline/rpcDeadline.helper.d.ts.map +0 -1
  87. package/dist/rpc/reliability/deadline/rpcDeadline.helper.js.map +0 -1
  88. package/dist/rpc/reliability/index.d.ts.map +0 -1
  89. package/dist/rpc/reliability/index.js.map +0 -1
  90. package/dist/rpc/reliability/retry/rpcRetry.helper.d.ts.map +0 -1
  91. package/dist/rpc/reliability/retry/rpcRetry.helper.js.map +0 -1
  92. package/dist/rpc/reliability/timeout/rpcTimeout.helper.d.ts.map +0 -1
  93. package/dist/rpc/reliability/timeout/rpcTimeout.helper.js.map +0 -1
  94. package/dist/rpc/server/index.d.ts.map +0 -1
  95. package/dist/rpc/server/index.js.map +0 -1
  96. package/dist/rpc/server/rpcServer.core.d.ts.map +0 -1
  97. package/dist/rpc/server/rpcServer.core.js.map +0 -1
  98. package/dist/rpc/streaming/index.d.ts.map +0 -1
  99. package/dist/rpc/streaming/index.js.map +0 -1
  100. package/dist/rpc/streaming/rpcStreaming.type.d.ts.map +0 -1
  101. package/dist/rpc/streaming/rpcStreaming.type.js.map +0 -1
  102. package/dist/rpc/transport/index.d.ts.map +0 -1
  103. package/dist/rpc/transport/index.js.map +0 -1
  104. package/dist/rpc/transport/rpcTransport.type.d.ts.map +0 -1
  105. package/dist/rpc/transport/rpcTransport.type.js.map +0 -1
  106. package/dist/rpc/types/index.d.ts.map +0 -1
  107. package/dist/rpc/types/index.js.map +0 -1
  108. package/dist/rpc/types/rpcMetadata.type.d.ts.map +0 -1
  109. package/dist/rpc/types/rpcMetadata.type.js.map +0 -1
  110. package/dist/rpc/types/rpcProcedureName.type.d.ts.map +0 -1
  111. package/dist/rpc/types/rpcProcedureName.type.js.map +0 -1
  112. package/dist/rpc/types/rpcRequest.type.d.ts.map +0 -1
  113. package/dist/rpc/types/rpcRequest.type.js.map +0 -1
  114. package/dist/rpc/types/rpcResponse.type.d.ts.map +0 -1
  115. package/dist/rpc/types/rpcResponse.type.js.map +0 -1
  116. package/dist/testing/index.d.ts.map +0 -1
  117. package/dist/testing/index.js.map +0 -1
@@ -1,6 +1,7 @@
1
- export { createTimeout, withTimeout } from "./timeout/rpcTimeout.helper.js";
2
- export { getRemainingTime, isDeadlineExceeded, throwIfDeadlineExceeded, } from "./deadline/rpcDeadline.helper.js";
3
- export { createCancellableSignal, cancelSignal, } from "./cancellation/rpcCancellation.helper.js";
4
- export type { RPCBackoff, RPCRetryOptions } from "./retry/rpcRetry.helper.js";
1
+ export { createTimeout, withTimeout, runWithTimeout, } from "./timeout/rpcTimeout.helper.js";
2
+ export { getRemainingTime, isDeadlineExceeded, throwIfDeadlineExceeded, readDeadline, } from "./deadline/rpcDeadline.helper.js";
3
+ export type { CancellableSignal } from "./cancellation/rpcCancellation.helper.js";
4
+ export { createCancellableSignal, cancelSignal, throwIfCancelled, combineSignals, } from "./cancellation/rpcCancellation.helper.js";
5
+ export type { RPCBackoff, RPCJitter, RPCRetryOptions, } from "./retry/rpcRetry.helper.js";
5
6
  export { DEFAULT_RETRY_OPTIONS, calculateRetryDelay, retry, } from "./retry/rpcRetry.helper.js";
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,5 @@
1
- export { createTimeout, withTimeout } from "./timeout/rpcTimeout.helper.js";
2
- export { getRemainingTime, isDeadlineExceeded, throwIfDeadlineExceeded, } from "./deadline/rpcDeadline.helper.js";
3
- export { createCancellableSignal, cancelSignal, } from "./cancellation/rpcCancellation.helper.js";
1
+ export { createTimeout, withTimeout, runWithTimeout, } from "./timeout/rpcTimeout.helper.js";
2
+ export { getRemainingTime, isDeadlineExceeded, throwIfDeadlineExceeded, readDeadline, } from "./deadline/rpcDeadline.helper.js";
3
+ export { createCancellableSignal, cancelSignal, throwIfCancelled, combineSignals, } from "./cancellation/rpcCancellation.helper.js";
4
4
  export { DEFAULT_RETRY_OPTIONS, calculateRetryDelay, retry, } from "./retry/rpcRetry.helper.js";
5
5
  //# sourceMappingURL=index.js.map
@@ -7,6 +7,10 @@
7
7
  * Retry backoff strategies.
8
8
  */
9
9
  export type RPCBackoff = "fixed" | "linear" | "exponential";
10
+ /**
11
+ * Randomisation applied to a computed retry delay.
12
+ */
13
+ export type RPCJitter = "none" | "full" | "equal";
10
14
  /**
11
15
  * Retry options.
12
16
  */
@@ -15,7 +19,17 @@ export interface RPCRetryOptions {
15
19
  readonly delay: number;
16
20
  readonly maxDelay?: number;
17
21
  readonly backoff?: RPCBackoff;
22
+ /**
23
+ * Randomisation applied to each delay. Defaults to `"full"`, which
24
+ * spreads retries over `[0, delay]`. Without jitter every client
25
+ * retrying the same failed dependency retries at the same instant.
26
+ */
27
+ readonly jitter?: RPCJitter;
18
28
  readonly retryIf?: (error: unknown) => boolean;
29
+ /** Aborts the retry sequence, including while waiting between tries. */
30
+ readonly signal?: AbortSignal;
31
+ /** Invoked before each delay, for logging and metrics. */
32
+ readonly onRetry?: (error: unknown, attempt: number, delay: number) => void;
19
33
  }
20
34
  /**
21
35
  * Default retry options.
@@ -23,10 +37,18 @@ export interface RPCRetryOptions {
23
37
  export declare const DEFAULT_RETRY_OPTIONS: RPCRetryOptions;
24
38
  /**
25
39
  * Calculates the delay for a retry attempt.
40
+ *
41
+ * The result is finite, non-negative, and no greater than a timer can
42
+ * represent. Jitter defaults to `"none"` here so the calculation stays
43
+ * predictable for callers that compose it; {@link retry} opts into
44
+ * `"full"` jitter by default.
26
45
  */
27
46
  export declare function calculateRetryDelay(attempt: number, options: RPCRetryOptions): number;
28
47
  /**
29
48
  * Retries an asynchronous operation.
49
+ *
50
+ * Jitter defaults to `"full"` so concurrent callers retrying the same
51
+ * failing dependency do not synchronise into a retry wave.
30
52
  */
31
53
  export declare function retry<T>(operation: () => Promise<T>, options: RPCRetryOptions): Promise<T>;
32
54
  //# sourceMappingURL=rpcRetry.helper.d.ts.map
@@ -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,33 @@ 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, RPCInternalError, 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
+ // 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
+ ];
6
32
  /**
7
33
  * RPC server that receives and dispatches requests.
8
34
  */
@@ -10,13 +36,22 @@ export class RPCServer {
10
36
  registry;
11
37
  middleware;
12
38
  dispatcher;
13
- constructor(registry, middleware) {
39
+ options;
40
+ constructor(registry, middleware, options = {}) {
14
41
  this.registry = registry ?? new RPCProcedureRegistry();
15
42
  this.middleware = middleware ?? new RPCMiddlewareStack();
16
- this.dispatcher = new RPCDispatcher(this.registry, this.middleware);
43
+ this.options = options;
44
+ this.dispatcher = new RPCDispatcher(this.registry, this.middleware, options.dispatch ?? {});
17
45
  }
18
46
  /**
19
47
  * Registers a procedure.
48
+ *
49
+ * Generic over the procedure's input and output so a procedure built
50
+ * with `createRPCProcedure<TInput, TOutput>` can be registered
51
+ * directly. `RPCProcedure` is contravariant in `TInput` (its handler
52
+ * accepts that input), so a typed procedure is NOT assignable to
53
+ * `RPCProcedure<unknown>` and a non-generic signature would reject
54
+ * every typed procedure. Decoding stays a runtime concern.
20
55
  */
21
56
  register(procedure) {
22
57
  this.registry.register(procedure);
@@ -24,28 +59,49 @@ export class RPCServer {
24
59
  }
25
60
  /**
26
61
  * Handles an incoming RPC request.
62
+ *
63
+ * The frame is validated before dispatch, and every failure is mapped
64
+ * to a typed wire code. Unexpected errors are reported to
65
+ * `onInternalError` and answered with a fixed message: internal
66
+ * exception text can name hosts, paths, credentials or queries, and
67
+ * the caller is an untrusted peer.
27
68
  */
28
69
  async handle(request) {
70
+ const requestId = typeof request?.id === "string"
71
+ ? request.id
72
+ : "";
29
73
  try {
30
- return await this.dispatcher.dispatch(request);
74
+ assertValidRequest(request, this.options.limits);
75
+ // `metadata` is optional on the wire (`createRPCRequest` fills it in,
76
+ // a hand-built or JSON-decoded frame need not). Every consumer below
77
+ // reads it as an object, so a frame without it used to fail with a
78
+ // TypeError reported as an internal error.
79
+ const frame = request.metadata === undefined ? { ...request, metadata: {} } : request;
80
+ return await this.dispatcher.dispatch(frame);
31
81
  }
32
82
  catch (error) {
33
- if (error instanceof RPCProcedureNotFoundError) {
34
- return createRPCErrorResponse(request.id, {
35
- code: "RPC_PROCEDURE_NOT_FOUND",
36
- message: error.message,
83
+ const mapped = this.mapError(error);
84
+ if (mapped === undefined) {
85
+ this.options.onInternalError?.(error, requestId);
86
+ return createRPCErrorResponse(requestId, {
87
+ code: "RPC_INTERNAL_ERROR",
88
+ message: INTERNAL_ERROR_MESSAGE,
37
89
  });
38
90
  }
39
- if (error instanceof RPCValidationError) {
40
- return createRPCErrorResponse(request.id, {
41
- code: "RPC_VALIDATION_ERROR",
42
- message: error.message,
43
- details: error.issues,
91
+ if (mapped.internal) {
92
+ // The error is typed, so the caller keeps its code, but it was
93
+ // constructed with `expose: false`: its message is server detail
94
+ // and goes to the log, not the wire.
95
+ this.options.onInternalError?.(error, requestId);
96
+ return createRPCErrorResponse(requestId, {
97
+ code: mapped.code,
98
+ message: INTERNAL_ERROR_MESSAGE,
44
99
  });
45
100
  }
46
- return createRPCErrorResponse(request.id, {
47
- code: "RPC_INTERNAL_ERROR",
48
- message: error instanceof Error ? error.message : "Unknown error",
101
+ return createRPCErrorResponse(requestId, {
102
+ code: mapped.code,
103
+ message: mapped.message,
104
+ ...(mapped.details !== undefined ? { details: mapped.details } : {}),
49
105
  });
50
106
  }
51
107
  }
@@ -55,5 +111,50 @@ export class RPCServer {
55
111
  getRegistry() {
56
112
  return this.registry;
57
113
  }
114
+ /**
115
+ * Maps a known error onto a wire payload.
116
+ *
117
+ * Returns `undefined` for anything unrecognised, which the caller
118
+ * answers with a generic internal error.
119
+ */
120
+ mapError(error) {
121
+ // An RPCInternalError *is* the generic internal failure: it carries
122
+ // `expose: false` and a message written for the log. Mapping it like
123
+ // any other RPCError put that message on the wire.
124
+ if (error instanceof RPCInternalError) {
125
+ return undefined;
126
+ }
127
+ for (const [type, code, internal] of ERROR_CODES) {
128
+ if (error instanceof type) {
129
+ const payload = {
130
+ code,
131
+ message: error.message,
132
+ ...(internal ? { internal: true } : {}),
133
+ };
134
+ if (error instanceof RPCValidationError &&
135
+ error.issues !== undefined &&
136
+ error.issues.length > 0) {
137
+ payload.details = error.issues;
138
+ }
139
+ if (error instanceof RPCRateLimitedError &&
140
+ error.retryAfter !== undefined) {
141
+ payload.details = { retryAfter: error.retryAfter };
142
+ }
143
+ return payload;
144
+ }
145
+ }
146
+ // A custom RPCError subclass is still a deliberate, caller-facing
147
+ // error; honour its own code rather than hiding it. Its message only
148
+ // travels when the error was built to be exposed — `RPCError`
149
+ // defaults to `expose: false`.
150
+ if (isRPCError(error)) {
151
+ return {
152
+ code: error.code,
153
+ message: error.message,
154
+ internal: error.expose === false,
155
+ };
156
+ }
157
+ return undefined;
158
+ }
58
159
  }
59
160
  //# 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