@procedures/client 0.2.3 → 0.3.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/CHANGELOG.md +65 -0
- package/README.md +61 -38
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/client/transport.d.ts.map +1 -1
- package/dist/adapters/http/client/transport.js +35 -3
- package/dist/adapters/http/client/transport.js.map +1 -1
- package/dist/adapters/http/client/types.d.ts +3 -2
- package/dist/adapters/http/client/types.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.d.ts +2 -2
- package/dist/adapters/http/server/transport.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.js +53 -20
- package/dist/adapters/http/server/transport.js.map +1 -1
- package/dist/adapters/http/shared/headers.d.ts +1 -1
- package/dist/adapters/http/shared/headers.d.ts.map +1 -1
- package/dist/adapters/http/shared/headers.js +7 -6
- package/dist/adapters/http/shared/headers.js.map +1 -1
- package/dist/adapters/local/client/transport.d.ts +3 -0
- package/dist/adapters/local/client/transport.d.ts.map +1 -1
- package/dist/adapters/local/client/transport.js +63 -4
- package/dist/adapters/local/client/transport.js.map +1 -1
- package/dist/adapters/local/client/types.d.ts +15 -0
- package/dist/adapters/local/client/types.d.ts.map +1 -1
- package/dist/adapters/local/client/types.js.map +1 -1
- package/dist/adapters/mock/client/transport.d.ts +3 -2
- package/dist/adapters/mock/client/transport.d.ts.map +1 -1
- package/dist/adapters/mock/client/transport.js +8 -5
- package/dist/adapters/mock/client/transport.js.map +1 -1
- package/dist/adapters/mock/client/types.d.ts +7 -6
- package/dist/adapters/mock/client/types.d.ts.map +1 -1
- package/dist/adapters/port/transport.d.ts +62 -0
- package/dist/adapters/port/transport.d.ts.map +1 -0
- package/dist/adapters/port/transport.js +94 -0
- package/dist/adapters/port/transport.js.map +1 -0
- package/dist/adapters/websocket/client/index.d.ts +1 -1
- package/dist/adapters/websocket/client/index.d.ts.map +1 -1
- package/dist/adapters/websocket/client/index.js.map +1 -1
- package/dist/adapters/websocket/client/transport.d.ts +47 -7
- package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.js +213 -67
- package/dist/adapters/websocket/client/transport.js.map +1 -1
- package/dist/adapters/websocket/client/types.d.ts +70 -21
- package/dist/adapters/websocket/client/types.d.ts.map +1 -1
- package/dist/adapters/websocket/client/types.js.map +1 -1
- package/dist/adapters/websocket/server/transport.d.ts +65 -24
- package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/server/transport.js +192 -263
- package/dist/adapters/websocket/server/transport.js.map +1 -1
- package/dist/adapters/websocket/server/types.d.ts +20 -1
- package/dist/adapters/websocket/server/types.d.ts.map +1 -1
- package/dist/adapters/websocket/shared/rpc.d.ts +129 -0
- package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -0
- package/dist/adapters/websocket/shared/rpc.js +425 -0
- package/dist/adapters/websocket/shared/rpc.js.map +1 -0
- package/dist/browser.d.ts +29 -7
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +26 -3
- package/dist/browser.js.map +1 -1
- package/dist/client/call-types.d.ts +10 -10
- package/dist/client/call-types.d.ts.map +1 -1
- package/dist/client/call-types.js +1 -6
- package/dist/client/call-types.js.map +1 -1
- package/dist/client/client.d.ts +6 -1
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +23 -9
- package/dist/client/client.js.map +1 -1
- package/dist/client/deadline.d.ts +38 -0
- package/dist/client/deadline.d.ts.map +1 -0
- package/dist/client/deadline.js +60 -0
- package/dist/client/deadline.js.map +1 -0
- package/dist/client/errors/factory.d.ts.map +1 -1
- package/dist/client/errors/factory.js +11 -7
- package/dist/client/errors/factory.js.map +1 -1
- package/dist/client/index.d.ts +9 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +8 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/middleware/cache.d.ts +19 -5
- package/dist/client/middleware/cache.d.ts.map +1 -1
- package/dist/client/middleware/cache.js +106 -18
- package/dist/client/middleware/cache.js.map +1 -1
- package/dist/client/middleware/circuit-breaker.d.ts +28 -10
- package/dist/client/middleware/circuit-breaker.d.ts.map +1 -1
- package/dist/client/middleware/circuit-breaker.js +35 -24
- package/dist/client/middleware/circuit-breaker.js.map +1 -1
- package/dist/client/middleware/contexts.d.ts +16 -10
- package/dist/client/middleware/contexts.d.ts.map +1 -1
- package/dist/client/middleware/contexts.js.map +1 -1
- package/dist/client/middleware/items.d.ts +31 -2
- package/dist/client/middleware/items.d.ts.map +1 -1
- package/dist/client/middleware/items.js +86 -6
- package/dist/client/middleware/items.js.map +1 -1
- package/dist/client/middleware/lru.d.ts +2 -0
- package/dist/client/middleware/lru.d.ts.map +1 -1
- package/dist/client/middleware/lru.js +11 -0
- package/dist/client/middleware/lru.js.map +1 -1
- package/dist/client/middleware/rate-limit.d.ts +7 -6
- package/dist/client/middleware/rate-limit.d.ts.map +1 -1
- package/dist/client/middleware/rate-limit.js +9 -6
- package/dist/client/middleware/rate-limit.js.map +1 -1
- package/dist/client/middleware/resilience.d.ts +61 -0
- package/dist/client/middleware/resilience.d.ts.map +1 -0
- package/dist/client/middleware/resilience.js +59 -0
- package/dist/client/middleware/resilience.js.map +1 -0
- package/dist/client/middleware/retry.d.ts +45 -11
- package/dist/client/middleware/retry.d.ts.map +1 -1
- package/dist/client/middleware/retry.js +102 -22
- package/dist/client/middleware/retry.js.map +1 -1
- package/dist/client/middleware/timeout.d.ts +15 -12
- package/dist/client/middleware/timeout.d.ts.map +1 -1
- package/dist/client/middleware/timeout.js +46 -37
- package/dist/client/middleware/timeout.js.map +1 -1
- package/dist/client/middleware/tracing.d.ts.map +1 -1
- package/dist/client/middleware/tracing.js +11 -3
- package/dist/client/middleware/tracing.js.map +1 -1
- package/dist/client/types.d.ts +9 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/types.js.map +1 -1
- package/dist/clock.d.ts +19 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +18 -0
- package/dist/clock.js.map +1 -0
- package/dist/duration.d.ts +37 -0
- package/dist/duration.d.ts.map +1 -0
- package/dist/duration.js +44 -0
- package/dist/duration.js.map +1 -0
- package/dist/machines.d.ts +90 -0
- package/dist/machines.d.ts.map +1 -0
- package/dist/machines.js +127 -0
- package/dist/machines.js.map +1 -0
- package/dist/procedures/capability.d.ts +3 -6
- package/dist/procedures/capability.d.ts.map +1 -1
- package/dist/procedures/capability.js +7 -8
- package/dist/procedures/capability.js.map +1 -1
- package/dist/procedures/collection/index.d.ts +3 -3
- package/dist/procedures/collection/index.d.ts.map +1 -1
- package/dist/procedures/collection/index.js +1 -1
- package/dist/procedures/collection/index.js.map +1 -1
- package/dist/procedures/collection/procedures.d.ts.map +1 -1
- package/dist/procedures/collection/procedures.js +49 -1
- package/dist/procedures/collection/procedures.js.map +1 -1
- package/dist/procedures/collection/schemas.d.ts +33 -0
- package/dist/procedures/collection/schemas.d.ts.map +1 -1
- package/dist/procedures/collection/schemas.js +68 -0
- package/dist/procedures/collection/schemas.js.map +1 -1
- package/dist/procedures/collection/storage.d.ts +37 -0
- package/dist/procedures/collection/storage.d.ts.map +1 -1
- package/dist/procedures/core/array.d.ts.map +1 -1
- package/dist/procedures/core/array.js +26 -15
- package/dist/procedures/core/array.js.map +1 -1
- package/dist/procedures/core/index.d.ts.map +1 -1
- package/dist/procedures/core/index.js +8 -0
- package/dist/procedures/core/index.js.map +1 -1
- package/dist/procedures/core/meta.d.ts.map +1 -1
- package/dist/procedures/core/meta.js +2 -1
- package/dist/procedures/core/meta.js.map +1 -1
- package/dist/procedures/core/object.d.ts.map +1 -1
- package/dist/procedures/core/object.js +42 -13
- package/dist/procedures/core/object.js.map +1 -1
- package/dist/procedures/core/operands.d.ts.map +1 -1
- package/dist/procedures/core/operands.js +6 -1
- package/dist/procedures/core/operands.js.map +1 -1
- package/dist/procedures/core/string.d.ts.map +1 -1
- package/dist/procedures/core/string.js +4 -2
- package/dist/procedures/core/string.js.map +1 -1
- package/dist/procedures/define-procedure.d.ts +19 -7
- package/dist/procedures/define-procedure.d.ts.map +1 -1
- package/dist/procedures/define-procedure.js +57 -17
- package/dist/procedures/define-procedure.js.map +1 -1
- package/dist/procedures/effects.d.ts +16 -0
- package/dist/procedures/effects.d.ts.map +1 -1
- package/dist/procedures/effects.js +33 -0
- package/dist/procedures/effects.js.map +1 -1
- package/dist/procedures/idempotency.d.ts +73 -0
- package/dist/procedures/idempotency.d.ts.map +1 -0
- package/dist/procedures/idempotency.js +131 -0
- package/dist/procedures/idempotency.js.map +1 -0
- package/dist/procedures/index.d.ts +7 -3
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +6 -2
- package/dist/procedures/index.js.map +1 -1
- package/dist/procedures/interceptors.d.ts +17 -4
- package/dist/procedures/interceptors.d.ts.map +1 -1
- package/dist/procedures/interceptors.js +18 -7
- package/dist/procedures/interceptors.js.map +1 -1
- package/dist/procedures/invoke.d.ts +15 -3
- package/dist/procedures/invoke.d.ts.map +1 -1
- package/dist/procedures/invoke.js +8 -3
- package/dist/procedures/invoke.js.map +1 -1
- package/dist/procedures/manifest.d.ts +83 -0
- package/dist/procedures/manifest.d.ts.map +1 -0
- package/dist/procedures/manifest.js +90 -0
- package/dist/procedures/manifest.js.map +1 -0
- package/dist/procedures/runtime-programs.d.ts +19 -0
- package/dist/procedures/runtime-programs.d.ts.map +1 -0
- package/dist/procedures/runtime-programs.js +24 -0
- package/dist/procedures/runtime-programs.js.map +1 -0
- package/dist/procedures/types.d.ts +16 -0
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/procedures/types.js.map +1 -1
- package/dist/protocol.d.ts +40 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +58 -0
- package/dist/protocol.js.map +1 -0
- package/dist/server/host-policy.d.ts +4 -3
- package/dist/server/host-policy.d.ts.map +1 -1
- package/dist/server/host-policy.js +6 -3
- package/dist/server/host-policy.js.map +1 -1
- package/dist/server/host-procedures.d.ts +31 -0
- package/dist/server/host-procedures.d.ts.map +1 -0
- package/dist/server/host-procedures.js +75 -0
- package/dist/server/host-procedures.js.map +1 -0
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/procedure-server.d.ts +110 -4
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +269 -17
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/server/request-auth.d.ts +19 -0
- package/dist/server/request-auth.d.ts.map +1 -0
- package/dist/server/request-auth.js +32 -0
- package/dist/server/request-auth.js.map +1 -0
- package/dist/server/types.d.ts +32 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/dist/timers.d.ts +60 -0
- package/dist/timers.d.ts.map +1 -0
- package/dist/timers.js +33 -0
- package/dist/timers.js.map +1 -0
- package/dist/tracing/traceparent.d.ts +26 -0
- package/dist/tracing/traceparent.d.ts.map +1 -0
- package/dist/tracing/traceparent.js +53 -0
- package/dist/tracing/traceparent.js.map +1 -0
- package/package.json +7 -1
- package/src/adapters/http/client/transport.ts +31 -3
- package/src/adapters/http/client/types.ts +3 -2
- package/src/adapters/http/server/transport.ts +60 -24
- package/src/adapters/http/shared/headers.ts +7 -6
- package/src/adapters/local/client/transport.ts +70 -5
- package/src/adapters/local/client/types.ts +18 -0
- package/src/adapters/mock/client/transport.ts +9 -6
- package/src/adapters/mock/client/types.ts +7 -6
- package/src/adapters/port/transport.ts +124 -0
- package/src/adapters/websocket/client/index.ts +1 -0
- package/src/adapters/websocket/client/transport.ts +232 -73
- package/src/adapters/websocket/client/types.ts +80 -21
- package/src/adapters/websocket/server/transport.ts +199 -301
- package/src/adapters/websocket/server/types.ts +20 -1
- package/src/adapters/websocket/shared/rpc.ts +453 -0
- package/src/browser.ts +51 -3
- package/src/client/call-types.ts +10 -14
- package/src/client/client.ts +28 -9
- package/src/client/deadline.ts +64 -0
- package/src/client/errors/factory.ts +12 -8
- package/src/client/index.ts +21 -3
- package/src/client/middleware/cache.ts +120 -23
- package/src/client/middleware/circuit-breaker.ts +65 -35
- package/src/client/middleware/contexts.ts +17 -10
- package/src/client/middleware/items.ts +101 -6
- package/src/client/middleware/lru.ts +12 -0
- package/src/client/middleware/rate-limit.ts +9 -7
- package/src/client/middleware/resilience.ts +81 -0
- package/src/client/middleware/retry.ts +146 -34
- package/src/client/middleware/timeout.ts +55 -46
- package/src/client/middleware/tracing.ts +13 -4
- package/src/client/types.ts +9 -1
- package/src/clock.ts +21 -0
- package/src/duration.ts +56 -0
- package/src/machines.ts +186 -0
- package/src/procedures/capability.ts +7 -8
- package/src/procedures/collection/index.ts +9 -1
- package/src/procedures/collection/procedures.ts +57 -0
- package/src/procedures/collection/schemas.ts +110 -0
- package/src/procedures/collection/storage.ts +38 -0
- package/src/procedures/core/array.ts +27 -16
- package/src/procedures/core/index.ts +8 -0
- package/src/procedures/core/meta.ts +2 -1
- package/src/procedures/core/object.ts +48 -14
- package/src/procedures/core/operands.ts +7 -0
- package/src/procedures/core/string.ts +4 -2
- package/src/procedures/define-procedure.ts +73 -20
- package/src/procedures/effects.ts +35 -0
- package/src/procedures/idempotency.ts +152 -0
- package/src/procedures/index.ts +18 -2
- package/src/procedures/interceptors.ts +28 -11
- package/src/procedures/invoke.ts +19 -5
- package/src/procedures/manifest.ts +158 -0
- package/src/procedures/runtime-programs.ts +27 -0
- package/src/procedures/types.ts +16 -0
- package/src/protocol.ts +97 -0
- package/src/server/host-policy.ts +8 -5
- package/src/server/host-procedures.ts +102 -0
- package/src/server/index.ts +5 -2
- package/src/server/procedure-server.ts +324 -10
- package/src/server/request-auth.ts +35 -0
- package/src/server/types.ts +37 -0
- package/src/timers.ts +82 -0
- package/src/tracing/traceparent.ts +61 -0
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
import type { IncomingMessage } from "http";
|
|
9
9
|
import { WebSocketServer, WebSocket } from "ws";
|
|
10
|
-
import type { ServerTransport, ServerRequest
|
|
11
|
-
import type {
|
|
10
|
+
import type { ServerTransport, ServerRequest } from "../../../server/types.js";
|
|
11
|
+
import type { Message, ResponseItem, Transport } from "../../../client/types.js";
|
|
12
|
+
import { pathToMethod } from "../../../server/method.js";
|
|
13
|
+
import { IncomingCalls, OutgoingCalls } from "../shared/rpc.js";
|
|
14
|
+
import type { RequestHandler } from "../../../server/types.js";
|
|
12
15
|
import type {
|
|
13
16
|
WebSocketServerTransportOptions,
|
|
14
17
|
WebSocketMessage,
|
|
@@ -17,6 +20,10 @@ import type {
|
|
|
17
20
|
} from "./types.js";
|
|
18
21
|
import { withoutInternalKeys } from "../../metadata.js";
|
|
19
22
|
import { checkBrowserRequest, type BrowserGuardOptions } from "../../../server/browser-guard.js";
|
|
23
|
+
import { optionalMs } from "../../../duration.js";
|
|
24
|
+
import { credentialsOf, withoutSecretHeaders } from "../../../server/request-auth.js";
|
|
25
|
+
import { PROTOCOL_VERSION, protocolVersionOf } from "../../../protocol.js";
|
|
26
|
+
import { TIMER_DEFAULTS } from "../../../timers.js";
|
|
20
27
|
|
|
21
28
|
/**
|
|
22
29
|
* WebSocket server transport adapter.
|
|
@@ -43,87 +50,36 @@ import { checkBrowserRequest, type BrowserGuardOptions } from "../../../server/b
|
|
|
43
50
|
* httpServer.listen(3000);
|
|
44
51
|
* ```
|
|
45
52
|
*/
|
|
46
|
-
/**
|
|
47
|
-
* The credit of one stream: the number of items the client can take now. The client sends
|
|
48
|
-
* more credit as its reader takes items (deep dive TRN-3).
|
|
49
|
-
*/
|
|
50
|
-
class StreamCredit {
|
|
51
|
-
private wake: (() => void) | undefined;
|
|
52
|
-
|
|
53
|
-
constructor(private credit: number) {}
|
|
54
|
-
|
|
55
|
-
grant(n: number): void {
|
|
56
|
-
this.credit += n;
|
|
57
|
-
const wake = this.wake;
|
|
58
|
-
this.wake = undefined;
|
|
59
|
-
wake?.();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Take one item of credit. Returns false when the signal aborted the wait. */
|
|
63
|
-
async take(signal: AbortSignal): Promise<boolean> {
|
|
64
|
-
while (this.credit <= 0) {
|
|
65
|
-
if (signal.aborted) return false;
|
|
66
|
-
await new Promise<void>((resolve) => {
|
|
67
|
-
const onAbort = (): void => resolve();
|
|
68
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
69
|
-
this.wake = () => {
|
|
70
|
-
signal.removeEventListener("abort", onAbort);
|
|
71
|
-
resolve();
|
|
72
|
-
};
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
if (signal.aborted) return false;
|
|
76
|
-
this.credit--;
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** The error of a failed stream or handler: its code (a string) and its retryable flag. */
|
|
82
|
-
function errorStatus(error: unknown): { type: "error"; code: string; message: string; retryable: boolean } {
|
|
83
|
-
const code = (error as { code?: unknown } | undefined)?.code;
|
|
84
|
-
const retryable = (error as { retryable?: unknown } | undefined)?.retryable;
|
|
85
|
-
return {
|
|
86
|
-
type: "error",
|
|
87
|
-
code: typeof code === "string" ? code : "HANDLER_ERROR",
|
|
88
|
-
message: error instanceof Error ? error.message : String(error),
|
|
89
|
-
// Before, every stream error lost its retryable flag (deep dive TRN-7)
|
|
90
|
-
retryable: typeof retryable === "boolean" ? retryable : false,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
interface PendingRequest {
|
|
95
|
-
id: string;
|
|
96
|
-
/** The connection the request was sent to: only it may answer, and its close ends the request */
|
|
97
|
-
connectionId: string;
|
|
98
|
-
resolve: (value: unknown) => void;
|
|
99
|
-
reject: (error: Error) => void;
|
|
100
|
-
timeout: NodeJS.Timeout;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
53
|
export class WebSocketServerTransport implements ServerTransport {
|
|
104
54
|
readonly name = "websocket";
|
|
105
55
|
|
|
106
56
|
private wss: WebSocketServer | null = null;
|
|
107
57
|
private options: Required<
|
|
108
|
-
Omit<WebSocketServerTransportOptions, "authenticate" | "onConnection" | "allowedOrigins" | "allowedHosts">
|
|
58
|
+
Omit<WebSocketServerTransportOptions, "authenticate" | "onConnection" | "allowedOrigins" | "allowedHosts" | "heartbeat" | "reverseRequestTimeout">
|
|
109
59
|
> & {
|
|
110
60
|
authenticate?: WebSocketServerTransportOptions["authenticate"];
|
|
111
61
|
onConnection?: WebSocketServerTransportOptions["onConnection"];
|
|
112
62
|
};
|
|
113
|
-
private server:
|
|
63
|
+
private server: RequestHandler;
|
|
114
64
|
private connections: Set<WebSocket> = new Set();
|
|
115
65
|
|
|
66
|
+
/** The time until the first frame of a call to a client, in milliseconds (R24) */
|
|
67
|
+
private readonly reverseRequestTimeoutMs: number;
|
|
68
|
+
/** The heartbeat of the server, or undefined when it is off (roadmap R3) */
|
|
69
|
+
private readonly heartbeat: { interval: number; misses: number } | undefined;
|
|
70
|
+
private heartbeatTimer: ReturnType<typeof setInterval> | undefined;
|
|
71
|
+
/** The pings without a pong, one after the other, of each connection */
|
|
72
|
+
private readonly missedPongs = new WeakMap<WebSocket, number>();
|
|
73
|
+
|
|
116
74
|
// Tracked connections with IDs for bidirectional RPC
|
|
117
75
|
private trackedConnections = new Map<string, TrackedConnection>();
|
|
118
|
-
private pendingRequests = new Map<string, PendingRequest>();
|
|
119
76
|
private connectionCounter = 0;
|
|
120
77
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
private credits = new Map<string, StreamCredit>();
|
|
78
|
+
/**
|
|
79
|
+
* The calls of each connection in the two directions (roadmap R24): the calls that the client
|
|
80
|
+
* sends (served by the server) and the calls that the server sends to the client.
|
|
81
|
+
*/
|
|
82
|
+
private readonly channels = new Map<string, { incoming: IncomingCalls; outgoing: OutgoingCalls }>();
|
|
127
83
|
|
|
128
84
|
/** The browser checks of each connection (see `server/browser-guard.ts`) */
|
|
129
85
|
private readonly guard: BrowserGuardOptions;
|
|
@@ -132,7 +88,7 @@ export class WebSocketServerTransport implements ServerTransport {
|
|
|
132
88
|
private connectHandlers: ConnectionEventHandler[] = [];
|
|
133
89
|
private disconnectHandlers: ConnectionEventHandler[] = [];
|
|
134
90
|
|
|
135
|
-
constructor(server:
|
|
91
|
+
constructor(server: RequestHandler, options: WebSocketServerTransportOptions) {
|
|
136
92
|
this.server = server;
|
|
137
93
|
this.options = {
|
|
138
94
|
server: options.server,
|
|
@@ -145,6 +101,36 @@ export class WebSocketServerTransport implements ServerTransport {
|
|
|
145
101
|
highWaterMark: options.highWaterMark ?? 1024 * 1024,
|
|
146
102
|
};
|
|
147
103
|
this.guard = { allowedOrigins: options.allowedOrigins, allowedHosts: options.allowedHosts };
|
|
104
|
+
this.reverseRequestTimeoutMs = optionalMs(options.reverseRequestTimeout, "reverseRequestTimeout") ?? TIMER_DEFAULTS.webSocketServer.reverseRequestTimeoutMs;
|
|
105
|
+
this.heartbeat =
|
|
106
|
+
options.heartbeat === false
|
|
107
|
+
? undefined
|
|
108
|
+
: {
|
|
109
|
+
interval: optionalMs(options.heartbeat?.interval, "heartbeat.interval") ?? TIMER_DEFAULTS.webSocketServer.heartbeatIntervalMs,
|
|
110
|
+
misses: Math.max(1, options.heartbeat?.misses ?? TIMER_DEFAULTS.webSocketServer.heartbeatMisses),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* One beat of the heartbeat: a connection that missed too many pongs closes at once (its close
|
|
116
|
+
* event aborts its requests), and each other connection gets a ping.
|
|
117
|
+
*/
|
|
118
|
+
private beat(): void {
|
|
119
|
+
if (!this.heartbeat) return;
|
|
120
|
+
for (const ws of this.connections) {
|
|
121
|
+
const missed = this.missedPongs.get(ws) ?? 0;
|
|
122
|
+
if (missed >= this.heartbeat.misses) {
|
|
123
|
+
console.error(`[${this.name}] A connection missed ${missed} pongs: closing it`);
|
|
124
|
+
ws.terminate();
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
this.missedPongs.set(ws, missed + 1);
|
|
128
|
+
try {
|
|
129
|
+
ws.ping();
|
|
130
|
+
} catch {
|
|
131
|
+
// The socket is closing: its close event cleans up
|
|
132
|
+
}
|
|
133
|
+
}
|
|
148
134
|
}
|
|
149
135
|
|
|
150
136
|
async start(): Promise<void> {
|
|
@@ -185,17 +171,28 @@ export class WebSocketServerTransport implements ServerTransport {
|
|
|
185
171
|
this.handleConnection(ws, req);
|
|
186
172
|
});
|
|
187
173
|
|
|
174
|
+
if (this.heartbeat) {
|
|
175
|
+
this.heartbeatTimer = setInterval(() => this.beat(), this.heartbeat.interval);
|
|
176
|
+
// The heartbeat must not keep the process alive
|
|
177
|
+
this.heartbeatTimer.unref?.();
|
|
178
|
+
}
|
|
179
|
+
|
|
188
180
|
console.log(`[${this.name}] WebSocket server listening on ${this.options.path}`);
|
|
189
181
|
}
|
|
190
182
|
|
|
191
183
|
async stop(): Promise<void> {
|
|
184
|
+
if (this.heartbeatTimer) {
|
|
185
|
+
clearInterval(this.heartbeatTimer);
|
|
186
|
+
this.heartbeatTimer = undefined;
|
|
187
|
+
}
|
|
192
188
|
if (this.wss) {
|
|
193
|
-
// Stop the requests in progress, so their handlers end (deep dive TRN-15)
|
|
194
|
-
|
|
195
|
-
this.
|
|
196
|
-
|
|
197
|
-
|
|
189
|
+
// Stop the requests in progress, so their handlers end (deep dive TRN-15), and fail the
|
|
190
|
+
// calls that the server sent to its clients
|
|
191
|
+
for (const channel of this.channels.values()) {
|
|
192
|
+
channel.incoming.abortAll();
|
|
193
|
+
channel.outgoing.failAll("Server stopped");
|
|
198
194
|
}
|
|
195
|
+
this.channels.clear();
|
|
199
196
|
|
|
200
197
|
// Close all connections
|
|
201
198
|
for (const ws of this.connections) {
|
|
@@ -237,11 +234,31 @@ export class WebSocketServerTransport implements ServerTransport {
|
|
|
237
234
|
connectedAt: new Date(),
|
|
238
235
|
metadata: {
|
|
239
236
|
remoteAddress: req.socket.remoteAddress,
|
|
240
|
-
headers: req.headers,
|
|
237
|
+
headers: withoutSecretHeaders(req.headers as Record<string, unknown>),
|
|
241
238
|
},
|
|
242
239
|
};
|
|
243
240
|
this.trackedConnections.set(connectionId, trackedConnection);
|
|
244
241
|
|
|
242
|
+
// The calls of this connection in the two directions (roadmap R24)
|
|
243
|
+
const isOpen = (): boolean => ws.readyState === WebSocket.OPEN;
|
|
244
|
+
const send = (frame: WebSocketMessage): void => this.send(ws, frame);
|
|
245
|
+
this.channels.set(connectionId, {
|
|
246
|
+
incoming: new IncomingCalls({
|
|
247
|
+
handler: this.server,
|
|
248
|
+
send,
|
|
249
|
+
sendWithBackpressure: (frame, signal) => this.sendWithBackpressure(ws, frame, signal),
|
|
250
|
+
isOpen,
|
|
251
|
+
requestOf: (frame, signal) => this.requestOf(frame, req, connectionId, signal),
|
|
252
|
+
}),
|
|
253
|
+
outgoing: new OutgoingCalls({
|
|
254
|
+
send,
|
|
255
|
+
isOpen,
|
|
256
|
+
requestTimeoutMs: this.reverseRequestTimeoutMs,
|
|
257
|
+
streamWindow: 64,
|
|
258
|
+
idPrefix: "r:",
|
|
259
|
+
}),
|
|
260
|
+
});
|
|
261
|
+
|
|
245
262
|
// Call user connection handler (legacy)
|
|
246
263
|
if (this.options.onConnection) {
|
|
247
264
|
this.options.onConnection(ws, req);
|
|
@@ -256,6 +273,9 @@ export class WebSocketServerTransport implements ServerTransport {
|
|
|
256
273
|
}
|
|
257
274
|
}
|
|
258
275
|
|
|
276
|
+
// A pong answers the ping of the heartbeat
|
|
277
|
+
ws.on("pong", () => this.missedPongs.set(ws, 0));
|
|
278
|
+
|
|
259
279
|
// Handle messages
|
|
260
280
|
ws.on("message", async (data: Buffer) => {
|
|
261
281
|
try {
|
|
@@ -269,8 +289,11 @@ export class WebSocketServerTransport implements ServerTransport {
|
|
|
269
289
|
// Handle close
|
|
270
290
|
ws.on("close", () => {
|
|
271
291
|
this.connections.delete(ws);
|
|
272
|
-
|
|
273
|
-
this.
|
|
292
|
+
// The calls of the client stop, and the calls to the client fail (BUGS-2026-07 L29)
|
|
293
|
+
const channel = this.channels.get(connectionId);
|
|
294
|
+
channel?.incoming.abortAll();
|
|
295
|
+
channel?.outgoing.failAll("WebSocket connection closed");
|
|
296
|
+
this.channels.delete(connectionId);
|
|
274
297
|
const conn = this.trackedConnections.get(connectionId);
|
|
275
298
|
if (conn) {
|
|
276
299
|
this.trackedConnections.delete(connectionId);
|
|
@@ -294,201 +317,79 @@ export class WebSocketServerTransport implements ServerTransport {
|
|
|
294
317
|
}
|
|
295
318
|
|
|
296
319
|
/**
|
|
297
|
-
*
|
|
298
|
-
*
|
|
320
|
+
* The server request of a `request` frame of a connection: the metadata of the frame and of the
|
|
321
|
+
* connection, the signal of the call, and the credentials of the upgrade (roadmap R18).
|
|
299
322
|
*/
|
|
300
|
-
private
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
323
|
+
private requestOf(frame: WebSocketMessage, req: IncomingMessage, connectionId: string, signal: AbortSignal): ServerRequest {
|
|
324
|
+
return {
|
|
325
|
+
id: frame.id,
|
|
326
|
+
method: frame.method!,
|
|
327
|
+
// Only an absent payload becomes {}: before, 0, false and "" became {} too (deep dive TRN-10)
|
|
328
|
+
payload: frame.payload === undefined ? {} : frame.payload,
|
|
329
|
+
metadata: {
|
|
330
|
+
...withoutInternalKeys(frame.metadata),
|
|
331
|
+
// Add connection info
|
|
332
|
+
connectionId,
|
|
333
|
+
remoteAddress: req.socket.remoteAddress,
|
|
334
|
+
// The upgrade headers without their secrets (roadmap R18)
|
|
335
|
+
headers: withoutSecretHeaders(req.headers as Record<string, unknown>),
|
|
336
|
+
},
|
|
337
|
+
signal,
|
|
338
|
+
// The credentials of the connection: the bearer token of its upgrade (R18)
|
|
339
|
+
credentials: credentialsOf(req),
|
|
340
|
+
};
|
|
318
341
|
}
|
|
319
342
|
|
|
320
343
|
/**
|
|
321
|
-
* Handle WebSocket message
|
|
344
|
+
* Handle WebSocket message: a heartbeat, a frame of a call that the server sent (a response,
|
|
345
|
+
* an error, a stream), or a frame of a call of the client (a request, a cancel, a credit).
|
|
322
346
|
*/
|
|
323
347
|
private async handleMessage(
|
|
324
348
|
ws: WebSocket,
|
|
325
349
|
data: Buffer,
|
|
326
|
-
|
|
350
|
+
_req: IncomingMessage,
|
|
327
351
|
connectionId: string
|
|
328
352
|
): Promise<void> {
|
|
329
353
|
// Parse message
|
|
330
354
|
let message: WebSocketMessage;
|
|
331
355
|
try {
|
|
332
356
|
message = JSON.parse(data.toString());
|
|
333
|
-
} catch
|
|
357
|
+
} catch {
|
|
334
358
|
this.sendError(ws, "unknown", "Invalid JSON message");
|
|
335
359
|
return;
|
|
336
360
|
}
|
|
337
361
|
|
|
338
362
|
// Handle ping (client heartbeat)
|
|
339
363
|
if (message.type === "ping") {
|
|
340
|
-
|
|
341
|
-
id: message.id,
|
|
342
|
-
type: "pong",
|
|
343
|
-
};
|
|
344
|
-
this.send(ws, pong);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Handle pong (response to our ping)
|
|
349
|
-
if (message.type === "pong") {
|
|
350
|
-
// Just acknowledge, no action needed
|
|
364
|
+
this.send(ws, { id: message.id, type: "pong" });
|
|
351
365
|
return;
|
|
352
366
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
367
|
+
// A pong answers a ping of the server: nothing to do
|
|
368
|
+
if (message.type === "pong") return;
|
|
369
|
+
|
|
370
|
+
// The hello of the client: the protocol version (roadmap R25)
|
|
371
|
+
if (message.type === "hello") {
|
|
372
|
+
const version = protocolVersionOf(message.protocol);
|
|
373
|
+
const connection = this.trackedConnections.get(connectionId);
|
|
374
|
+
if (connection && version !== undefined) connection.metadata = { ...connection.metadata, protocol: version };
|
|
375
|
+
if (version !== undefined && version > PROTOCOL_VERSION) {
|
|
376
|
+
this.sendError(ws, message.id || "hello", `This server speaks the protocol ${PROTOCOL_VERSION}, and the client uses ${version}`, "PROTOCOL_VERSION");
|
|
377
|
+
ws.close(1002, "protocol version");
|
|
378
|
+
return;
|
|
366
379
|
}
|
|
380
|
+
this.send(ws, { id: message.id || "hello", type: "hello", protocol: PROTOCOL_VERSION });
|
|
367
381
|
return;
|
|
368
382
|
}
|
|
369
383
|
|
|
370
|
-
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (message.type === "credit") {
|
|
380
|
-
if (typeof message.credit === "number" && message.credit > 0) {
|
|
381
|
-
this.credits.get(`${connectionId}:${message.id}`)?.grant(message.credit);
|
|
382
|
-
}
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Validate request message
|
|
387
|
-
if (!message.id || message.type !== "request" || !message.method) {
|
|
388
|
-
this.sendError(ws, message.id || "unknown", "Invalid message format");
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const key = `${connectionId}:${message.id}`;
|
|
393
|
-
// A request id names one request of the connection while it runs. A second request with
|
|
394
|
-
// the id would take the first one's controller: its stream could not be cancelled, and the
|
|
395
|
-
// two outputs would mix (deep dive TRN-9).
|
|
396
|
-
if (this.inflight.has(key)) {
|
|
397
|
-
this.sendError(ws, message.id, "A request with this id is still running", "DUPLICATE_REQUEST");
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
const controller = new AbortController();
|
|
401
|
-
this.inflight.set(key, controller);
|
|
402
|
-
|
|
403
|
-
// Convert to ServerRequest. Only an absent payload becomes {}: before, 0, false and ""
|
|
404
|
-
// became {} too (deep dive TRN-10).
|
|
405
|
-
const serverRequest: ServerRequest = {
|
|
406
|
-
id: message.id,
|
|
407
|
-
method: message.method,
|
|
408
|
-
payload: message.payload === undefined ? {} : message.payload,
|
|
409
|
-
metadata: {
|
|
410
|
-
...withoutInternalKeys(message.metadata),
|
|
411
|
-
// Add connection info
|
|
412
|
-
connectionId,
|
|
413
|
-
remoteAddress: req.socket.remoteAddress,
|
|
414
|
-
headers: req.headers,
|
|
415
|
-
},
|
|
416
|
-
signal: controller.signal,
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
try {
|
|
420
|
-
// Process through universal server
|
|
421
|
-
const serverResponse = await this.server.handle(serverRequest);
|
|
422
|
-
|
|
423
|
-
if (serverResponse.stream) {
|
|
424
|
-
const credit =
|
|
425
|
-
typeof message.credit === "number" && message.credit > 0 ? new StreamCredit(message.credit) : undefined;
|
|
426
|
-
if (credit) this.credits.set(key, credit);
|
|
427
|
-
await this.sendStream(ws, serverResponse.id, serverResponse.stream, controller.signal, credit);
|
|
428
|
-
} else {
|
|
429
|
-
this.sendResponse(ws, serverResponse);
|
|
430
|
-
}
|
|
431
|
-
} catch (error) {
|
|
432
|
-
console.error("[WebSocket] Request handling error:", error);
|
|
433
|
-
this.sendError(
|
|
434
|
-
ws,
|
|
435
|
-
message.id,
|
|
436
|
-
error instanceof Error ? error.message : "Internal server error"
|
|
437
|
-
);
|
|
438
|
-
} finally {
|
|
439
|
-
// Delete only this request's entries (deep dive TRN-9)
|
|
440
|
-
if (this.inflight.get(key) === controller) this.inflight.delete(key);
|
|
441
|
-
this.credits.delete(key);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Send a stream response (BUGS-2026-07 H5): one "stream" frame for each item, then a frame
|
|
447
|
-
* with `stream.done`. An error of the stream ends it with an "error" frame. When the signal
|
|
448
|
-
* aborts (a cancel message, a closed connection), the loop stops, and the stream's handler
|
|
449
|
-
* gets return().
|
|
450
|
-
*
|
|
451
|
-
* Flow control (deep dive TRN-3): the stream takes its next item only when the client has
|
|
452
|
-
* credit for it (when the request carried credit), and waits while the connection's send
|
|
453
|
-
* buffer holds more than `highWaterMark` bytes.
|
|
454
|
-
*/
|
|
455
|
-
private async sendStream(
|
|
456
|
-
ws: WebSocket,
|
|
457
|
-
id: string,
|
|
458
|
-
stream: AsyncIterable<unknown>,
|
|
459
|
-
signal: AbortSignal,
|
|
460
|
-
credit: StreamCredit | undefined
|
|
461
|
-
): Promise<void> {
|
|
462
|
-
const iterator = stream[Symbol.asyncIterator]();
|
|
463
|
-
let done = false;
|
|
464
|
-
try {
|
|
465
|
-
for (;;) {
|
|
466
|
-
if (credit && !(await credit.take(signal))) return;
|
|
467
|
-
if (signal.aborted || ws.readyState !== WebSocket.OPEN) return;
|
|
468
|
-
const result = await iterator.next();
|
|
469
|
-
if (result.done) {
|
|
470
|
-
done = true;
|
|
471
|
-
break;
|
|
472
|
-
}
|
|
473
|
-
if (signal.aborted || ws.readyState !== WebSocket.OPEN) return;
|
|
474
|
-
await this.sendWithBackpressure(ws, { id, type: "stream", payload: result.value, stream: { done: false } }, signal);
|
|
475
|
-
}
|
|
476
|
-
this.send(ws, { id, type: "stream", stream: { done: true } });
|
|
477
|
-
} catch (error) {
|
|
478
|
-
done = true;
|
|
479
|
-
if (signal.aborted) return;
|
|
480
|
-
const status = errorStatus(error);
|
|
481
|
-
this.send(ws, {
|
|
482
|
-
id,
|
|
483
|
-
type: "error",
|
|
484
|
-
status,
|
|
485
|
-
error: { code: status.code, message: status.message, retryable: status.retryable },
|
|
486
|
-
stream: { done: true },
|
|
487
|
-
});
|
|
488
|
-
} finally {
|
|
489
|
-
// The loop stopped before the end: the handler gets return()
|
|
490
|
-
if (!done) await iterator.return?.();
|
|
491
|
-
}
|
|
384
|
+
const channel = this.channels.get(connectionId);
|
|
385
|
+
if (!channel) return;
|
|
386
|
+
// A frame of a call that the server sent to this client (roadmap R24)
|
|
387
|
+
if (channel.outgoing.receive(message)) return;
|
|
388
|
+
// A frame of a call of the client: a request, a cancel or a credit
|
|
389
|
+
if (channel.incoming.receive(message)) return;
|
|
390
|
+
// A server-response frame of an older client, for a call that no longer exists
|
|
391
|
+
if (message.type === "server-response") return;
|
|
392
|
+
this.sendError(ws, message.id || "unknown", "Invalid message format");
|
|
492
393
|
}
|
|
493
394
|
|
|
494
395
|
/**
|
|
@@ -514,26 +415,6 @@ export class WebSocketServerTransport implements ServerTransport {
|
|
|
514
415
|
});
|
|
515
416
|
}
|
|
516
417
|
|
|
517
|
-
private sendResponse(ws: WebSocket, response: ServerResponse): void {
|
|
518
|
-
const message: WebSocketMessage = {
|
|
519
|
-
id: response.id,
|
|
520
|
-
type: response.status.type === "success" ? "response" : "error",
|
|
521
|
-
payload: response.payload,
|
|
522
|
-
metadata: response.metadata,
|
|
523
|
-
status: response.status,
|
|
524
|
-
};
|
|
525
|
-
|
|
526
|
-
if (response.status.type === "error") {
|
|
527
|
-
message.error = {
|
|
528
|
-
code: response.status.code,
|
|
529
|
-
message: response.status.message || "Unknown error",
|
|
530
|
-
retryable: response.status.retryable,
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
this.send(ws, message);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
418
|
/**
|
|
538
419
|
* Send error to client.
|
|
539
420
|
*/
|
|
@@ -611,48 +492,65 @@ export class WebSocketServerTransport implements ServerTransport {
|
|
|
611
492
|
}
|
|
612
493
|
|
|
613
494
|
/**
|
|
614
|
-
*
|
|
615
|
-
*
|
|
495
|
+
* Send a call to a client (roadmap R24): the same frames as the calls of the client, with
|
|
496
|
+
* streaming, credit, cancel, the signal and the remaining time of the call. The client serves it
|
|
497
|
+
* with its own handler (the `serve` option of `WebSocketTransport`). An unknown connection gives a
|
|
498
|
+
* `CONNECTION_NOT_FOUND` error item.
|
|
616
499
|
*/
|
|
617
|
-
|
|
618
|
-
connectionId
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
500
|
+
callConnection<TReq, TRes>(connectionId: string, message: Message<TReq>): AsyncIterable<ResponseItem<TRes>> {
|
|
501
|
+
const channel = this.channels.get(connectionId);
|
|
502
|
+
if (!channel) {
|
|
503
|
+
return (async function* () {
|
|
504
|
+
yield {
|
|
505
|
+
id: message.id,
|
|
506
|
+
status: { type: "error", code: "CONNECTION_NOT_FOUND", message: `Connection not found: ${connectionId}`, retryable: false },
|
|
507
|
+
payload: null as TRes,
|
|
508
|
+
metadata: {},
|
|
509
|
+
} as ResponseItem<TRes>;
|
|
510
|
+
})();
|
|
626
511
|
}
|
|
512
|
+
return channel.outgoing.call<TReq, TRes>(message);
|
|
513
|
+
}
|
|
627
514
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
resolve,
|
|
640
|
-
reject,
|
|
641
|
-
timeout: timeoutHandle,
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
const message: WebSocketMessage = {
|
|
645
|
-
type: "server-request",
|
|
646
|
-
id: requestId,
|
|
647
|
-
path,
|
|
648
|
-
input,
|
|
649
|
-
};
|
|
515
|
+
/**
|
|
516
|
+
* A transport to one client: a `Client` over it calls the procedures that the client serves,
|
|
517
|
+
* with each client middleware (retry, timeouts) of the caller (roadmap R24).
|
|
518
|
+
*/
|
|
519
|
+
transportFor(connectionId: string): Transport {
|
|
520
|
+
return {
|
|
521
|
+
name: `websocket-reverse:${connectionId}`,
|
|
522
|
+
send: <TReq, TRes>(message: Message<TReq>) => this.callConnection<TReq, TRes>(connectionId, message),
|
|
523
|
+
close: async () => undefined,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
650
526
|
|
|
651
|
-
|
|
652
|
-
|
|
527
|
+
/**
|
|
528
|
+
* Call a procedure on a specific client (server-to-client RPC). It gives the value of the
|
|
529
|
+
* call, and fails with its error. It is a wrapper of `callConnection` (roadmap R24).
|
|
530
|
+
*/
|
|
531
|
+
async callClient(connectionId: string, path: string[], input: unknown, timeout = 30000): Promise<unknown> {
|
|
532
|
+
if (!this.channels.has(connectionId)) throw new Error(`Connection not found: ${connectionId}`);
|
|
533
|
+
const signal = AbortSignal.timeout(timeout);
|
|
534
|
+
const message: Message<unknown> = {
|
|
535
|
+
id: `sreq_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
536
|
+
method: pathToMethod(path),
|
|
537
|
+
payload: input,
|
|
538
|
+
metadata: { "procedure-timeout": timeout },
|
|
539
|
+
signal,
|
|
540
|
+
};
|
|
541
|
+
let last: ResponseItem<unknown> | undefined;
|
|
542
|
+
for await (const item of this.callConnection(connectionId, message)) {
|
|
543
|
+
if (item.status.type === "error") {
|
|
544
|
+
if (signal.aborted) throw new Error(`Request timeout after ${timeout}ms`);
|
|
545
|
+
throw Object.assign(new Error(item.status.message), { code: item.status.code, retryable: item.status.retryable });
|
|
546
|
+
}
|
|
547
|
+
last = item;
|
|
548
|
+
}
|
|
549
|
+
return last?.payload;
|
|
653
550
|
}
|
|
654
551
|
|
|
655
552
|
/**
|
|
553
|
+
* Register a handler for new connections. /**
|
|
656
554
|
* Register a handler for new connections.
|
|
657
555
|
* Returns a function to unregister the handler.
|
|
658
556
|
*/
|