@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.
- package/LICENSE +21 -0
- package/README.md +104 -12
- 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 +189 -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/context/rpcContext.type.js +3 -1
- package/dist/rpc/dispatcher/index.d.ts +1 -0
- package/dist/rpc/dispatcher/rpcDispatcher.core.d.ts +49 -2
- package/dist/rpc/dispatcher/rpcDispatcher.core.js +109 -14
- 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 +117 -16
- 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 +86 -0
- package/dist/rpc/validation/rpcValidation.core.js +133 -0
- package/package.json +27 -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,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
|
|
@@ -3,15 +3,38 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Retry utilities for RPC operations.
|
|
5
5
|
*/
|
|
6
|
+
import { RPCCancelledError } from "../../errors/rpc.errors.js";
|
|
7
|
+
import { MAX_TIMER_DELAY } from "../../constants/rpcConstants.core.js";
|
|
6
8
|
/**
|
|
7
9
|
* Default retry options.
|
|
8
10
|
*/
|
|
9
|
-
export const DEFAULT_RETRY_OPTIONS = {
|
|
11
|
+
export const DEFAULT_RETRY_OPTIONS = Object.freeze({
|
|
10
12
|
attempts: 1,
|
|
11
13
|
delay: 0,
|
|
12
|
-
};
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Applies the configured jitter strategy to a delay.
|
|
17
|
+
*/
|
|
18
|
+
function applyJitter(delay, jitter) {
|
|
19
|
+
if (delay <= 0) {
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
switch (jitter) {
|
|
23
|
+
case "full":
|
|
24
|
+
return Math.round(Math.random() * delay);
|
|
25
|
+
case "equal":
|
|
26
|
+
return Math.round(delay / 2 + Math.random() * (delay / 2));
|
|
27
|
+
default:
|
|
28
|
+
return delay;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
13
31
|
/**
|
|
14
32
|
* Calculates the delay for a retry attempt.
|
|
33
|
+
*
|
|
34
|
+
* The result is finite, non-negative, and no greater than a timer can
|
|
35
|
+
* represent. Jitter defaults to `"none"` here so the calculation stays
|
|
36
|
+
* predictable for callers that compose it; {@link retry} opts into
|
|
37
|
+
* `"full"` jitter by default.
|
|
15
38
|
*/
|
|
16
39
|
export function calculateRetryDelay(attempt, options) {
|
|
17
40
|
const backoff = options.backoff ?? "fixed";
|
|
@@ -21,30 +44,79 @@ export function calculateRetryDelay(attempt, options) {
|
|
|
21
44
|
delay = options.delay * attempt;
|
|
22
45
|
}
|
|
23
46
|
else if (backoff === "exponential") {
|
|
24
|
-
delay = options.delay * 2 ** (attempt - 1);
|
|
47
|
+
delay = options.delay * 2 ** Math.max(0, attempt - 1);
|
|
48
|
+
}
|
|
49
|
+
if (!Number.isFinite(delay) || delay < 0) {
|
|
50
|
+
delay = 0;
|
|
51
|
+
}
|
|
52
|
+
const capped = Math.min(delay, maxDelay, MAX_TIMER_DELAY);
|
|
53
|
+
return Math.min(applyJitter(capped, options.jitter ?? "none"), MAX_TIMER_DELAY);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Sleeps for a duration, rejecting early if the signal aborts.
|
|
57
|
+
*/
|
|
58
|
+
function sleep(ms, signal) {
|
|
59
|
+
if (ms <= 0) {
|
|
60
|
+
return Promise.resolve();
|
|
25
61
|
}
|
|
26
|
-
return
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
signal?.removeEventListener("abort", onAbort);
|
|
65
|
+
resolve();
|
|
66
|
+
}, ms);
|
|
67
|
+
timer.unref?.();
|
|
68
|
+
function onAbort() {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
reject(signal?.reason instanceof Error
|
|
71
|
+
? signal.reason
|
|
72
|
+
: new RPCCancelledError("Retry cancelled."));
|
|
73
|
+
}
|
|
74
|
+
if (signal) {
|
|
75
|
+
if (signal.aborted) {
|
|
76
|
+
onAbort();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
27
82
|
}
|
|
28
83
|
/**
|
|
29
84
|
* Retries an asynchronous operation.
|
|
85
|
+
*
|
|
86
|
+
* Jitter defaults to `"full"` so concurrent callers retrying the same
|
|
87
|
+
* failing dependency do not synchronise into a retry wave.
|
|
30
88
|
*/
|
|
31
89
|
export async function retry(operation, options) {
|
|
32
|
-
const attempts = Math.max(1, options.attempts);
|
|
90
|
+
const attempts = Math.max(1, Math.floor(options.attempts));
|
|
91
|
+
const jittered = {
|
|
92
|
+
...options,
|
|
93
|
+
jitter: options.jitter ?? "full",
|
|
94
|
+
};
|
|
95
|
+
let lastError;
|
|
33
96
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
97
|
+
if (options.signal?.aborted) {
|
|
98
|
+
throw options.signal.reason instanceof Error
|
|
99
|
+
? options.signal.reason
|
|
100
|
+
: new RPCCancelledError("Retry cancelled.");
|
|
101
|
+
}
|
|
34
102
|
try {
|
|
35
103
|
return await operation();
|
|
36
104
|
}
|
|
37
105
|
catch (error) {
|
|
106
|
+
lastError = error;
|
|
38
107
|
if (attempt >= attempts - 1) {
|
|
39
108
|
throw error;
|
|
40
109
|
}
|
|
41
110
|
if (options.retryIf && !options.retryIf(error)) {
|
|
42
111
|
throw error;
|
|
43
112
|
}
|
|
44
|
-
const delay = calculateRetryDelay(attempt + 1,
|
|
45
|
-
|
|
113
|
+
const delay = calculateRetryDelay(attempt + 1, jittered);
|
|
114
|
+
options.onRetry?.(error, attempt + 1, delay);
|
|
115
|
+
await sleep(delay, options.signal);
|
|
46
116
|
}
|
|
47
117
|
}
|
|
48
|
-
|
|
118
|
+
// Unreachable while `attempts >= 1`, but throwing the last observed
|
|
119
|
+
// error beats inventing a new one if that ever changes.
|
|
120
|
+
throw lastError ?? new Error("Retry loop exited without running.");
|
|
49
121
|
}
|
|
50
122
|
//# sourceMappingURL=rpcRetry.helper.js.map
|
|
@@ -5,10 +5,26 @@
|
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
7
|
* Creates a timeout promise that rejects after the given duration.
|
|
8
|
+
*
|
|
9
|
+
* The returned `cancel` clears the underlying timer. A timeout promise
|
|
10
|
+
* whose timer is never cleared keeps the event loop alive for its full
|
|
11
|
+
* duration even after the work it guarded has finished.
|
|
8
12
|
*/
|
|
9
|
-
export declare function createTimeout(duration: number):
|
|
13
|
+
export declare function createTimeout(duration: number, procedureName?: string): {
|
|
14
|
+
readonly promise: Promise<never>;
|
|
15
|
+
cancel(): void;
|
|
16
|
+
};
|
|
10
17
|
/**
|
|
11
18
|
* Wraps a promise with a timeout.
|
|
19
|
+
*
|
|
20
|
+
* The timer is always cleared once the race settles, whichever side won.
|
|
21
|
+
*/
|
|
22
|
+
export declare function withTimeout<T>(promise: Promise<T>, duration: number, procedureName?: string): Promise<T>;
|
|
23
|
+
/**
|
|
24
|
+
* Runs an operation under a timeout, aborting it when the timeout wins.
|
|
25
|
+
*
|
|
26
|
+
* Unlike {@link withTimeout}, this gives the operation a signal so it can
|
|
27
|
+
* stop its own work rather than continuing unobserved.
|
|
12
28
|
*/
|
|
13
|
-
export declare function
|
|
29
|
+
export declare function runWithTimeout<T>(operation: (signal: AbortSignal) => Promise<T>, duration: number, procedureName?: string, parentSignal?: AbortSignal): Promise<T>;
|
|
14
30
|
//# sourceMappingURL=rpcTimeout.helper.d.ts.map
|
|
@@ -3,20 +3,84 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Timeout utilities for RPC operations.
|
|
5
5
|
*/
|
|
6
|
+
import { RPCTimeoutError } from "../../errors/rpc.errors.js";
|
|
7
|
+
import { MAX_TIMER_DELAY } from "../../constants/rpcConstants.core.js";
|
|
6
8
|
/**
|
|
7
9
|
* Creates a timeout promise that rejects after the given duration.
|
|
10
|
+
*
|
|
11
|
+
* The returned `cancel` clears the underlying timer. A timeout promise
|
|
12
|
+
* whose timer is never cleared keeps the event loop alive for its full
|
|
13
|
+
* duration even after the work it guarded has finished.
|
|
8
14
|
*/
|
|
9
|
-
export function createTimeout(duration) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
export function createTimeout(duration, procedureName) {
|
|
16
|
+
const delay = Math.min(Math.max(0, duration), MAX_TIMER_DELAY);
|
|
17
|
+
let timer;
|
|
18
|
+
const promise = new Promise((_, reject) => {
|
|
19
|
+
timer = setTimeout(() => {
|
|
20
|
+
reject(new RPCTimeoutError(duration, procedureName));
|
|
21
|
+
}, delay);
|
|
22
|
+
timer.unref?.();
|
|
14
23
|
});
|
|
24
|
+
return {
|
|
25
|
+
promise,
|
|
26
|
+
cancel() {
|
|
27
|
+
if (timer !== undefined) {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
timer = undefined;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
15
33
|
}
|
|
16
34
|
/**
|
|
17
35
|
* Wraps a promise with a timeout.
|
|
36
|
+
*
|
|
37
|
+
* The timer is always cleared once the race settles, whichever side won.
|
|
38
|
+
*/
|
|
39
|
+
export async function withTimeout(promise, duration, procedureName) {
|
|
40
|
+
const timeout = createTimeout(duration, procedureName);
|
|
41
|
+
try {
|
|
42
|
+
return await Promise.race([promise, timeout.promise]);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
timeout.cancel();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Runs an operation under a timeout, aborting it when the timeout wins.
|
|
50
|
+
*
|
|
51
|
+
* Unlike {@link withTimeout}, this gives the operation a signal so it can
|
|
52
|
+
* stop its own work rather than continuing unobserved.
|
|
18
53
|
*/
|
|
19
|
-
export async function
|
|
20
|
-
|
|
54
|
+
export async function runWithTimeout(operation, duration, procedureName, parentSignal) {
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timeout = createTimeout(duration, procedureName);
|
|
57
|
+
const onParentAbort = () => {
|
|
58
|
+
if (!controller.signal.aborted) {
|
|
59
|
+
controller.abort(parentSignal?.reason);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
if (parentSignal) {
|
|
63
|
+
if (parentSignal.aborted) {
|
|
64
|
+
onParentAbort();
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
parentSignal.addEventListener("abort", onParentAbort, { once: true });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
return await Promise.race([
|
|
72
|
+
operation(controller.signal),
|
|
73
|
+
timeout.promise.catch((error) => {
|
|
74
|
+
if (!controller.signal.aborted) {
|
|
75
|
+
controller.abort(error);
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}),
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
timeout.cancel();
|
|
83
|
+
parentSignal?.removeEventListener("abort", onParentAbort);
|
|
84
|
+
}
|
|
21
85
|
}
|
|
22
86
|
//# sourceMappingURL=rpcTimeout.helper.js.map
|
|
@@ -3,6 +3,24 @@ import type { RPCResponse } from "../types/rpcResponse.type.js";
|
|
|
3
3
|
import type { RPCProcedure } from "../procedure/rpcProcedure.type.js";
|
|
4
4
|
import { RPCProcedureRegistry } from "../procedure/rpcProcedureRegistry.core.js";
|
|
5
5
|
import { RPCMiddlewareStack } from "../middleware/rpcMiddleware.core.js";
|
|
6
|
+
import type { RPCDispatcherOptions } from "../dispatcher/rpcDispatcher.core.js";
|
|
7
|
+
import type { RPCRequestLimits } from "../validation/rpcValidation.core.js";
|
|
8
|
+
/**
|
|
9
|
+
* Options for an RPC server.
|
|
10
|
+
*/
|
|
11
|
+
export interface RPCServerOptions {
|
|
12
|
+
/** Limits applied to every incoming request before dispatch. */
|
|
13
|
+
readonly limits?: RPCRequestLimits;
|
|
14
|
+
/** Dispatch behaviour: default timeout and deadline handling. */
|
|
15
|
+
readonly dispatch?: RPCDispatcherOptions;
|
|
16
|
+
/**
|
|
17
|
+
* Receives errors that produced an internal error response, together
|
|
18
|
+
* with the request id the caller was given. Internal error detail is
|
|
19
|
+
* never returned over the wire, so this is where it must be recorded
|
|
20
|
+
* for the failure to be diagnosable.
|
|
21
|
+
*/
|
|
22
|
+
readonly onInternalError?: (error: unknown, requestId: string) => void;
|
|
23
|
+
}
|
|
6
24
|
/**
|
|
7
25
|
* RPC server that receives and dispatches requests.
|
|
8
26
|
*/
|
|
@@ -10,18 +28,39 @@ export declare class RPCServer {
|
|
|
10
28
|
private readonly registry;
|
|
11
29
|
private readonly middleware;
|
|
12
30
|
private readonly dispatcher;
|
|
13
|
-
|
|
31
|
+
private readonly options;
|
|
32
|
+
constructor(registry?: RPCProcedureRegistry, middleware?: RPCMiddlewareStack, options?: RPCServerOptions);
|
|
14
33
|
/**
|
|
15
34
|
* Registers a procedure.
|
|
35
|
+
*
|
|
36
|
+
* Generic over the procedure's input and output so a procedure built
|
|
37
|
+
* with `createRPCProcedure<TInput, TOutput>` can be registered
|
|
38
|
+
* directly. `RPCProcedure` is contravariant in `TInput` (its handler
|
|
39
|
+
* accepts that input), so a typed procedure is NOT assignable to
|
|
40
|
+
* `RPCProcedure<unknown>` and a non-generic signature would reject
|
|
41
|
+
* every typed procedure. Decoding stays a runtime concern.
|
|
16
42
|
*/
|
|
17
|
-
register(procedure: RPCProcedure): this;
|
|
43
|
+
register<TInput = unknown, TOutput = unknown>(procedure: RPCProcedure<TInput, TOutput>): this;
|
|
18
44
|
/**
|
|
19
45
|
* Handles an incoming RPC request.
|
|
46
|
+
*
|
|
47
|
+
* The frame is validated before dispatch, and every failure is mapped
|
|
48
|
+
* to a typed wire code. Unexpected errors are reported to
|
|
49
|
+
* `onInternalError` and answered with a fixed message: internal
|
|
50
|
+
* exception text can name hosts, paths, credentials or queries, and
|
|
51
|
+
* the caller is an untrusted peer.
|
|
20
52
|
*/
|
|
21
53
|
handle(request: RPCRequest): Promise<RPCResponse>;
|
|
22
54
|
/**
|
|
23
55
|
* Returns the procedure registry.
|
|
24
56
|
*/
|
|
25
57
|
getRegistry(): RPCProcedureRegistry;
|
|
58
|
+
/**
|
|
59
|
+
* Maps a known error onto a wire payload.
|
|
60
|
+
*
|
|
61
|
+
* Returns `undefined` for anything unrecognised, which the caller
|
|
62
|
+
* answers with a generic internal error.
|
|
63
|
+
*/
|
|
64
|
+
private mapError;
|
|
26
65
|
}
|
|
27
66
|
//# sourceMappingURL=rpcServer.core.d.ts.map
|
|
@@ -2,7 +2,33 @@ import { RPCProcedureRegistry } from "../procedure/rpcProcedureRegistry.core.js"
|
|
|
2
2
|
import { RPCMiddlewareStack } from "../middleware/rpcMiddleware.core.js";
|
|
3
3
|
import { RPCDispatcher } from "../dispatcher/rpcDispatcher.core.js";
|
|
4
4
|
import { createRPCErrorResponse } from "../types/rpcResponse.type.js";
|
|
5
|
-
import { RPCInternalError, RPCProcedureNotFoundError, RPCValidationError, } from "../errors/rpc.errors.js";
|
|
5
|
+
import { isRPCError, RPCAuthenticationError, RPCCancelledError, RPCDeadlineExceededError, RPCDeserializationError, RPCForbiddenError, RPCInternalError, RPCInvalidRequestError, RPCProcedureNotFoundError, RPCRateLimitedError, RPCSerializationError, RPCTimeoutError, RPCUnavailableError, RPCValidationError, } from "../errors/rpc.errors.js";
|
|
6
|
+
import { INTERNAL_ERROR_MESSAGE } from "../constants/rpcConstants.core.js";
|
|
7
|
+
import { assertValidRequest } from "../validation/rpcValidation.core.js";
|
|
8
|
+
/**
|
|
9
|
+
* Wire codes for the error types the server maps.
|
|
10
|
+
*
|
|
11
|
+
* Ordered most specific first; every entry is a type a caller can act
|
|
12
|
+
* on, which is why they must not be collapsed into a generic internal
|
|
13
|
+
* error.
|
|
14
|
+
*/
|
|
15
|
+
const ERROR_CODES = [
|
|
16
|
+
[RPCProcedureNotFoundError, "RPC_PROCEDURE_NOT_FOUND"],
|
|
17
|
+
[RPCValidationError, "RPC_VALIDATION_ERROR"],
|
|
18
|
+
[RPCInvalidRequestError, "RPC_INVALID_REQUEST"],
|
|
19
|
+
[RPCAuthenticationError, "RPC_UNAUTHENTICATED"],
|
|
20
|
+
[RPCForbiddenError, "RPC_FORBIDDEN"],
|
|
21
|
+
[RPCRateLimitedError, "RPC_RATE_LIMITED"],
|
|
22
|
+
[RPCDeadlineExceededError, "RPC_DEADLINE_EXCEEDED"],
|
|
23
|
+
[RPCTimeoutError, "RPC_TIMEOUT"],
|
|
24
|
+
[RPCCancelledError, "RPC_CANCELLED"],
|
|
25
|
+
[RPCUnavailableError, "RPC_UNAVAILABLE"],
|
|
26
|
+
// A serialization failure is a server fault (500, `expose: false`) whose
|
|
27
|
+
// message names what could not be encoded; the code travels, the
|
|
28
|
+
// message goes to `onInternalError`.
|
|
29
|
+
[RPCSerializationError, "RPC_SERIALIZATION_ERROR", true],
|
|
30
|
+
[RPCDeserializationError, "RPC_DESERIALIZATION_ERROR"],
|
|
31
|
+
];
|
|
6
32
|
/**
|
|
7
33
|
* RPC server that receives and dispatches requests.
|
|
8
34
|
*/
|
|
@@ -10,13 +36,22 @@ export class RPCServer {
|
|
|
10
36
|
registry;
|
|
11
37
|
middleware;
|
|
12
38
|
dispatcher;
|
|
13
|
-
|
|
39
|
+
options;
|
|
40
|
+
constructor(registry, middleware, options = {}) {
|
|
14
41
|
this.registry = registry ?? new RPCProcedureRegistry();
|
|
15
42
|
this.middleware = middleware ?? new RPCMiddlewareStack();
|
|
16
|
-
this.
|
|
43
|
+
this.options = options;
|
|
44
|
+
this.dispatcher = new RPCDispatcher(this.registry, this.middleware, options.dispatch ?? {});
|
|
17
45
|
}
|
|
18
46
|
/**
|
|
19
47
|
* Registers a procedure.
|
|
48
|
+
*
|
|
49
|
+
* Generic over the procedure's input and output so a procedure built
|
|
50
|
+
* with `createRPCProcedure<TInput, TOutput>` can be registered
|
|
51
|
+
* directly. `RPCProcedure` is contravariant in `TInput` (its handler
|
|
52
|
+
* accepts that input), so a typed procedure is NOT assignable to
|
|
53
|
+
* `RPCProcedure<unknown>` and a non-generic signature would reject
|
|
54
|
+
* every typed procedure. Decoding stays a runtime concern.
|
|
20
55
|
*/
|
|
21
56
|
register(procedure) {
|
|
22
57
|
this.registry.register(procedure);
|
|
@@ -24,28 +59,49 @@ export class RPCServer {
|
|
|
24
59
|
}
|
|
25
60
|
/**
|
|
26
61
|
* Handles an incoming RPC request.
|
|
62
|
+
*
|
|
63
|
+
* The frame is validated before dispatch, and every failure is mapped
|
|
64
|
+
* to a typed wire code. Unexpected errors are reported to
|
|
65
|
+
* `onInternalError` and answered with a fixed message: internal
|
|
66
|
+
* exception text can name hosts, paths, credentials or queries, and
|
|
67
|
+
* the caller is an untrusted peer.
|
|
27
68
|
*/
|
|
28
69
|
async handle(request) {
|
|
70
|
+
const requestId = typeof request?.id === "string"
|
|
71
|
+
? request.id
|
|
72
|
+
: "";
|
|
29
73
|
try {
|
|
30
|
-
|
|
74
|
+
assertValidRequest(request, this.options.limits);
|
|
75
|
+
// `metadata` is optional on the wire (`createRPCRequest` fills it in,
|
|
76
|
+
// a hand-built or JSON-decoded frame need not). Every consumer below
|
|
77
|
+
// reads it as an object, so a frame without it used to fail with a
|
|
78
|
+
// TypeError reported as an internal error.
|
|
79
|
+
const frame = request.metadata === undefined ? { ...request, metadata: {} } : request;
|
|
80
|
+
return await this.dispatcher.dispatch(frame);
|
|
31
81
|
}
|
|
32
82
|
catch (error) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
83
|
+
const mapped = this.mapError(error);
|
|
84
|
+
if (mapped === undefined) {
|
|
85
|
+
this.options.onInternalError?.(error, requestId);
|
|
86
|
+
return createRPCErrorResponse(requestId, {
|
|
87
|
+
code: "RPC_INTERNAL_ERROR",
|
|
88
|
+
message: INTERNAL_ERROR_MESSAGE,
|
|
37
89
|
});
|
|
38
90
|
}
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
91
|
+
if (mapped.internal) {
|
|
92
|
+
// The error is typed, so the caller keeps its code, but it was
|
|
93
|
+
// constructed with `expose: false`: its message is server detail
|
|
94
|
+
// and goes to the log, not the wire.
|
|
95
|
+
this.options.onInternalError?.(error, requestId);
|
|
96
|
+
return createRPCErrorResponse(requestId, {
|
|
97
|
+
code: mapped.code,
|
|
98
|
+
message: INTERNAL_ERROR_MESSAGE,
|
|
44
99
|
});
|
|
45
100
|
}
|
|
46
|
-
return createRPCErrorResponse(
|
|
47
|
-
code:
|
|
48
|
-
message:
|
|
101
|
+
return createRPCErrorResponse(requestId, {
|
|
102
|
+
code: mapped.code,
|
|
103
|
+
message: mapped.message,
|
|
104
|
+
...(mapped.details !== undefined ? { details: mapped.details } : {}),
|
|
49
105
|
});
|
|
50
106
|
}
|
|
51
107
|
}
|
|
@@ -55,5 +111,50 @@ export class RPCServer {
|
|
|
55
111
|
getRegistry() {
|
|
56
112
|
return this.registry;
|
|
57
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Maps a known error onto a wire payload.
|
|
116
|
+
*
|
|
117
|
+
* Returns `undefined` for anything unrecognised, which the caller
|
|
118
|
+
* answers with a generic internal error.
|
|
119
|
+
*/
|
|
120
|
+
mapError(error) {
|
|
121
|
+
// An RPCInternalError *is* the generic internal failure: it carries
|
|
122
|
+
// `expose: false` and a message written for the log. Mapping it like
|
|
123
|
+
// any other RPCError put that message on the wire.
|
|
124
|
+
if (error instanceof RPCInternalError) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
for (const [type, code, internal] of ERROR_CODES) {
|
|
128
|
+
if (error instanceof type) {
|
|
129
|
+
const payload = {
|
|
130
|
+
code,
|
|
131
|
+
message: error.message,
|
|
132
|
+
...(internal ? { internal: true } : {}),
|
|
133
|
+
};
|
|
134
|
+
if (error instanceof RPCValidationError &&
|
|
135
|
+
error.issues !== undefined &&
|
|
136
|
+
error.issues.length > 0) {
|
|
137
|
+
payload.details = error.issues;
|
|
138
|
+
}
|
|
139
|
+
if (error instanceof RPCRateLimitedError &&
|
|
140
|
+
error.retryAfter !== undefined) {
|
|
141
|
+
payload.details = { retryAfter: error.retryAfter };
|
|
142
|
+
}
|
|
143
|
+
return payload;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// A custom RPCError subclass is still a deliberate, caller-facing
|
|
147
|
+
// error; honour its own code rather than hiding it. Its message only
|
|
148
|
+
// travels when the error was built to be exposed — `RPCError`
|
|
149
|
+
// defaults to `expose: false`.
|
|
150
|
+
if (isRPCError(error)) {
|
|
151
|
+
return {
|
|
152
|
+
code: error.code,
|
|
153
|
+
message: error.message,
|
|
154
|
+
internal: error.expose === false,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
58
159
|
}
|
|
59
160
|
//# sourceMappingURL=rpcServer.core.js.map
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
* @zudojs/rpc/streaming
|
|
3
3
|
*
|
|
4
4
|
* Streaming types for RPC operations.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* These types describe a streaming contract but are **not yet
|
|
8
|
+
* dispatchable**: `RPCProcedureRegistry` and `RPCServer` accept only
|
|
9
|
+
* `RPCProcedure`, and `RPCTransport.send` resolves with a single
|
|
10
|
+
* `RPCResponse` rather than a stream. A procedure built with
|
|
11
|
+
* {@link createRPCStreamingProcedure} therefore cannot be registered or
|
|
12
|
+
* called over a transport today. Use it to shape a handler ahead of
|
|
13
|
+
* transport support; do not rely on it to serve traffic.
|
|
5
14
|
*/
|
|
6
15
|
/**
|
|
7
16
|
* A streaming RPC procedure handler.
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
* @zudojs/rpc/streaming
|
|
3
3
|
*
|
|
4
4
|
* Streaming types for RPC operations.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* These types describe a streaming contract but are **not yet
|
|
8
|
+
* dispatchable**: `RPCProcedureRegistry` and `RPCServer` accept only
|
|
9
|
+
* `RPCProcedure`, and `RPCTransport.send` resolves with a single
|
|
10
|
+
* `RPCResponse` rather than a stream. A procedure built with
|
|
11
|
+
* {@link createRPCStreamingProcedure} therefore cannot be registered or
|
|
12
|
+
* called over a transport today. Use it to shape a handler ahead of
|
|
13
|
+
* transport support; do not rely on it to serve traffic.
|
|
5
14
|
*/
|
|
6
15
|
/**
|
|
7
16
|
* Creates a streaming RPC procedure.
|
|
@@ -11,7 +11,15 @@ export interface RPCTransportRequestOptions {
|
|
|
11
11
|
* Transport interface for sending and receiving RPC messages.
|
|
12
12
|
*/
|
|
13
13
|
export interface RPCTransport {
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Sends a request and resolves with the peer's response.
|
|
16
|
+
*
|
|
17
|
+
* `options.signal` aborts the in-flight send. A transport that ignores
|
|
18
|
+
* it still works — the client races the signal itself — but the
|
|
19
|
+
* underlying connection is only released by a transport that honours
|
|
20
|
+
* it.
|
|
21
|
+
*/
|
|
22
|
+
send(request: RPCRequest, options?: RPCTransportRequestOptions): Promise<RPCResponse>;
|
|
15
23
|
close?(): Promise<void>;
|
|
16
24
|
}
|
|
17
25
|
//# sourceMappingURL=rpcTransport.type.d.ts.map
|