@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.
- package/LICENSE +21 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.js +3 -4
- package/dist/rpc/client/index.d.ts +1 -1
- package/dist/rpc/client/rpcClient.core.d.ts +57 -3
- package/dist/rpc/client/rpcClient.core.js +172 -32
- package/dist/rpc/constants/index.d.ts +1 -1
- package/dist/rpc/constants/index.js +1 -1
- package/dist/rpc/constants/rpcConstants.core.d.ts +18 -0
- package/dist/rpc/constants/rpcConstants.core.js +18 -0
- package/dist/rpc/dispatcher/index.d.ts +1 -0
- package/dist/rpc/dispatcher/rpcDispatcher.core.d.ts +48 -1
- package/dist/rpc/dispatcher/rpcDispatcher.core.js +104 -13
- package/dist/rpc/middleware/rpcMiddleware.core.d.ts +12 -0
- package/dist/rpc/middleware/rpcMiddleware.core.js +29 -6
- package/dist/rpc/procedure/rpcProcedure.type.d.ts +25 -3
- package/dist/rpc/procedure/rpcProcedure.type.js +9 -0
- package/dist/rpc/procedure/rpcProcedureRegistry.core.d.ts +15 -1
- package/dist/rpc/procedure/rpcProcedureRegistry.core.js +26 -1
- package/dist/rpc/procedure/rpcProcedureRouter.core.d.ts +1 -1
- package/dist/rpc/procedure/rpcProcedureRouter.core.js +5 -0
- package/dist/rpc/reliability/cancellation/rpcCancellation.helper.d.ts +30 -3
- package/dist/rpc/reliability/cancellation/rpcCancellation.helper.js +60 -4
- package/dist/rpc/reliability/deadline/rpcDeadline.helper.d.ts +14 -1
- package/dist/rpc/reliability/deadline/rpcDeadline.helper.js +23 -2
- package/dist/rpc/reliability/index.d.ts +5 -4
- package/dist/rpc/reliability/index.js +3 -3
- package/dist/rpc/reliability/retry/rpcRetry.helper.d.ts +22 -0
- package/dist/rpc/reliability/retry/rpcRetry.helper.js +80 -8
- package/dist/rpc/reliability/timeout/rpcTimeout.helper.d.ts +18 -2
- package/dist/rpc/reliability/timeout/rpcTimeout.helper.js +71 -7
- package/dist/rpc/server/index.d.ts +1 -0
- package/dist/rpc/server/rpcServer.core.d.ts +41 -2
- package/dist/rpc/server/rpcServer.core.js +82 -18
- package/dist/rpc/streaming/rpcStreaming.type.d.ts +9 -0
- package/dist/rpc/streaming/rpcStreaming.type.js +9 -0
- package/dist/rpc/transport/rpcTransport.type.d.ts +9 -1
- package/dist/rpc/validation/index.d.ts +3 -0
- package/dist/rpc/validation/index.js +2 -0
- package/dist/rpc/validation/rpcValidation.core.d.ts +81 -0
- package/dist/rpc/validation/rpcValidation.core.js +125 -0
- package/package.json +23 -16
- package/dist/.tsbuildinfo +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/rpc/client/index.d.ts.map +0 -1
- package/dist/rpc/client/index.js.map +0 -1
- package/dist/rpc/client/rpcClient.core.d.ts.map +0 -1
- package/dist/rpc/client/rpcClient.core.js.map +0 -1
- package/dist/rpc/constants/index.d.ts.map +0 -1
- package/dist/rpc/constants/index.js.map +0 -1
- package/dist/rpc/constants/rpcConstants.core.d.ts.map +0 -1
- package/dist/rpc/constants/rpcConstants.core.js.map +0 -1
- package/dist/rpc/context/index.d.ts.map +0 -1
- package/dist/rpc/context/index.js.map +0 -1
- package/dist/rpc/context/rpcContext.type.d.ts.map +0 -1
- package/dist/rpc/context/rpcContext.type.js.map +0 -1
- package/dist/rpc/dispatcher/index.d.ts.map +0 -1
- package/dist/rpc/dispatcher/index.js.map +0 -1
- package/dist/rpc/dispatcher/rpcDispatcher.core.d.ts.map +0 -1
- package/dist/rpc/dispatcher/rpcDispatcher.core.js.map +0 -1
- package/dist/rpc/errors/index.d.ts.map +0 -1
- package/dist/rpc/errors/index.js.map +0 -1
- package/dist/rpc/errors/rpc.errors.d.ts.map +0 -1
- package/dist/rpc/errors/rpc.errors.js.map +0 -1
- package/dist/rpc/interceptor/index.d.ts.map +0 -1
- package/dist/rpc/interceptor/index.js.map +0 -1
- package/dist/rpc/interceptor/rpcInterceptor.type.d.ts.map +0 -1
- package/dist/rpc/interceptor/rpcInterceptor.type.js.map +0 -1
- package/dist/rpc/middleware/index.d.ts.map +0 -1
- package/dist/rpc/middleware/index.js.map +0 -1
- package/dist/rpc/middleware/rpcMiddleware.core.d.ts.map +0 -1
- package/dist/rpc/middleware/rpcMiddleware.core.js.map +0 -1
- package/dist/rpc/procedure/index.d.ts.map +0 -1
- package/dist/rpc/procedure/index.js.map +0 -1
- package/dist/rpc/procedure/rpcProcedure.type.d.ts.map +0 -1
- package/dist/rpc/procedure/rpcProcedure.type.js.map +0 -1
- package/dist/rpc/procedure/rpcProcedureRegistry.core.d.ts.map +0 -1
- package/dist/rpc/procedure/rpcProcedureRegistry.core.js.map +0 -1
- package/dist/rpc/procedure/rpcProcedureRouter.core.d.ts.map +0 -1
- package/dist/rpc/procedure/rpcProcedureRouter.core.js.map +0 -1
- package/dist/rpc/reliability/cancellation/rpcCancellation.helper.d.ts.map +0 -1
- package/dist/rpc/reliability/cancellation/rpcCancellation.helper.js.map +0 -1
- package/dist/rpc/reliability/deadline/rpcDeadline.helper.d.ts.map +0 -1
- package/dist/rpc/reliability/deadline/rpcDeadline.helper.js.map +0 -1
- package/dist/rpc/reliability/index.d.ts.map +0 -1
- package/dist/rpc/reliability/index.js.map +0 -1
- package/dist/rpc/reliability/retry/rpcRetry.helper.d.ts.map +0 -1
- package/dist/rpc/reliability/retry/rpcRetry.helper.js.map +0 -1
- package/dist/rpc/reliability/timeout/rpcTimeout.helper.d.ts.map +0 -1
- package/dist/rpc/reliability/timeout/rpcTimeout.helper.js.map +0 -1
- package/dist/rpc/server/index.d.ts.map +0 -1
- package/dist/rpc/server/index.js.map +0 -1
- package/dist/rpc/server/rpcServer.core.d.ts.map +0 -1
- package/dist/rpc/server/rpcServer.core.js.map +0 -1
- package/dist/rpc/streaming/index.d.ts.map +0 -1
- package/dist/rpc/streaming/index.js.map +0 -1
- package/dist/rpc/streaming/rpcStreaming.type.d.ts.map +0 -1
- package/dist/rpc/streaming/rpcStreaming.type.js.map +0 -1
- package/dist/rpc/transport/index.d.ts.map +0 -1
- package/dist/rpc/transport/index.js.map +0 -1
- package/dist/rpc/transport/rpcTransport.type.d.ts.map +0 -1
- package/dist/rpc/transport/rpcTransport.type.js.map +0 -1
- package/dist/rpc/types/index.d.ts.map +0 -1
- package/dist/rpc/types/index.js.map +0 -1
- package/dist/rpc/types/rpcMetadata.type.d.ts.map +0 -1
- package/dist/rpc/types/rpcMetadata.type.js.map +0 -1
- package/dist/rpc/types/rpcProcedureName.type.d.ts.map +0 -1
- package/dist/rpc/types/rpcProcedureName.type.js.map +0 -1
- package/dist/rpc/types/rpcRequest.type.d.ts.map +0 -1
- package/dist/rpc/types/rpcRequest.type.js.map +0 -1
- package/dist/rpc/types/rpcResponse.type.d.ts.map +0 -1
- package/dist/rpc/types/rpcResponse.type.js.map +0 -1
- package/dist/testing/index.d.ts.map +0 -1
- 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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
23
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
15
|
-
const
|
|
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
|
|
45
|
+
return runAt(index + 1);
|
|
23
46
|
}
|
|
24
|
-
return mw(context,
|
|
47
|
+
return mw(context, () => runAt(index + 1));
|
|
25
48
|
};
|
|
26
|
-
return
|
|
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
|
|
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():
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
23
|
+
* Cancels a cancellable signal.
|
|
15
24
|
*/
|
|
16
|
-
export function cancelSignal(
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
4
|
-
export
|
|
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
|