@zudojs/rpc 0.0.1 → 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
|
@@ -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,30 @@ 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 {
|
|
5
|
+
import { isRPCError, RPCAuthenticationError, RPCCancelledError, RPCDeadlineExceededError, RPCDeserializationError, RPCForbiddenError, 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
|
+
[RPCSerializationError, "RPC_SERIALIZATION_ERROR"],
|
|
27
|
+
[RPCDeserializationError, "RPC_DESERIALIZATION_ERROR"],
|
|
28
|
+
];
|
|
6
29
|
/**
|
|
7
30
|
* RPC server that receives and dispatches requests.
|
|
8
31
|
*/
|
|
@@ -10,13 +33,22 @@ export class RPCServer {
|
|
|
10
33
|
registry;
|
|
11
34
|
middleware;
|
|
12
35
|
dispatcher;
|
|
13
|
-
|
|
36
|
+
options;
|
|
37
|
+
constructor(registry, middleware, options = {}) {
|
|
14
38
|
this.registry = registry ?? new RPCProcedureRegistry();
|
|
15
39
|
this.middleware = middleware ?? new RPCMiddlewareStack();
|
|
16
|
-
this.
|
|
40
|
+
this.options = options;
|
|
41
|
+
this.dispatcher = new RPCDispatcher(this.registry, this.middleware, options.dispatch ?? {});
|
|
17
42
|
}
|
|
18
43
|
/**
|
|
19
44
|
* Registers a procedure.
|
|
45
|
+
*
|
|
46
|
+
* Generic over the procedure's input and output so a procedure built
|
|
47
|
+
* with `createRPCProcedure<TInput, TOutput>` can be registered
|
|
48
|
+
* directly. `RPCProcedure` is contravariant in `TInput` (its handler
|
|
49
|
+
* accepts that input), so a typed procedure is NOT assignable to
|
|
50
|
+
* `RPCProcedure<unknown>` and a non-generic signature would reject
|
|
51
|
+
* every typed procedure. Decoding stays a runtime concern.
|
|
20
52
|
*/
|
|
21
53
|
register(procedure) {
|
|
22
54
|
this.registry.register(procedure);
|
|
@@ -24,29 +56,31 @@ export class RPCServer {
|
|
|
24
56
|
}
|
|
25
57
|
/**
|
|
26
58
|
* Handles an incoming RPC request.
|
|
59
|
+
*
|
|
60
|
+
* The frame is validated before dispatch, and every failure is mapped
|
|
61
|
+
* to a typed wire code. Unexpected errors are reported to
|
|
62
|
+
* `onInternalError` and answered with a fixed message: internal
|
|
63
|
+
* exception text can name hosts, paths, credentials or queries, and
|
|
64
|
+
* the caller is an untrusted peer.
|
|
27
65
|
*/
|
|
28
66
|
async handle(request) {
|
|
67
|
+
const requestId = typeof request?.id === "string"
|
|
68
|
+
? request.id
|
|
69
|
+
: "";
|
|
29
70
|
try {
|
|
71
|
+
assertValidRequest(request, this.options.limits);
|
|
30
72
|
return await this.dispatcher.dispatch(request);
|
|
31
73
|
}
|
|
32
74
|
catch (error) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
75
|
+
const mapped = this.mapError(error);
|
|
76
|
+
if (mapped === undefined) {
|
|
77
|
+
this.options.onInternalError?.(error, requestId);
|
|
78
|
+
return createRPCErrorResponse(requestId, {
|
|
79
|
+
code: "RPC_INTERNAL_ERROR",
|
|
80
|
+
message: INTERNAL_ERROR_MESSAGE,
|
|
37
81
|
});
|
|
38
82
|
}
|
|
39
|
-
|
|
40
|
-
return createRPCErrorResponse(request.id, {
|
|
41
|
-
code: "RPC_VALIDATION_ERROR",
|
|
42
|
-
message: error.message,
|
|
43
|
-
details: error.issues,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
return createRPCErrorResponse(request.id, {
|
|
47
|
-
code: "RPC_INTERNAL_ERROR",
|
|
48
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
49
|
-
});
|
|
83
|
+
return createRPCErrorResponse(requestId, mapped);
|
|
50
84
|
}
|
|
51
85
|
}
|
|
52
86
|
/**
|
|
@@ -55,5 +89,35 @@ export class RPCServer {
|
|
|
55
89
|
getRegistry() {
|
|
56
90
|
return this.registry;
|
|
57
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Maps a known error onto a wire payload.
|
|
94
|
+
*
|
|
95
|
+
* Returns `undefined` for anything unrecognised, which the caller
|
|
96
|
+
* answers with a generic internal error.
|
|
97
|
+
*/
|
|
98
|
+
mapError(error) {
|
|
99
|
+
for (const [type, code] of ERROR_CODES) {
|
|
100
|
+
if (error instanceof type) {
|
|
101
|
+
const payload = {
|
|
102
|
+
code,
|
|
103
|
+
message: error.message,
|
|
104
|
+
};
|
|
105
|
+
if (error instanceof RPCValidationError && error.issues !== undefined) {
|
|
106
|
+
payload.details = error.issues;
|
|
107
|
+
}
|
|
108
|
+
if (error instanceof RPCRateLimitedError &&
|
|
109
|
+
error.retryAfter !== undefined) {
|
|
110
|
+
payload.details = { retryAfter: error.retryAfter };
|
|
111
|
+
}
|
|
112
|
+
return payload;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// A custom RPCError subclass is still a deliberate, caller-facing
|
|
116
|
+
// error; honour its own code rather than hiding it.
|
|
117
|
+
if (isRPCError(error)) {
|
|
118
|
+
return { code: error.code, message: error.message };
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
58
122
|
}
|
|
59
123
|
//# 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
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/rpc/validation
|
|
3
|
+
*
|
|
4
|
+
* Request-level checks applied before a payload reaches a handler.
|
|
5
|
+
*
|
|
6
|
+
* Requests arrive from an untrusted peer over a transport, so these are
|
|
7
|
+
* the first checks the server runs, not the last.
|
|
8
|
+
*/
|
|
9
|
+
import type { Schema, SchemaIssue } from "@zudojs/schema";
|
|
10
|
+
import type { RPCRequest } from "../types/rpcRequest.type.js";
|
|
11
|
+
/**
|
|
12
|
+
* Limits applied to an incoming request.
|
|
13
|
+
*/
|
|
14
|
+
export interface RPCRequestLimits {
|
|
15
|
+
/**
|
|
16
|
+
* Maximum encoded payload size in bytes. Defaults to
|
|
17
|
+
* {@link MAX_RPC_PAYLOAD_SIZE}. Set to `0` to skip the check when the
|
|
18
|
+
* transport already enforces a frame limit.
|
|
19
|
+
*/
|
|
20
|
+
readonly maxPayloadBytes?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Whether procedure names must match {@link PROCEDURE_NAME_PATTERN}.
|
|
23
|
+
* Defaults to `true`.
|
|
24
|
+
*/
|
|
25
|
+
readonly enforceProcedureNamePattern?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validates a procedure name.
|
|
29
|
+
*
|
|
30
|
+
* Length is checked before the pattern so a pathological name cannot
|
|
31
|
+
* drive regex backtracking.
|
|
32
|
+
*/
|
|
33
|
+
export declare function assertValidProcedureName(name: unknown): asserts name is string;
|
|
34
|
+
/**
|
|
35
|
+
* Measures the encoded size of a payload in bytes.
|
|
36
|
+
*
|
|
37
|
+
* Returns `undefined` for a payload that cannot be encoded, which the
|
|
38
|
+
* caller reports as an invalid request rather than passing on.
|
|
39
|
+
*/
|
|
40
|
+
export declare function measurePayloadBytes(payload: unknown): number | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Validates the shape and size of an incoming request.
|
|
43
|
+
*
|
|
44
|
+
* @throws {RPCInvalidRequestError} when the frame is malformed or the
|
|
45
|
+
* payload exceeds the configured limit.
|
|
46
|
+
*/
|
|
47
|
+
export declare function assertValidRequest(request: unknown, limits?: RPCRequestLimits): asserts request is RPCRequest;
|
|
48
|
+
/**
|
|
49
|
+
* Converts schema issues into metadata safe to return to a caller.
|
|
50
|
+
*
|
|
51
|
+
* Only the path, code and message travel; the received input is dropped
|
|
52
|
+
* so a validation response cannot echo back data the caller sent.
|
|
53
|
+
*/
|
|
54
|
+
export declare function toValidationIssues(issues: readonly SchemaIssue[]): readonly {
|
|
55
|
+
path: string;
|
|
56
|
+
code: string;
|
|
57
|
+
message: string;
|
|
58
|
+
}[];
|
|
59
|
+
/**
|
|
60
|
+
* The part of a `@zudojs/schema` Schema this package depends on.
|
|
61
|
+
*
|
|
62
|
+
* Structural so any schema-like validator can be supplied, and so the
|
|
63
|
+
* dependency stays to the parse contract rather than the class.
|
|
64
|
+
*/
|
|
65
|
+
export type RPCSchema<TOutput = unknown> = Pick<Schema<TOutput, never>, "safeParse">;
|
|
66
|
+
/**
|
|
67
|
+
* Parses caller input against a schema.
|
|
68
|
+
*
|
|
69
|
+
* @throws {RPCValidationError} carrying the issues, which are safe to
|
|
70
|
+
* return: they describe the caller's own input.
|
|
71
|
+
*/
|
|
72
|
+
export declare function parseInput<T>(schema: RPCSchema<T>, value: unknown, procedureName: string): T;
|
|
73
|
+
/**
|
|
74
|
+
* Parses a handler's result against its output schema.
|
|
75
|
+
*
|
|
76
|
+
* An invalid output is a server defect rather than a caller mistake, so
|
|
77
|
+
* the issues describe internal shape and stay server-side: the thrown
|
|
78
|
+
* error carries no issue detail.
|
|
79
|
+
*/
|
|
80
|
+
export declare function parseOutput<T>(schema: RPCSchema<T>, value: unknown, procedureName: string): T;
|
|
81
|
+
//# sourceMappingURL=rpcValidation.core.d.ts.map
|