@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
@@ -2,18 +2,65 @@ import type { RPCRequest } from "../types/rpcRequest.type.js";
2
2
  import type { RPCResponse } from "../types/rpcResponse.type.js";
3
3
  import type { RPCProcedure } from "../procedure/rpcProcedure.type.js";
4
4
  import type { RPCMiddlewareStack } from "../middleware/rpcMiddleware.core.js";
5
+ import type { RPCInterceptor } from "../interceptor/rpcInterceptor.type.js";
6
+ /**
7
+ * Options controlling dispatch.
8
+ */
9
+ export interface RPCDispatcherOptions {
10
+ /**
11
+ * Timeout applied to a procedure that declares none, in milliseconds.
12
+ * Defaults to {@link DEFAULT_RPC_TIMEOUT}. Set to `0` to leave
13
+ * procedures without their own timeout unbounded.
14
+ */
15
+ readonly defaultTimeout?: number;
16
+ /**
17
+ * Whether a `deadline` in request metadata caps the effective timeout.
18
+ * Defaults to `true`.
19
+ */
20
+ readonly honourDeadline?: boolean;
21
+ /**
22
+ * Interceptors wrapping every dispatch, outermost first.
23
+ *
24
+ * They run around the middleware stack and the handler, so an
25
+ * interceptor observes validation and middleware as part of the call
26
+ * it wraps.
27
+ */
28
+ readonly interceptors?: readonly RPCInterceptor[];
29
+ }
5
30
  /**
6
31
  * Dispatches RPC requests to registered procedures.
32
+ *
33
+ * Every dispatch runs under a real `AbortController`, a bounded timeout,
34
+ * and — when the procedure declares schemas — input and output
35
+ * validation. Errors keep their identity: an `RPCError` thrown by a
36
+ * middleware or handler propagates unchanged so the server can map it to
37
+ * the right response code, and only genuinely unexpected errors are
38
+ * wrapped.
7
39
  */
8
40
  export declare class RPCDispatcher {
9
41
  private readonly registry;
10
42
  private readonly middleware;
43
+ private readonly options;
11
44
  constructor(registry: {
12
45
  require(name: string): RPCProcedure;
13
- }, middleware: RPCMiddlewareStack);
46
+ }, middleware: RPCMiddlewareStack, options?: RPCDispatcherOptions);
14
47
  /**
15
48
  * Dispatches an RPC request.
16
49
  */
17
- dispatch(request: RPCRequest): Promise<RPCResponse>;
50
+ dispatch(input: RPCRequest): Promise<RPCResponse>;
51
+ /**
52
+ * Runs the interceptor chain around the dispatch.
53
+ *
54
+ * Each interceptor may call `next()` once; a second call throws rather
55
+ * than re-running the handler.
56
+ */
57
+ private applyInterceptors;
58
+ /**
59
+ * Resolves the timeout for a dispatch.
60
+ *
61
+ * A `deadline` in request metadata can only shorten the timeout, never
62
+ * extend it past what the procedure allows.
63
+ */
64
+ private resolveTimeout;
18
65
  }
19
66
  //# sourceMappingURL=rpcDispatcher.core.d.ts.map
@@ -1,38 +1,133 @@
1
1
  import { createRPCContext } from "../context/rpcContext.type.js";
2
2
  import { createRPCResponse } from "../types/rpcResponse.type.js";
3
- import { RPCProcedureNotFoundError, RPCValidationError, } from "../errors/rpc.errors.js";
3
+ import { RPCCancelledError } from "../errors/rpc.errors.js";
4
+ import { DEFAULT_RPC_TIMEOUT } from "../constants/rpcConstants.core.js";
5
+ import { parseInput, parseOutput } from "../validation/rpcValidation.core.js";
6
+ import { readDeadline, throwIfDeadlineExceeded, } from "../reliability/deadline/rpcDeadline.helper.js";
7
+ import { createTimeout } from "../reliability/timeout/rpcTimeout.helper.js";
4
8
  /**
5
9
  * Dispatches RPC requests to registered procedures.
10
+ *
11
+ * Every dispatch runs under a real `AbortController`, a bounded timeout,
12
+ * and — when the procedure declares schemas — input and output
13
+ * validation. Errors keep their identity: an `RPCError` thrown by a
14
+ * middleware or handler propagates unchanged so the server can map it to
15
+ * the right response code, and only genuinely unexpected errors are
16
+ * wrapped.
6
17
  */
7
18
  export class RPCDispatcher {
8
19
  registry;
9
20
  middleware;
10
- constructor(registry, middleware) {
21
+ options;
22
+ constructor(registry, middleware, options = {}) {
11
23
  this.registry = registry;
12
24
  this.middleware = middleware;
25
+ this.options = options;
13
26
  }
14
27
  /**
15
28
  * Dispatches an RPC request.
16
29
  */
17
- async dispatch(request) {
30
+ async dispatch(input) {
31
+ // Tolerate a frame without `metadata`: the field is optional when a
32
+ // request is built by hand or decoded from JSON, and everything below
33
+ // — deadline reading, the context's `metadata` — reads it as an object.
34
+ const request = input.metadata === undefined ? { ...input, metadata: {} } : input;
18
35
  const procedure = this.registry.require(request.procedure);
19
- const signal = new AbortController().signal;
20
- const context = createRPCContext(request, signal);
36
+ const controller = new AbortController();
37
+ const context = createRPCContext(request, controller.signal);
38
+ const timeoutMs = this.resolveTimeout(request, procedure);
39
+ // A deadline already in the past is rejected before any work runs.
40
+ if (this.options.honourDeadline ?? true) {
41
+ const deadline = readDeadline(request.metadata);
42
+ if (deadline !== undefined) {
43
+ throwIfDeadlineExceeded(deadline, request.procedure);
44
+ }
45
+ }
46
+ const timeout = timeoutMs > 0 ? createTimeout(timeoutMs, request.procedure) : undefined;
21
47
  try {
22
- const result = await this.middleware.execute(context, async () => {
23
- return procedure.handler(request.payload, context);
24
- });
48
+ const run = async () => {
49
+ const input = procedure.options?.input
50
+ ? parseInput(procedure.options.input, request.payload, request.procedure)
51
+ : request.payload;
52
+ const result = await this.middleware.execute(context, async () => {
53
+ return procedure.handler(input, context);
54
+ });
55
+ return procedure.options?.output
56
+ ? parseOutput(procedure.options.output, result, request.procedure)
57
+ : result;
58
+ };
59
+ const invoke = () => this.applyInterceptors(context, run);
60
+ const result = timeout === undefined
61
+ ? await invoke()
62
+ : await Promise.race([
63
+ invoke(),
64
+ timeout.promise.catch((error) => {
65
+ // Abort first so a cooperative handler stops working
66
+ // instead of running on with its result discarded.
67
+ if (!controller.signal.aborted) {
68
+ controller.abort(error);
69
+ }
70
+ throw error;
71
+ }),
72
+ ]);
25
73
  return createRPCResponse(request.id, result);
26
74
  }
27
- catch (error) {
28
- if (error instanceof RPCProcedureNotFoundError) {
29
- throw error;
75
+ finally {
76
+ // Errors propagate unchanged. Rewriting them here is what turned
77
+ // an auth failure or a rate limit into a generic internal fault;
78
+ // the server owns the mapping from error type to response code.
79
+ timeout?.cancel();
80
+ if (!controller.signal.aborted) {
81
+ // Release anything still listening on the request signal.
82
+ controller.abort(new RPCCancelledError("Request completed.", request.procedure));
30
83
  }
31
- if (error instanceof RPCValidationError) {
32
- throw error;
84
+ }
85
+ }
86
+ /**
87
+ * Runs the interceptor chain around the dispatch.
88
+ *
89
+ * Each interceptor may call `next()` once; a second call throws rather
90
+ * than re-running the handler.
91
+ */
92
+ async applyInterceptors(context, run) {
93
+ const interceptors = this.options.interceptors;
94
+ if (!interceptors || interceptors.length === 0) {
95
+ return run();
96
+ }
97
+ const called = new Set();
98
+ const runAt = async (index) => {
99
+ if (called.has(index)) {
100
+ throw new Error(`RPC interceptor at index ${index - 1} called next() more than once.`);
101
+ }
102
+ called.add(index);
103
+ if (index >= interceptors.length) {
104
+ return run();
105
+ }
106
+ const interceptor = interceptors[index];
107
+ return interceptor.intercept(context, () => runAt(index + 1));
108
+ };
109
+ return runAt(0);
110
+ }
111
+ /**
112
+ * Resolves the timeout for a dispatch.
113
+ *
114
+ * A `deadline` in request metadata can only shorten the timeout, never
115
+ * extend it past what the procedure allows.
116
+ */
117
+ resolveTimeout(request, procedure) {
118
+ const configured = procedure.options?.timeout ??
119
+ this.options.defaultTimeout ??
120
+ DEFAULT_RPC_TIMEOUT;
121
+ if (configured <= 0) {
122
+ return 0;
123
+ }
124
+ if (this.options.honourDeadline ?? true) {
125
+ const deadline = readDeadline(request.metadata);
126
+ if (deadline !== undefined) {
127
+ return Math.max(1, Math.min(configured, deadline - Date.now()));
33
128
  }
34
- throw new Error(`Handler for procedure "${request.procedure}" threw: ${String(error)}`);
35
129
  }
130
+ return configured;
36
131
  }
37
132
  }
38
133
  //# sourceMappingURL=rpcDispatcher.core.js.map
@@ -9,8 +9,20 @@ export type RPCMiddleware = (context: RPCContext, next: () => Promise<unknown>)
9
9
  export declare class RPCMiddlewareStack {
10
10
  private readonly middleware;
11
11
  constructor(middleware?: RPCMiddleware[]);
12
+ /**
13
+ * Number of middleware in the stack.
14
+ */
15
+ get size(): number;
16
+ /**
17
+ * Returns a new stack with additional middleware appended.
18
+ */
19
+ with(...middleware: RPCMiddleware[]): RPCMiddlewareStack;
12
20
  /**
13
21
  * Executes the middleware stack around the given handler.
22
+ *
23
+ * Each middleware may call `next()` exactly once. A second call throws
24
+ * rather than silently re-entering the chain, which would run the
25
+ * handler — and every middleware below it — twice.
14
26
  */
15
27
  execute(context: RPCContext, handler: () => Promise<unknown>): Promise<unknown>;
16
28
  }
@@ -1,29 +1,52 @@
1
+ import { MAX_MIDDLEWARE } from "../constants/rpcConstants.core.js";
1
2
  /**
2
3
  * Stack of RPC middleware.
3
4
  */
4
5
  export class RPCMiddlewareStack {
5
6
  middleware;
6
7
  constructor(middleware = []) {
8
+ if (middleware.length > MAX_MIDDLEWARE) {
9
+ throw new RangeError(`An RPC middleware stack may hold at most ${MAX_MIDDLEWARE} entries; received ${middleware.length}.`);
10
+ }
7
11
  this.middleware = Object.freeze([...middleware]);
8
12
  }
13
+ /**
14
+ * Number of middleware in the stack.
15
+ */
16
+ get size() {
17
+ return this.middleware.length;
18
+ }
19
+ /**
20
+ * Returns a new stack with additional middleware appended.
21
+ */
22
+ with(...middleware) {
23
+ return new RPCMiddlewareStack([...this.middleware, ...middleware]);
24
+ }
9
25
  /**
10
26
  * Executes the middleware stack around the given handler.
27
+ *
28
+ * Each middleware may call `next()` exactly once. A second call throws
29
+ * rather than silently re-entering the chain, which would run the
30
+ * handler — and every middleware below it — twice.
11
31
  */
12
32
  async execute(context, handler) {
13
33
  const stack = this.middleware;
14
- let index = 0;
15
- const runNext = async () => {
34
+ const called = new Set();
35
+ const runAt = async (index) => {
36
+ if (called.has(index)) {
37
+ throw new Error(`RPC middleware at index ${index - 1} called next() more than once.`);
38
+ }
39
+ called.add(index);
16
40
  if (index >= stack.length) {
17
41
  return handler();
18
42
  }
19
43
  const mw = stack[index];
20
- index += 1;
21
44
  if (!mw) {
22
- return handler();
45
+ return runAt(index + 1);
23
46
  }
24
- return mw(context, runNext);
47
+ return mw(context, () => runAt(index + 1));
25
48
  };
26
- return runNext();
49
+ return runAt(0);
27
50
  }
28
51
  }
29
52
  //# sourceMappingURL=rpcMiddleware.core.js.map
@@ -1,5 +1,6 @@
1
1
  import type { RPCProcedureName } from "../types/rpcProcedureName.type.js";
2
2
  import type { RPCContext } from "../context/rpcContext.type.js";
3
+ import type { RPCSchema } from "../validation/rpcValidation.core.js";
3
4
  /**
4
5
  * Handler for an RPC procedure.
5
6
  */
@@ -7,10 +8,31 @@ export type RPCHandler<TInput = unknown, TOutput = unknown> = (input: TInput, co
7
8
  /**
8
9
  * Options for an RPC procedure.
9
10
  */
10
- export interface RPCProcedureOptions {
11
+ export interface RPCProcedureOptions<TInput = unknown, TOutput = unknown> {
12
+ /**
13
+ * Whether calling this procedure more than once is safe.
14
+ *
15
+ * Advisory: the server does not deduplicate or auto-retry on this
16
+ * flag. It is exposed through the registry so a caller's retry policy
17
+ * can consult it before replaying a failed call.
18
+ */
11
19
  readonly idempotent?: boolean;
20
+ /**
21
+ * Maximum time the handler may run, in milliseconds. Enforced by the
22
+ * dispatcher, which aborts the context signal when it elapses.
23
+ */
12
24
  readonly timeout?: number;
25
+ /** Human-readable description, surfaced by registry introspection. */
13
26
  readonly description?: string;
27
+ /**
28
+ * Schema the request payload is parsed against before the handler
29
+ * runs. Without one, the handler receives the raw untrusted payload.
30
+ */
31
+ readonly input?: RPCSchema<TInput>;
32
+ /**
33
+ * Schema the handler's result is checked against before it is sent.
34
+ */
35
+ readonly output?: RPCSchema<TOutput>;
14
36
  }
15
37
  /**
16
38
  * An RPC procedure definition.
@@ -18,10 +40,10 @@ export interface RPCProcedureOptions {
18
40
  export interface RPCProcedure<TInput = unknown, TOutput = unknown> {
19
41
  readonly name: RPCProcedureName;
20
42
  readonly handler: RPCHandler<TInput, TOutput>;
21
- readonly options?: RPCProcedureOptions;
43
+ readonly options?: RPCProcedureOptions<TInput, TOutput>;
22
44
  }
23
45
  /**
24
46
  * Creates a new RPC procedure.
25
47
  */
26
- export declare function createRPCProcedure<TInput = unknown, TOutput = unknown>(name: RPCProcedureName, handler: RPCHandler<TInput, TOutput>, options?: RPCProcedureOptions): RPCProcedure<TInput, TOutput>;
48
+ export declare function createRPCProcedure<TInput = unknown, TOutput = unknown>(name: RPCProcedureName, handler: RPCHandler<TInput, TOutput>, options?: RPCProcedureOptions<TInput, TOutput>): RPCProcedure<TInput, TOutput>;
27
49
  //# sourceMappingURL=rpcProcedure.type.d.ts.map
@@ -1,7 +1,16 @@
1
+ import { assertValidProcedureName } from "../validation/rpcValidation.core.js";
1
2
  /**
2
3
  * Creates a new RPC procedure.
3
4
  */
4
5
  export function createRPCProcedure(name, handler, options = {}) {
6
+ // Names are validated at definition time so a malformed name is a
7
+ // startup error rather than a request that can never be routed.
8
+ assertValidProcedureName(name);
9
+ if (options.timeout !== undefined) {
10
+ if (!Number.isFinite(options.timeout) || options.timeout <= 0) {
11
+ throw new TypeError(`Procedure "${name}" declares a non-positive timeout.`);
12
+ }
13
+ }
5
14
  return Object.freeze({
6
15
  name,
7
16
  handler,
@@ -11,7 +11,7 @@ export declare class RPCProcedureRegistry {
11
11
  *
12
12
  * @throws {RPCDuplicateProcedureError} if a procedure with the same name is already registered.
13
13
  */
14
- register(procedure: RPCProcedure): void;
14
+ register<TInput = unknown, TOutput = unknown>(procedure: RPCProcedure<TInput, TOutput>): void;
15
15
  /**
16
16
  * Retrieves a procedure by name.
17
17
  */
@@ -28,6 +28,20 @@ export declare class RPCProcedureRegistry {
28
28
  * Returns all registered procedure names.
29
29
  */
30
30
  list(): readonly string[];
31
+ /**
32
+ * Describes every registered procedure.
33
+ *
34
+ * Surfaces the `description` and `idempotent` options, which are
35
+ * otherwise carried on the procedure and never readable — a caller's
36
+ * retry policy needs `idempotent` to decide whether replaying a failed
37
+ * call is safe.
38
+ */
39
+ describe(): readonly {
40
+ name: string;
41
+ description?: string;
42
+ idempotent: boolean;
43
+ timeout?: number;
44
+ }[];
31
45
  /**
32
46
  * Unregisters a procedure.
33
47
  */
@@ -1,5 +1,6 @@
1
1
  import { RPCDuplicateProcedureError, RPCProcedureNotFoundError, } from "../errors/rpc.errors.js";
2
2
  import { MAX_PROCEDURES } from "../constants/rpcConstants.core.js";
3
+ import { assertValidProcedureName } from "../validation/rpcValidation.core.js";
3
4
  /**
4
5
  * Registry for RPC procedures.
5
6
  *
@@ -14,12 +15,16 @@ export class RPCProcedureRegistry {
14
15
  */
15
16
  register(procedure) {
16
17
  if (this.procedures.size >= MAX_PROCEDURES) {
17
- throw new Error(`Maximum number of procedures (${MAX_PROCEDURES}) exceeded.`);
18
+ throw new RangeError(`Maximum number of procedures (${MAX_PROCEDURES}) exceeded.`);
18
19
  }
20
+ assertValidProcedureName(procedure.name);
19
21
  const existing = this.procedures.get(procedure.name);
20
22
  if (existing !== undefined) {
21
23
  throw new RPCDuplicateProcedureError(procedure.name);
22
24
  }
25
+ // Stored erased. The dispatcher decodes the wire payload at runtime,
26
+ // so the registry holds procedures of mixed input types; the cast is
27
+ // the single point where that erasure is acknowledged.
23
28
  this.procedures.set(procedure.name, Object.freeze(procedure));
24
29
  }
25
30
  /**
@@ -50,6 +55,26 @@ export class RPCProcedureRegistry {
50
55
  list() {
51
56
  return Array.from(this.procedures.keys());
52
57
  }
58
+ /**
59
+ * Describes every registered procedure.
60
+ *
61
+ * Surfaces the `description` and `idempotent` options, which are
62
+ * otherwise carried on the procedure and never readable — a caller's
63
+ * retry policy needs `idempotent` to decide whether replaying a failed
64
+ * call is safe.
65
+ */
66
+ describe() {
67
+ return Array.from(this.procedures.values()).map((procedure) => ({
68
+ name: procedure.name,
69
+ ...(procedure.options?.description !== undefined
70
+ ? { description: procedure.options.description }
71
+ : {}),
72
+ idempotent: procedure.options?.idempotent ?? false,
73
+ ...(procedure.options?.timeout !== undefined
74
+ ? { timeout: procedure.options.timeout }
75
+ : {}),
76
+ }));
77
+ }
53
78
  /**
54
79
  * Unregisters a procedure.
55
80
  */
@@ -8,7 +8,7 @@ export declare class RPCProcedureRouter {
8
8
  /**
9
9
  * Registers a procedure in this router.
10
10
  */
11
- register(procedure: RPCProcedure): this;
11
+ register<TInput = unknown, TOutput = unknown>(procedure: RPCProcedure<TInput, TOutput>): this;
12
12
  /**
13
13
  * Retrieves a procedure by name.
14
14
  */
@@ -1,4 +1,5 @@
1
1
  import { RPCDuplicateProcedureError, RPCProcedureNotFoundError, } from "../errors/rpc.errors.js";
2
+ import { assertValidProcedureName } from "../validation/rpcValidation.core.js";
2
3
  /**
3
4
  * Router for grouping RPC procedures by namespace.
4
5
  */
@@ -8,10 +9,14 @@ export class RPCProcedureRouter {
8
9
  * Registers a procedure in this router.
9
10
  */
10
11
  register(procedure) {
12
+ assertValidProcedureName(procedure.name);
11
13
  const existing = this.routes.get(procedure.name);
12
14
  if (existing !== undefined) {
13
15
  throw new RPCDuplicateProcedureError(procedure.name);
14
16
  }
17
+ // Stored erased. The dispatcher decodes the wire payload at runtime,
18
+ // so the registry holds procedures of mixed input types; the cast is
19
+ // the single point where that erasure is acknowledged.
15
20
  this.routes.set(procedure.name, Object.freeze(procedure));
16
21
  return this;
17
22
  }
@@ -3,12 +3,39 @@
3
3
  *
4
4
  * Cancellation utilities for RPC operations.
5
5
  */
6
+ /**
7
+ * A signal paired with the means to abort it.
8
+ *
9
+ * An `AbortSignal` on its own cannot be aborted — only the controller
10
+ * that owns it can. Returning the pair keeps that capability reachable.
11
+ */
12
+ export interface CancellableSignal {
13
+ readonly signal: AbortSignal;
14
+ /** Aborts the signal. Calling it more than once is a no-op. */
15
+ cancel(reason?: unknown): void;
16
+ }
6
17
  /**
7
18
  * Creates an AbortSignal that can be triggered manually.
8
19
  */
9
- export declare function createCancellableSignal(): AbortSignal;
20
+ export declare function createCancellableSignal(): CancellableSignal;
21
+ /**
22
+ * Cancels a cancellable signal.
23
+ */
24
+ export declare function cancelSignal(cancellable: CancellableSignal, reason?: unknown): void;
10
25
  /**
11
- * Cancels an AbortSignal by aborting its controller.
26
+ * Throws if the signal has been aborted.
27
+ *
28
+ * Use at cooperative cancellation points inside a handler.
29
+ */
30
+ export declare function throwIfCancelled(signal: AbortSignal, procedureName?: string): void;
31
+ /**
32
+ * Combines several signals into one that aborts with the first of them.
33
+ *
34
+ * The returned object must be disposed to detach its listeners; leaving
35
+ * them attached would keep every source signal referenced.
12
36
  */
13
- export declare function cancelSignal(signal: AbortSignal): void;
37
+ export declare function combineSignals(...signals: readonly (AbortSignal | undefined)[]): {
38
+ readonly signal: AbortSignal;
39
+ dispose(): void;
40
+ };
14
41
  //# sourceMappingURL=rpcCancellation.helper.d.ts.map
@@ -3,19 +3,75 @@
3
3
  *
4
4
  * Cancellation utilities for RPC operations.
5
5
  */
6
+ import { RPCCancelledError } from "../../errors/rpc.errors.js";
6
7
  /**
7
8
  * Creates an AbortSignal that can be triggered manually.
8
9
  */
9
10
  export function createCancellableSignal() {
10
11
  const controller = new AbortController();
11
- return controller.signal;
12
+ return {
13
+ signal: controller.signal,
14
+ cancel(reason) {
15
+ if (controller.signal.aborted) {
16
+ return;
17
+ }
18
+ controller.abort(reason ?? new RPCCancelledError());
19
+ },
20
+ };
12
21
  }
13
22
  /**
14
- * Cancels an AbortSignal by aborting its controller.
23
+ * Cancels a cancellable signal.
15
24
  */
16
- export function cancelSignal(signal) {
25
+ export function cancelSignal(cancellable, reason) {
26
+ cancellable.cancel(reason);
27
+ }
28
+ /**
29
+ * Throws if the signal has been aborted.
30
+ *
31
+ * Use at cooperative cancellation points inside a handler.
32
+ */
33
+ export function throwIfCancelled(signal, procedureName) {
17
34
  if (!signal.aborted) {
18
- signal.abort();
35
+ return;
36
+ }
37
+ const reason = signal.reason;
38
+ if (reason instanceof Error) {
39
+ throw reason;
40
+ }
41
+ throw new RPCCancelledError(typeof reason === "string" ? reason : undefined, procedureName);
42
+ }
43
+ /**
44
+ * Combines several signals into one that aborts with the first of them.
45
+ *
46
+ * The returned object must be disposed to detach its listeners; leaving
47
+ * them attached would keep every source signal referenced.
48
+ */
49
+ export function combineSignals(...signals) {
50
+ const present = signals.filter((signal) => signal !== undefined);
51
+ const controller = new AbortController();
52
+ const detach = [];
53
+ const abort = (reason) => {
54
+ if (!controller.signal.aborted) {
55
+ controller.abort(reason);
56
+ }
57
+ };
58
+ for (const source of present) {
59
+ if (source.aborted) {
60
+ abort(source.reason);
61
+ break;
62
+ }
63
+ const listener = () => abort(source.reason);
64
+ source.addEventListener("abort", listener, { once: true });
65
+ detach.push(() => source.removeEventListener("abort", listener));
19
66
  }
67
+ return {
68
+ signal: controller.signal,
69
+ dispose() {
70
+ for (const remove of detach) {
71
+ remove();
72
+ }
73
+ detach.length = 0;
74
+ },
75
+ };
20
76
  }
21
77
  //# sourceMappingURL=rpcCancellation.helper.js.map
@@ -13,6 +13,19 @@ export declare function getRemainingTime(deadline: number): number;
13
13
  export declare function isDeadlineExceeded(deadline: number): boolean;
14
14
  /**
15
15
  * Throws if the deadline has been exceeded.
16
+ *
17
+ * Throws the typed `RPCDeadlineExceededError` so callers and the server's
18
+ * error mapping can recognise it, rather than a bare `Error`.
19
+ */
20
+ export declare function throwIfDeadlineExceeded(deadline: number, procedureName?: string): void;
21
+ /**
22
+ * Reads a deadline from request metadata.
23
+ *
24
+ * Returns `undefined` when absent or not a usable epoch milliseconds
25
+ * value, so a malformed deadline is ignored rather than treated as
26
+ * already expired.
16
27
  */
17
- export declare function throwIfDeadlineExceeded(deadline: number): void;
28
+ export declare function readDeadline(metadata: {
29
+ readonly deadline?: unknown;
30
+ }): number | undefined;
18
31
  //# sourceMappingURL=rpcDeadline.helper.d.ts.map
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Deadline utilities for RPC operations.
5
5
  */
6
+ import { RPCDeadlineExceededError } from "../../errors/rpc.errors.js";
6
7
  /**
7
8
  * Calculates the remaining time until a deadline.
8
9
  */
@@ -18,10 +19,30 @@ export function isDeadlineExceeded(deadline) {
18
19
  }
19
20
  /**
20
21
  * Throws if the deadline has been exceeded.
22
+ *
23
+ * Throws the typed `RPCDeadlineExceededError` so callers and the server's
24
+ * error mapping can recognise it, rather than a bare `Error`.
21
25
  */
22
- export function throwIfDeadlineExceeded(deadline) {
26
+ export function throwIfDeadlineExceeded(deadline, procedureName) {
23
27
  if (isDeadlineExceeded(deadline)) {
24
- throw new Error("Deadline exceeded.");
28
+ throw new RPCDeadlineExceededError(deadline, procedureName);
29
+ }
30
+ }
31
+ /**
32
+ * Reads a deadline from request metadata.
33
+ *
34
+ * Returns `undefined` when absent or not a usable epoch milliseconds
35
+ * value, so a malformed deadline is ignored rather than treated as
36
+ * already expired.
37
+ */
38
+ export function readDeadline(metadata) {
39
+ const deadline = metadata.deadline;
40
+ if (typeof deadline !== "number" || !Number.isFinite(deadline)) {
41
+ return undefined;
42
+ }
43
+ if (deadline <= 0) {
44
+ return undefined;
25
45
  }
46
+ return deadline;
26
47
  }
27
48
  //# sourceMappingURL=rpcDeadline.helper.js.map