@zudojs/rpc 0.1.0 → 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
@@ -1,38 +1,129 @@
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
30
  async dispatch(request) {
18
31
  const procedure = this.registry.require(request.procedure);
19
- const signal = new AbortController().signal;
20
- const context = createRPCContext(request, signal);
32
+ const controller = new AbortController();
33
+ const context = createRPCContext(request, controller.signal);
34
+ const timeoutMs = this.resolveTimeout(request, procedure);
35
+ // A deadline already in the past is rejected before any work runs.
36
+ if (this.options.honourDeadline ?? true) {
37
+ const deadline = readDeadline(request.metadata);
38
+ if (deadline !== undefined) {
39
+ throwIfDeadlineExceeded(deadline, request.procedure);
40
+ }
41
+ }
42
+ const timeout = timeoutMs > 0 ? createTimeout(timeoutMs, request.procedure) : undefined;
21
43
  try {
22
- const result = await this.middleware.execute(context, async () => {
23
- return procedure.handler(request.payload, context);
24
- });
44
+ const run = async () => {
45
+ const input = procedure.options?.input
46
+ ? parseInput(procedure.options.input, request.payload, request.procedure)
47
+ : request.payload;
48
+ const result = await this.middleware.execute(context, async () => {
49
+ return procedure.handler(input, context);
50
+ });
51
+ return procedure.options?.output
52
+ ? parseOutput(procedure.options.output, result, request.procedure)
53
+ : result;
54
+ };
55
+ const invoke = () => this.applyInterceptors(context, run);
56
+ const result = timeout === undefined
57
+ ? await invoke()
58
+ : await Promise.race([
59
+ invoke(),
60
+ timeout.promise.catch((error) => {
61
+ // Abort first so a cooperative handler stops working
62
+ // instead of running on with its result discarded.
63
+ if (!controller.signal.aborted) {
64
+ controller.abort(error);
65
+ }
66
+ throw error;
67
+ }),
68
+ ]);
25
69
  return createRPCResponse(request.id, result);
26
70
  }
27
- catch (error) {
28
- if (error instanceof RPCProcedureNotFoundError) {
29
- throw error;
71
+ finally {
72
+ // Errors propagate unchanged. Rewriting them here is what turned
73
+ // an auth failure or a rate limit into a generic internal fault;
74
+ // the server owns the mapping from error type to response code.
75
+ timeout?.cancel();
76
+ if (!controller.signal.aborted) {
77
+ // Release anything still listening on the request signal.
78
+ controller.abort(new RPCCancelledError("Request completed.", request.procedure));
30
79
  }
31
- if (error instanceof RPCValidationError) {
32
- throw error;
80
+ }
81
+ }
82
+ /**
83
+ * Runs the interceptor chain around the dispatch.
84
+ *
85
+ * Each interceptor may call `next()` once; a second call throws rather
86
+ * than re-running the handler.
87
+ */
88
+ async applyInterceptors(context, run) {
89
+ const interceptors = this.options.interceptors;
90
+ if (!interceptors || interceptors.length === 0) {
91
+ return run();
92
+ }
93
+ const called = new Set();
94
+ const runAt = async (index) => {
95
+ if (called.has(index)) {
96
+ throw new Error(`RPC interceptor at index ${index - 1} called next() more than once.`);
97
+ }
98
+ called.add(index);
99
+ if (index >= interceptors.length) {
100
+ return run();
101
+ }
102
+ const interceptor = interceptors[index];
103
+ return interceptor.intercept(context, () => runAt(index + 1));
104
+ };
105
+ return runAt(0);
106
+ }
107
+ /**
108
+ * Resolves the timeout for a dispatch.
109
+ *
110
+ * A `deadline` in request metadata can only shorten the timeout, never
111
+ * extend it past what the procedure allows.
112
+ */
113
+ resolveTimeout(request, procedure) {
114
+ const configured = procedure.options?.timeout ??
115
+ this.options.defaultTimeout ??
116
+ DEFAULT_RPC_TIMEOUT;
117
+ if (configured <= 0) {
118
+ return 0;
119
+ }
120
+ if (this.options.honourDeadline ?? true) {
121
+ const deadline = readDeadline(request.metadata);
122
+ if (deadline !== undefined) {
123
+ return Math.max(1, Math.min(configured, deadline - Date.now()));
33
124
  }
34
- throw new Error(`Handler for procedure "${request.procedure}" threw: ${String(error)}`);
35
125
  }
126
+ return configured;
36
127
  }
37
128
  }
38
129
  //# 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
@@ -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