@floegence/flowersec-core 0.19.11 → 0.20.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/README.md +7 -7
- package/dist/browser/reconnectConfig.d.ts +8 -36
- package/dist/browser/reconnectConfig.js +13 -70
- package/dist/client-connect/connectCore.d.ts +14 -5
- package/dist/client-connect/connectCore.js +130 -38
- package/dist/client-connect/transportSecurity.js +13 -15
- package/dist/client.d.ts +2 -0
- package/dist/e2ee/handshake.d.ts +4 -0
- package/dist/e2ee/handshake.js +2 -0
- package/dist/e2ee/secureChannel.d.ts +4 -0
- package/dist/e2ee/secureChannel.js +18 -9
- package/dist/facade.d.ts +3 -0
- package/dist/node/reconnectConfig.d.ts +8 -34
- package/dist/node/reconnectConfig.js +13 -71
- package/dist/node/wsFactory.js +3 -0
- package/dist/observability/observer.d.ts +5 -2
- package/dist/observability/observer.js +3 -0
- package/dist/reconnect/artifactControlplane.d.ts +9 -6
- package/dist/reconnect/artifactControlplane.js +28 -27
- package/dist/reconnect/index.d.ts +2 -0
- package/dist/reconnect/index.js +7 -0
- package/dist/rpc/server.d.ts +29 -4
- package/dist/rpc/server.js +142 -34
- package/dist/tunnel-client/connect.js +6 -6
- package/dist/utils/errors.d.ts +1 -1
- package/dist/ws-client/binaryTransport.d.ts +20 -5
- package/dist/ws-client/binaryTransport.js +110 -8
- package/dist/yamux/byteReader.d.ts +1 -0
- package/dist/yamux/byteReader.js +26 -0
- package/dist/yamux/errors.d.ts +7 -0
- package/dist/yamux/errors.js +15 -0
- package/dist/yamux/session.d.ts +31 -1
- package/dist/yamux/session.js +191 -12
- package/dist/yamux/stream.d.ts +10 -3
- package/dist/yamux/stream.js +61 -19
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@ export class SecureChannel {
|
|
|
14
14
|
transport;
|
|
15
15
|
// Maximum allowed bytes per record frame.
|
|
16
16
|
maxRecordBytes;
|
|
17
|
+
outboundRecordChunkBytes;
|
|
17
18
|
// Upper bound for buffered plaintext in memory.
|
|
18
19
|
maxBufferedBytes;
|
|
19
20
|
// Active encryption keys and nonce prefixes for the current epoch.
|
|
@@ -48,6 +49,11 @@ export class SecureChannel {
|
|
|
48
49
|
constructor(args) {
|
|
49
50
|
this.transport = args.transport;
|
|
50
51
|
this.maxRecordBytes = args.maxRecordBytes;
|
|
52
|
+
const maxPlain = Math.max(1, maxPlaintextBytes(this.maxRecordBytes));
|
|
53
|
+
this.outboundRecordChunkBytes = args.outboundRecordChunkBytes ?? Math.min(64 * 1024, maxPlain);
|
|
54
|
+
if (!Number.isSafeInteger(this.outboundRecordChunkBytes) || this.outboundRecordChunkBytes <= 0 || this.outboundRecordChunkBytes > maxPlain) {
|
|
55
|
+
throw new RangeError("outboundRecordChunkBytes must be a positive integer within the record plaintext limit");
|
|
56
|
+
}
|
|
51
57
|
this.maxBufferedBytes = Math.max(0, args.maxBufferedBytes ?? 4 * (1 << 20));
|
|
52
58
|
this.sendKey = args.sendKey;
|
|
53
59
|
this.recvKey = args.recvKey;
|
|
@@ -64,13 +70,9 @@ export class SecureChannel {
|
|
|
64
70
|
}
|
|
65
71
|
// write splits payloads into record-sized chunks and queues them for send.
|
|
66
72
|
async write(plaintext) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const chunk = plaintext.slice(off, Math.min(plaintext.length, off + maxPlain));
|
|
71
|
-
await this.enqueueSend("app", chunk);
|
|
72
|
-
off += chunk.length;
|
|
73
|
-
}
|
|
73
|
+
if (plaintext.length === 0)
|
|
74
|
+
return;
|
|
75
|
+
await this.enqueueSend("app", plaintext.slice());
|
|
74
76
|
}
|
|
75
77
|
// read resolves with the next plaintext chunk or throws on errors/close.
|
|
76
78
|
async read() {
|
|
@@ -216,8 +218,15 @@ export class SecureChannel {
|
|
|
216
218
|
try {
|
|
217
219
|
let frame;
|
|
218
220
|
if (req.kind === "app") {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
+
const payload = req.payload ?? new Uint8Array();
|
|
222
|
+
for (let offset = 0; offset < payload.length; offset += this.outboundRecordChunkBytes) {
|
|
223
|
+
const chunk = payload.subarray(offset, Math.min(payload.length, offset + this.outboundRecordChunkBytes));
|
|
224
|
+
const seq = this.reserveSendSeq();
|
|
225
|
+
frame = encryptRecord(this.sendKey, this.sendNoncePrefix, RECORD_FLAG_APP, seq, chunk, this.maxRecordBytes);
|
|
226
|
+
await this.transport.writeBinary(frame);
|
|
227
|
+
}
|
|
228
|
+
req.resolve();
|
|
229
|
+
continue;
|
|
221
230
|
}
|
|
222
231
|
else if (req.kind === "ping") {
|
|
223
232
|
const seq = this.reserveSendSeq();
|
package/dist/facade.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ export type { ConnectArtifact, CorrelationContext, CorrelationKV, DirectClientCo
|
|
|
12
12
|
export { assertConnectArtifact } from "./connect/artifact.js";
|
|
13
13
|
export type { ClientObserverLike } from "./observability/observer.js";
|
|
14
14
|
export type { Client, ClientPath } from "./client.js";
|
|
15
|
+
export type { LivenessOptions } from "./client-connect/connectCore.js";
|
|
16
|
+
export type { WebSocketLimits } from "./ws-client/binaryTransport.js";
|
|
17
|
+
export type { YamuxLimits } from "./yamux/session.js";
|
|
15
18
|
export type { FlowersecErrorCode, FlowersecPath, FlowersecStage } from "./utils/errors.js";
|
|
16
19
|
export { FlowersecError } from "./utils/errors.js";
|
|
17
20
|
export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "./client-connect/transportSecurity.js";
|
|
@@ -1,42 +1,16 @@
|
|
|
1
|
-
import type { ConnectArtifact } from "../connect/artifact.js";
|
|
2
|
-
import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
|
|
3
|
-
import type { DirectConnectInfo } from "../gen/flowersec/direct/v1.gen.js";
|
|
4
1
|
import type { ClientObserverLike } from "../observability/observer.js";
|
|
5
2
|
import type { DirectConnectOptions } from "../direct-client/connect.js";
|
|
6
3
|
import type { TunnelConnectOptions } from "../tunnel-client/connect.js";
|
|
7
4
|
import type { AutoReconnectConfig, ConnectConfig as ReconnectConnectConfig } from "../reconnect/index.js";
|
|
8
|
-
import { type
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
import { type ArtifactSource } from "../reconnect/artifactControlplane.js";
|
|
6
|
+
export type NodeReconnectConfig = Readonly<{
|
|
7
|
+
source: ArtifactSource;
|
|
8
|
+
connect?: Omit<TunnelConnectOptions, "observer" | "signal"> | Omit<DirectConnectOptions, "observer" | "signal">;
|
|
11
9
|
observer?: ClientObserverLike;
|
|
12
10
|
autoReconnect?: AutoReconnectConfig;
|
|
13
11
|
}>;
|
|
14
|
-
type
|
|
15
|
-
type
|
|
16
|
-
type ArtifactAwareTunnelReconnectConfig = ArtifactAwareReconnectConfig & Readonly<{
|
|
17
|
-
artifact?: ConnectArtifact;
|
|
18
|
-
getArtifact?: (args: ArtifactFactoryArgs) => Promise<ConnectArtifact>;
|
|
19
|
-
artifactControlplane?: RequestConnectArtifactInput | RequestEntryConnectArtifactInput;
|
|
20
|
-
}>;
|
|
21
|
-
type ArtifactAwareDirectReconnectConfig = ArtifactAwareReconnectConfig & Readonly<{
|
|
22
|
-
artifact?: ConnectArtifact;
|
|
23
|
-
getArtifact?: (args: ArtifactFactoryArgs) => Promise<ConnectArtifact>;
|
|
24
|
-
artifactControlplane?: RequestConnectArtifactInput | RequestEntryConnectArtifactInput;
|
|
25
|
-
}>;
|
|
26
|
-
export type TunnelNodeReconnectConfig = SharedReconnectOptions & ArtifactAwareTunnelReconnectConfig & Readonly<{
|
|
27
|
-
mode?: "tunnel";
|
|
28
|
-
connect?: TunnelReconnectConnectOptions;
|
|
29
|
-
grant?: ChannelInitGrant;
|
|
30
|
-
getGrant?: () => Promise<ChannelInitGrant>;
|
|
31
|
-
}>;
|
|
32
|
-
export type DirectNodeReconnectConfig = SharedReconnectOptions & ArtifactAwareDirectReconnectConfig & Readonly<{
|
|
33
|
-
mode: "direct";
|
|
34
|
-
connect?: DirectReconnectConnectOptions;
|
|
35
|
-
directInfo?: DirectConnectInfo;
|
|
36
|
-
getDirectInfo?: () => Promise<DirectConnectInfo>;
|
|
37
|
-
}>;
|
|
38
|
-
export type NodeReconnectConfig = TunnelNodeReconnectConfig | DirectNodeReconnectConfig;
|
|
39
|
-
export declare function createTunnelNodeReconnectConfig(config: TunnelNodeReconnectConfig): ReconnectConnectConfig;
|
|
40
|
-
export declare function createDirectNodeReconnectConfig(config: DirectNodeReconnectConfig): ReconnectConnectConfig;
|
|
12
|
+
export type TunnelNodeReconnectConfig = NodeReconnectConfig;
|
|
13
|
+
export type DirectNodeReconnectConfig = NodeReconnectConfig;
|
|
41
14
|
export declare function createNodeReconnectConfig(config: NodeReconnectConfig): ReconnectConnectConfig;
|
|
42
|
-
export
|
|
15
|
+
export declare const createTunnelNodeReconnectConfig: typeof createNodeReconnectConfig;
|
|
16
|
+
export declare const createDirectNodeReconnectConfig: typeof createNodeReconnectConfig;
|
|
@@ -1,78 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
if (config.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
async function resolveDirectInfo(config) {
|
|
11
|
-
if (config.getDirectInfo)
|
|
12
|
-
return await config.getDirectInfo();
|
|
13
|
-
if (config.directInfo)
|
|
14
|
-
return config.directInfo;
|
|
15
|
-
throw new Error("Direct reconnect config requires `getDirectInfo` or `directInfo`");
|
|
16
|
-
}
|
|
17
|
-
export function createTunnelNodeReconnectConfig(config) {
|
|
18
|
-
let traceId = config.artifact?.correlation?.trace_id;
|
|
19
|
-
return {
|
|
20
|
-
...(config.observer === undefined ? {} : { observer: config.observer }),
|
|
21
|
-
...(config.autoReconnect === undefined ? {} : { autoReconnect: config.autoReconnect }),
|
|
22
|
-
connectOnce: async ({ signal, observer }) => {
|
|
23
|
-
if (config.getArtifact || config.artifact || config.artifactControlplane) {
|
|
24
|
-
const artifact = await resolveConnectArtifact(config, traceId, signal);
|
|
25
|
-
if (artifact.transport !== "tunnel") {
|
|
26
|
-
throw new Error("Tunnel reconnect config requires a tunnel ConnectArtifact");
|
|
27
|
-
}
|
|
28
|
-
traceId = updateTraceId(traceId, artifact);
|
|
29
|
-
const connectOptions = {
|
|
30
|
-
...(config.connect === undefined ? {} : config.connect),
|
|
31
|
-
signal,
|
|
32
|
-
observer,
|
|
33
|
-
};
|
|
34
|
-
return await connectNode(artifact, connectOptions);
|
|
35
|
-
}
|
|
36
|
-
const connectOptions = {
|
|
37
|
-
...(config.connect === undefined ? {} : config.connect),
|
|
38
|
-
signal,
|
|
39
|
-
observer,
|
|
40
|
-
};
|
|
41
|
-
return await connectTunnelNode(await resolveTunnelGrant(config), connectOptions);
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
export function createDirectNodeReconnectConfig(config) {
|
|
46
|
-
let traceId = config.artifact?.correlation?.trace_id;
|
|
1
|
+
import { createArtifactResolver, updateTraceId } from "../reconnect/artifactControlplane.js";
|
|
2
|
+
import { connectNode } from "./connect.js";
|
|
3
|
+
export function createNodeReconnectConfig(config) {
|
|
4
|
+
if (config.source.kind === "once" && config.autoReconnect?.enabled) {
|
|
5
|
+
throw new Error("automatic reconnect requires a refreshable artifact source");
|
|
6
|
+
}
|
|
7
|
+
let traceId = config.source.kind === "once" ? config.source.artifact.correlation?.trace_id : undefined;
|
|
8
|
+
const acquire = createArtifactResolver(config.source);
|
|
47
9
|
return {
|
|
48
10
|
...(config.observer === undefined ? {} : { observer: config.observer }),
|
|
49
11
|
...(config.autoReconnect === undefined ? {} : { autoReconnect: config.autoReconnect }),
|
|
50
12
|
connectOnce: async ({ signal, observer }) => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
throw new Error("Direct reconnect config requires a direct ConnectArtifact");
|
|
55
|
-
}
|
|
56
|
-
traceId = updateTraceId(traceId, artifact);
|
|
57
|
-
const connectOptions = {
|
|
58
|
-
...(config.connect === undefined ? {} : config.connect),
|
|
59
|
-
signal,
|
|
60
|
-
observer,
|
|
61
|
-
};
|
|
62
|
-
return await connectNode(artifact, connectOptions);
|
|
63
|
-
}
|
|
64
|
-
const connectOptions = {
|
|
65
|
-
...(config.connect === undefined ? {} : config.connect),
|
|
66
|
-
signal,
|
|
67
|
-
observer,
|
|
68
|
-
};
|
|
69
|
-
return await connectDirectNode(await resolveDirectInfo(config), connectOptions);
|
|
13
|
+
const artifact = await acquire({ ...(traceId === undefined ? {} : { traceId }), signal });
|
|
14
|
+
traceId = updateTraceId(traceId, artifact);
|
|
15
|
+
return await connectNode(artifact, { ...(config.connect ?? {}), signal, observer });
|
|
70
16
|
},
|
|
71
17
|
};
|
|
72
18
|
}
|
|
73
|
-
export
|
|
74
|
-
|
|
75
|
-
return createDirectNodeReconnectConfig(config);
|
|
76
|
-
}
|
|
77
|
-
return createTunnelNodeReconnectConfig(config);
|
|
78
|
-
}
|
|
19
|
+
export const createTunnelNodeReconnectConfig = createNodeReconnectConfig;
|
|
20
|
+
export const createDirectNodeReconnectConfig = createNodeReconnectConfig;
|
package/dist/node/wsFactory.js
CHANGED
|
@@ -6,13 +6,13 @@ export type AttachReason = "send_failed" | "too_many_connections" | "expected_at
|
|
|
6
6
|
export type HandshakeResult = "ok" | "fail";
|
|
7
7
|
export type HandshakeReason = "auth_tag_mismatch" | "handshake_failed" | "invalid_suite" | "invalid_version" | "timestamp_after_init_exp" | "timestamp_out_of_skew" | "timeout" | "canceled";
|
|
8
8
|
export type WsCloseKind = "local" | "peer_or_error";
|
|
9
|
-
export type WsErrorReason = "error" | "recv_buffer_exceeded" | "unexpected_text_frame" | "unexpected_message_type";
|
|
9
|
+
export type WsErrorReason = "error" | "recv_buffer_exceeded" | "send_buffer_exceeded" | "send_buffer_timeout" | "unexpected_text_frame" | "unexpected_message_type";
|
|
10
10
|
export type RpcCallResult = "ok" | "rpc_error" | "handler_not_found" | "transport_error" | "canceled";
|
|
11
11
|
export type DiagnosticEvent = Readonly<{
|
|
12
12
|
v: 1;
|
|
13
13
|
namespace: "connect";
|
|
14
14
|
path: ClientPath | "auto";
|
|
15
|
-
stage: "validate" | "normalize" | "scope" | "connect" | "attach" | "handshake" | "close" | "reconnect";
|
|
15
|
+
stage: "validate" | "normalize" | "scope" | "connect" | "attach" | "handshake" | "transport" | "yamux" | "rpc" | "close" | "reconnect";
|
|
16
16
|
code_domain: "error" | "event";
|
|
17
17
|
code: string;
|
|
18
18
|
result: "ok" | "fail" | "retry" | "skip";
|
|
@@ -20,6 +20,9 @@ export type DiagnosticEvent = Readonly<{
|
|
|
20
20
|
attempt_seq: number;
|
|
21
21
|
trace_id?: string;
|
|
22
22
|
session_id?: string;
|
|
23
|
+
resource?: string;
|
|
24
|
+
current?: number;
|
|
25
|
+
limit?: number;
|
|
23
26
|
}>;
|
|
24
27
|
type ObserverContext = Readonly<{
|
|
25
28
|
path?: ClientPath | "auto";
|
|
@@ -54,6 +54,9 @@ function buildDiagnosticEvent(context, event) {
|
|
|
54
54
|
...(context.sessionId === undefined
|
|
55
55
|
? {}
|
|
56
56
|
: { session_id: context.sessionId }),
|
|
57
|
+
...(event.resource === undefined ? {} : { resource: event.resource }),
|
|
58
|
+
...(event.current === undefined ? {} : { current: event.current }),
|
|
59
|
+
...(event.limit === undefined ? {} : { limit: event.limit }),
|
|
57
60
|
});
|
|
58
61
|
}
|
|
59
62
|
function mapConnectDiagnostic(path, result, reason) {
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { ConnectArtifact } from "../connect/artifact.js";
|
|
2
2
|
import { type RequestConnectArtifactInput, type RequestEntryConnectArtifactInput } from "../controlplane/index.js";
|
|
3
|
-
export type
|
|
3
|
+
export type ArtifactAcquireContext = Readonly<{
|
|
4
4
|
traceId?: string;
|
|
5
5
|
signal?: AbortSignal;
|
|
6
6
|
}>;
|
|
7
|
-
export type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
export type ArtifactSource = Readonly<{
|
|
8
|
+
kind: "once";
|
|
9
|
+
artifact: ConnectArtifact;
|
|
10
|
+
}> | Readonly<{
|
|
11
|
+
kind: "refreshable";
|
|
12
|
+
acquire: (context: ArtifactAcquireContext) => Promise<ConnectArtifact>;
|
|
11
13
|
}>;
|
|
12
|
-
export declare function
|
|
14
|
+
export declare function createControlplaneArtifactSource(input: RequestConnectArtifactInput | RequestEntryConnectArtifactInput): ArtifactSource;
|
|
15
|
+
export declare function createArtifactResolver(source: ArtifactSource): (context: ArtifactAcquireContext) => Promise<ConnectArtifact>;
|
|
13
16
|
export declare function updateTraceId(current: string | undefined, artifact: ConnectArtifact): string | undefined;
|
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
import { requestConnectArtifact, requestEntryConnectArtifact, } from "../controlplane/index.js";
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const input = {
|
|
17
|
-
...config.artifactControlplane,
|
|
2
|
+
export function createControlplaneArtifactSource(input) {
|
|
3
|
+
return {
|
|
4
|
+
kind: "refreshable",
|
|
5
|
+
acquire: async ({ traceId, signal }) => {
|
|
6
|
+
const correlation = traceId === undefined ? input.correlation : { traceId };
|
|
7
|
+
if ("entryTicket" in input) {
|
|
8
|
+
return await requestEntryConnectArtifact({
|
|
9
|
+
...input,
|
|
10
|
+
...(correlation === undefined ? {} : { correlation }),
|
|
11
|
+
...(signal === undefined ? {} : { signal }),
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
return await requestConnectArtifact({
|
|
15
|
+
...input,
|
|
18
16
|
...(correlation === undefined ? {} : { correlation }),
|
|
19
17
|
...(signal === undefined ? {} : { signal }),
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function createArtifactResolver(source) {
|
|
23
|
+
let consumed = false;
|
|
24
|
+
return async (context) => {
|
|
25
|
+
if (source.kind === "refreshable")
|
|
26
|
+
return await source.acquire(context);
|
|
27
|
+
if (consumed)
|
|
28
|
+
throw new Error("one-time artifact source has already been consumed");
|
|
29
|
+
consumed = true;
|
|
30
|
+
return source.artifact;
|
|
31
|
+
};
|
|
31
32
|
}
|
|
32
33
|
export function updateTraceId(current, artifact) {
|
|
33
34
|
return artifact.correlation?.trace_id ?? current;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Client } from "../client.js";
|
|
2
2
|
import { type ClientObserverLike } from "../observability/observer.js";
|
|
3
|
+
export type { ArtifactAcquireContext, ArtifactSource } from "./artifactControlplane.js";
|
|
4
|
+
export { createArtifactResolver, createControlplaneArtifactSource } from "./artifactControlplane.js";
|
|
3
5
|
export type ConnectionStatus = "disconnected" | "connecting" | "connected" | "error";
|
|
4
6
|
export type AutoReconnectConfig = Readonly<{
|
|
5
7
|
enabled?: boolean;
|
package/dist/reconnect/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { emitObserverDiagnostic, withObserverContext } from "../observability/observer.js";
|
|
2
|
+
export { createArtifactResolver, createControlplaneArtifactSource } from "./artifactControlplane.js";
|
|
2
3
|
function normalizeAutoReconnect(cfg) {
|
|
3
4
|
if (!cfg?.enabled) {
|
|
4
5
|
return {
|
|
@@ -159,6 +160,12 @@ export function createReconnectManager() {
|
|
|
159
160
|
},
|
|
160
161
|
onRpcCall: (...args) => user?.onRpcCall?.(...args),
|
|
161
162
|
onRpcNotify: (...args) => user?.onRpcNotify?.(...args),
|
|
163
|
+
onDiagnosticEvent: (event) => {
|
|
164
|
+
user?.onDiagnosticEvent?.(event);
|
|
165
|
+
if (event.code === "liveness_timeout") {
|
|
166
|
+
startReconnect(t, cfg, new Error("liveness timeout"));
|
|
167
|
+
}
|
|
168
|
+
},
|
|
162
169
|
}, {
|
|
163
170
|
attemptSeq: currentAttemptSeq,
|
|
164
171
|
});
|
package/dist/rpc/server.d.ts
CHANGED
|
@@ -3,13 +3,38 @@ export type RpcHandler = (payload: unknown) => Promise<{
|
|
|
3
3
|
payload: unknown;
|
|
4
4
|
error?: RpcError;
|
|
5
5
|
}>;
|
|
6
|
+
export type RpcServerOptions = Readonly<{
|
|
7
|
+
maxConcurrentRequests?: number;
|
|
8
|
+
maxQueuedRequests?: number;
|
|
9
|
+
maxQueuedNotifications?: number;
|
|
10
|
+
}>;
|
|
11
|
+
export type RpcServerTransport = Readonly<{
|
|
12
|
+
readExactly(n: number): Promise<Uint8Array>;
|
|
13
|
+
write(bytes: Uint8Array): Promise<void>;
|
|
14
|
+
close(error: unknown): void;
|
|
15
|
+
}>;
|
|
6
16
|
export declare class RpcServer {
|
|
7
|
-
private readonly
|
|
8
|
-
private readonly write;
|
|
17
|
+
private readonly transport;
|
|
9
18
|
private readonly handlers;
|
|
10
19
|
private closed;
|
|
11
|
-
|
|
20
|
+
private readonly options;
|
|
21
|
+
private readonly requests;
|
|
22
|
+
private readonly notifications;
|
|
23
|
+
private requestWaiters;
|
|
24
|
+
private notificationWaiters;
|
|
25
|
+
private writeChain;
|
|
26
|
+
private terminalError;
|
|
27
|
+
private readonly terminalSignal;
|
|
28
|
+
private signalTerminal;
|
|
29
|
+
private transportClosed;
|
|
30
|
+
constructor(transport: RpcServerTransport, options?: RpcServerOptions);
|
|
12
31
|
register(typeId: number, h: RpcHandler): void;
|
|
13
32
|
serve(signal?: AbortSignal): Promise<void>;
|
|
14
|
-
close(): void;
|
|
33
|
+
close(error?: unknown): void;
|
|
34
|
+
private fail;
|
|
35
|
+
private requestWorker;
|
|
36
|
+
private notificationWorker;
|
|
37
|
+
private nextWork;
|
|
38
|
+
private wakeOne;
|
|
39
|
+
private writeResponse;
|
|
15
40
|
}
|
package/dist/rpc/server.js
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
import { DEFAULT_MAX_JSON_FRAME_BYTES, readJsonFrame, writeJsonFrame } from "../framing/jsonframe.js";
|
|
2
2
|
import { assertRpcEnvelope } from "./validate.js";
|
|
3
|
+
const DEFAULT_RPC_SERVER_OPTIONS = Object.freeze({
|
|
4
|
+
maxConcurrentRequests: 32,
|
|
5
|
+
maxQueuedRequests: 128,
|
|
6
|
+
maxQueuedNotifications: 128,
|
|
7
|
+
});
|
|
3
8
|
// RpcServer dispatches request envelopes to registered handlers.
|
|
4
9
|
export class RpcServer {
|
|
5
|
-
|
|
6
|
-
write;
|
|
10
|
+
transport;
|
|
7
11
|
// Registered handlers keyed by type ID.
|
|
8
12
|
handlers = new Map();
|
|
9
13
|
// Closed flag to stop the serve loop.
|
|
10
14
|
closed = false;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
options;
|
|
16
|
+
requests = [];
|
|
17
|
+
notifications = [];
|
|
18
|
+
requestWaiters = [];
|
|
19
|
+
notificationWaiters = [];
|
|
20
|
+
writeChain = Promise.resolve();
|
|
21
|
+
terminalError;
|
|
22
|
+
terminalSignal;
|
|
23
|
+
signalTerminal;
|
|
24
|
+
transportClosed = false;
|
|
25
|
+
constructor(transport, options = {}) {
|
|
26
|
+
this.transport = transport;
|
|
27
|
+
this.terminalSignal = new Promise((resolve) => { this.signalTerminal = resolve; });
|
|
28
|
+
this.options = {
|
|
29
|
+
maxConcurrentRequests: positiveInteger(options.maxConcurrentRequests ?? DEFAULT_RPC_SERVER_OPTIONS.maxConcurrentRequests, "maxConcurrentRequests"),
|
|
30
|
+
maxQueuedRequests: nonNegativeInteger(options.maxQueuedRequests ?? DEFAULT_RPC_SERVER_OPTIONS.maxQueuedRequests, "maxQueuedRequests"),
|
|
31
|
+
maxQueuedNotifications: nonNegativeInteger(options.maxQueuedNotifications ?? DEFAULT_RPC_SERVER_OPTIONS.maxQueuedNotifications, "maxQueuedNotifications"),
|
|
32
|
+
};
|
|
14
33
|
}
|
|
15
34
|
// register binds a handler to a type ID.
|
|
16
35
|
register(typeId, h) {
|
|
@@ -18,50 +37,139 @@ export class RpcServer {
|
|
|
18
37
|
}
|
|
19
38
|
// serve handles request/response frames until closed or aborted.
|
|
20
39
|
async serve(signal) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
const supervise = (worker) => worker.catch((err) => {
|
|
41
|
+
this.fail(err);
|
|
42
|
+
});
|
|
43
|
+
const workers = Array.from({ length: this.options.maxConcurrentRequests }, () => supervise(this.requestWorker()));
|
|
44
|
+
workers.push(supervise(this.notificationWorker()));
|
|
45
|
+
try {
|
|
46
|
+
while (!this.closed) {
|
|
47
|
+
if (signal?.aborted)
|
|
48
|
+
throw signal.reason ?? new Error("aborted");
|
|
49
|
+
const next = await Promise.race([
|
|
50
|
+
readJsonFrame(this.transport.readExactly, DEFAULT_MAX_JSON_FRAME_BYTES),
|
|
51
|
+
this.terminalSignal.then((error) => { throw error; }),
|
|
52
|
+
]);
|
|
53
|
+
const v = assertRpcEnvelope(next);
|
|
54
|
+
if (v.response_to !== 0)
|
|
55
|
+
continue;
|
|
56
|
+
if (v.request_id === 0) {
|
|
57
|
+
if (this.notifications.length >= this.options.maxQueuedNotifications) {
|
|
58
|
+
throw new Error("rpc notification queue exhausted");
|
|
35
59
|
}
|
|
60
|
+
this.notifications.push({ envelope: v });
|
|
61
|
+
this.wakeOne(this.notificationWaiters);
|
|
62
|
+
continue;
|
|
36
63
|
}
|
|
37
|
-
|
|
64
|
+
if (this.requests.length >= this.options.maxQueuedRequests) {
|
|
65
|
+
await this.writeResponse(v, { payload: null, error: { code: 429, message: "server overloaded" } });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
this.requests.push({ envelope: v });
|
|
69
|
+
this.wakeOne(this.requestWaiters);
|
|
38
70
|
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
this.terminalError = err;
|
|
74
|
+
this.close(err);
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
this.close(this.terminalError ?? new Error("rpc server closed"));
|
|
79
|
+
void Promise.allSettled(workers);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// close stops the serve loop and closes the underlying RPC stream.
|
|
83
|
+
close(error = new Error("rpc server closed")) {
|
|
84
|
+
if (!this.closed) {
|
|
85
|
+
this.closed = true;
|
|
86
|
+
this.signalTerminal(error);
|
|
87
|
+
for (const wake of this.requestWaiters.splice(0))
|
|
88
|
+
wake();
|
|
89
|
+
for (const wake of this.notificationWaiters.splice(0))
|
|
90
|
+
wake();
|
|
91
|
+
}
|
|
92
|
+
if (!this.transportClosed) {
|
|
93
|
+
this.transportClosed = true;
|
|
94
|
+
this.transport.close(error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
fail(error) {
|
|
98
|
+
if (this.terminalError !== undefined)
|
|
99
|
+
return;
|
|
100
|
+
this.terminalError = error;
|
|
101
|
+
this.close(error);
|
|
102
|
+
}
|
|
103
|
+
async requestWorker() {
|
|
104
|
+
while (!this.closed) {
|
|
105
|
+
const work = await this.nextWork(this.requests, this.requestWaiters);
|
|
106
|
+
if (work == null)
|
|
107
|
+
return;
|
|
108
|
+
const v = work.envelope;
|
|
39
109
|
const h = this.handlers.get(v.type_id >>> 0);
|
|
40
110
|
let out;
|
|
41
|
-
if (h == null)
|
|
111
|
+
if (h == null)
|
|
42
112
|
out = { payload: null, error: { code: 404, message: "handler not found" } };
|
|
43
|
-
}
|
|
44
113
|
else {
|
|
45
114
|
try {
|
|
46
115
|
out = await h(v.payload);
|
|
47
116
|
}
|
|
48
117
|
catch {
|
|
49
|
-
// Keep the serve loop alive on request handler errors.
|
|
50
118
|
out = { payload: null, error: { code: 500, message: "internal error" } };
|
|
51
119
|
}
|
|
52
120
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
response_to: v.request_id,
|
|
57
|
-
payload: out.payload,
|
|
58
|
-
...(out.error != null ? { error: out.error } : {})
|
|
59
|
-
};
|
|
60
|
-
await writeJsonFrame(this.write, resp);
|
|
121
|
+
if (this.closed)
|
|
122
|
+
return;
|
|
123
|
+
await this.writeResponse(v, out);
|
|
61
124
|
}
|
|
62
125
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
126
|
+
async notificationWorker() {
|
|
127
|
+
while (!this.closed) {
|
|
128
|
+
const work = await this.nextWork(this.notifications, this.notificationWaiters);
|
|
129
|
+
if (work == null)
|
|
130
|
+
return;
|
|
131
|
+
const v = work.envelope;
|
|
132
|
+
const h = this.handlers.get(v.type_id >>> 0);
|
|
133
|
+
if (h == null)
|
|
134
|
+
continue;
|
|
135
|
+
try {
|
|
136
|
+
await h(v.payload);
|
|
137
|
+
}
|
|
138
|
+
catch { /* Notification failures are isolated. */ }
|
|
139
|
+
}
|
|
66
140
|
}
|
|
141
|
+
async nextWork(queue, waiters) {
|
|
142
|
+
while (!this.closed) {
|
|
143
|
+
const work = queue.shift();
|
|
144
|
+
if (work != null)
|
|
145
|
+
return work;
|
|
146
|
+
await new Promise((resolve) => waiters.push(resolve));
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
wakeOne(waiters) {
|
|
151
|
+
waiters.shift()?.();
|
|
152
|
+
}
|
|
153
|
+
async writeResponse(request, out) {
|
|
154
|
+
const resp = {
|
|
155
|
+
type_id: request.type_id,
|
|
156
|
+
request_id: 0,
|
|
157
|
+
response_to: request.request_id,
|
|
158
|
+
payload: out.payload,
|
|
159
|
+
...(out.error != null ? { error: out.error } : {}),
|
|
160
|
+
};
|
|
161
|
+
const write = this.writeChain.then(() => writeJsonFrame(this.transport.write, resp));
|
|
162
|
+
this.writeChain = write.catch(() => { });
|
|
163
|
+
await write;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function positiveInteger(value, name) {
|
|
167
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
168
|
+
throw new RangeError(`${name} must be a positive integer`);
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
function nonNegativeInteger(value, name) {
|
|
172
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
173
|
+
throw new RangeError(`${name} must be a non-negative integer`);
|
|
174
|
+
return value;
|
|
67
175
|
}
|