@floegence/flowersec-core 0.27.0 → 2.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/README.md +91 -22
- package/THIRD_PARTY_NOTICES.md +141 -0
- package/dist/browser/connectSession.d.ts +13 -0
- package/dist/browser/connectSession.js +40 -0
- package/dist/browser/index.d.ts +3 -10
- package/dist/browser/index.js +2 -5
- package/dist/browser/runtimeCapability.d.ts +13 -0
- package/dist/browser/runtimeCapability.js +27 -0
- package/dist/browser/sessionRuntime.d.ts +2 -0
- package/dist/browser/sessionRuntime.js +21 -0
- package/dist/browser/webTransportClient.d.ts +8 -0
- package/dist/browser/webTransportClient.js +55 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +126 -0
- package/dist/cliSpendMarker.d.ts +1 -0
- package/dist/cliSpendMarker.js +22 -0
- package/dist/connectionController.d.ts +54 -0
- package/dist/connectionController.js +438 -0
- package/dist/connector/adapters/webSocketCandidate.d.ts +7 -0
- package/dist/connector/adapters/webSocketCandidate.js +116 -0
- package/dist/connector/adapters/webTransportCandidate.d.ts +5 -0
- package/dist/connector/adapters/webTransportCandidate.js +73 -0
- package/dist/connector/admissionCommit.d.ts +31 -0
- package/dist/connector/admissionCommit.js +239 -0
- package/dist/connector/sessionAcceptor.d.ts +16 -0
- package/dist/connector/sessionAcceptor.js +114 -0
- package/dist/connector/sessionConfig.d.ts +3 -0
- package/dist/connector/sessionConfig.js +30 -0
- package/dist/connector/sessionConnector.d.ts +45 -0
- package/dist/connector/sessionConnector.js +603 -0
- package/dist/defaults.d.ts +7 -7
- package/dist/defaults.js +7 -7
- package/dist/facade.d.ts +12 -27
- package/dist/facade.js +6 -24
- package/dist/node/connectSession.d.ts +21 -0
- package/dist/node/connectSession.js +62 -0
- package/dist/node/index.d.ts +3 -11
- package/dist/node/index.js +2 -8
- package/dist/node/runtimeCapability.d.ts +7 -0
- package/dist/node/runtimeCapability.js +10 -0
- package/dist/node/sessionRuntime.d.ts +2 -0
- package/dist/node/sessionRuntime.js +13 -0
- package/dist/node/webTransportClient.d.ts +9 -0
- package/dist/node/webTransportClient.js +56 -0
- package/dist/node/webTransportServer.d.ts +22 -0
- package/dist/node/webTransportServer.js +100 -0
- package/dist/node/wsFactory.d.ts +3 -1
- package/dist/node/wsFactory.js +4 -3
- package/dist/proxy/controllerGuard.d.ts +4 -4
- package/dist/proxy/controllerGuard.js +83 -177
- package/dist/proxy/disableUpstreamServiceWorkerRegister.d.ts +3 -1
- package/dist/proxy/disableUpstreamServiceWorkerRegister.js +19 -17
- package/dist/proxy/index.d.ts +16 -15
- package/dist/proxy/index.js +8 -14
- package/dist/proxy/integration.d.ts +23 -71
- package/dist/proxy/integration.js +37 -340
- package/dist/proxy/registerServiceWorker.d.ts +1 -10
- package/dist/proxy/registerServiceWorker.js +43 -105
- package/dist/proxy/runtime.d.ts +3 -63
- package/dist/proxy/runtime.js +349 -462
- package/dist/proxy/scope.d.ts +6 -0
- package/dist/proxy/scope.js +131 -0
- package/dist/proxy/serviceWorker.d.ts +5 -16
- package/dist/proxy/serviceWorker.js +230 -617
- package/dist/proxy/stream.d.ts +9 -0
- package/dist/proxy/stream.js +40 -0
- package/dist/proxy/types.d.ts +78 -30
- package/dist/proxy/windowBridge.d.ts +61 -0
- package/dist/proxy/windowBridge.js +344 -0
- package/dist/proxy/wsPatch.d.ts +4 -14
- package/dist/proxy/wsPatch.js +173 -268
- package/dist/public/artifact.d.ts +11 -0
- package/dist/public/artifact.js +44 -0
- package/dist/public/artifactLease.d.ts +10 -0
- package/dist/public/artifactLease.js +48 -0
- package/dist/public/connectError.d.ts +6 -0
- package/dist/public/connectError.js +9 -0
- package/dist/public/contract.d.ts +83 -0
- package/dist/public/contract.js +17 -0
- package/dist/public/streamMetadata.d.ts +10 -0
- package/dist/public/streamMetadata.js +40 -0
- package/dist/rpc/caller.d.ts +1 -1
- package/dist/rpc/client.d.ts +1 -4
- package/dist/rpc/client.js +0 -21
- package/dist/rpc/rpcProxy.d.ts +1 -1
- package/dist/rpc/server.d.ts +1 -1
- package/dist/rpc/validate.d.ts +1 -1
- package/dist/rpc/wire.d.ts +15 -0
- package/dist/runtime/errors.d.ts +6 -0
- package/dist/runtime/errors.js +10 -0
- package/dist/transport/webSocketAdapter.d.ts +24 -0
- package/dist/transport/webSocketAdapter.js +234 -0
- package/dist/transport/webTransportAdapter.d.ts +27 -0
- package/dist/transport/webTransportAdapter.js +519 -0
- package/dist/utils/errors.d.ts +2 -20
- package/dist/utils/errors.js +73 -20
- package/dist/v2/admissionError.d.ts +4 -0
- package/dist/v2/admissionError.js +8 -0
- package/dist/v2/artifact.d.ts +128 -0
- package/dist/v2/artifact.js +1126 -0
- package/dist/v2/artifactLease.d.ts +1 -0
- package/dist/v2/artifactLease.js +7 -0
- package/dist/v2/capability.d.ts +29 -0
- package/dist/v2/capability.js +190 -0
- package/dist/v2/carrier.d.ts +79 -0
- package/dist/v2/carrier.js +405 -0
- package/dist/v2/contract.d.ts +37 -0
- package/dist/v2/contract.js +1 -0
- package/dist/v2/handshake.d.ts +80 -0
- package/dist/v2/handshake.js +470 -0
- package/dist/v2/opaqueArtifact.d.ts +1 -0
- package/dist/v2/opaqueArtifact.js +2 -0
- package/dist/v2/protocol.d.ts +99 -0
- package/dist/v2/protocol.js +739 -0
- package/dist/v2/publicSession.d.ts +1 -0
- package/dist/v2/publicSession.js +179 -0
- package/dist/v2/retryDisposition.d.ts +16 -0
- package/dist/v2/retryDisposition.js +30 -0
- package/dist/v2/session.d.ts +277 -0
- package/dist/v2/session.js +1879 -0
- package/dist/v2/streamLifetimeLedger.d.ts +31 -0
- package/dist/v2/streamLifetimeLedger.js +131 -0
- package/dist/v2/streamMetadata.d.ts +1 -0
- package/dist/v2/streamMetadata.js +7 -0
- package/dist/v2/unreliableMessage.d.ts +43 -0
- package/dist/v2/unreliableMessage.js +252 -0
- package/dist/vendor/tr46.d.ts +10 -0
- package/dist/vendor/tr46.js +501 -0
- package/dist/ws-client/binaryTransport.d.ts +0 -3
- package/dist/ws-client/binaryTransport.js +15 -39
- package/dist/yamux/errors.d.ts +0 -4
- package/dist/yamux/errors.js +0 -9
- package/dist/yamux/session.d.ts +0 -5
- package/dist/yamux/session.js +4 -70
- package/dist/yamux/stream.d.ts +1 -0
- package/dist/yamux/stream.js +4 -0
- package/package.json +29 -81
- package/sbom/cyclonedx.json +2764 -0
- package/sbom/spdx.json +1947 -0
- package/YAMUX_ALIGNMENT.md +0 -127
- package/dist/_examples/flowersec/demo/v1.facade.gen.d.ts +0 -12
- package/dist/_examples/flowersec/demo/v1.facade.gen.js +0 -15
- package/dist/_examples/flowersec/demo/v1.gen.d.ts +0 -16
- package/dist/_examples/flowersec/demo/v1.gen.js +0 -86
- package/dist/_examples/flowersec/demo/v1.rpc.gen.d.ts +0 -11
- package/dist/_examples/flowersec/demo/v1.rpc.gen.js +0 -22
- package/dist/browser/connect.d.ts +0 -13
- package/dist/browser/connect.js +0 -31
- package/dist/browser/reconnectConfig.d.ts +0 -15
- package/dist/browser/reconnectConfig.js +0 -20
- package/dist/client-connect/common.d.ts +0 -26
- package/dist/client-connect/common.js +0 -167
- package/dist/client-connect/connectCore.d.ts +0 -61
- package/dist/client-connect/connectCore.js +0 -572
- package/dist/client-connect/contract.d.ts +0 -8
- package/dist/client-connect/contract.js +0 -51
- package/dist/client-connect/termination.d.ts +0 -6
- package/dist/client-connect/termination.js +0 -7
- package/dist/client-connect/transportSecurity.d.ts +0 -27
- package/dist/client-connect/transportSecurity.js +0 -199
- package/dist/client-connect/tunnelAttachCloseReason.d.ts +0 -3
- package/dist/client-connect/tunnelAttachCloseReason.js +0 -23
- package/dist/client.d.ts +0 -23
- package/dist/connect/artifact.d.ts +0 -36
- package/dist/connect/artifact.js +0 -213
- package/dist/connect/resolveArtifact.d.ts +0 -22
- package/dist/connect/resolveArtifact.js +0 -87
- package/dist/controlplane/channelInit.d.ts +0 -27
- package/dist/controlplane/channelInit.js +0 -116
- package/dist/controlplane/http.d.ts +0 -16
- package/dist/controlplane/http.js +0 -59
- package/dist/controlplane/index.d.ts +0 -6
- package/dist/controlplane/index.js +0 -5
- package/dist/controlplane/issuer.d.ts +0 -18
- package/dist/controlplane/issuer.js +0 -113
- package/dist/controlplane/request.d.ts +0 -51
- package/dist/controlplane/request.js +0 -273
- package/dist/controlplane/token.d.ts +0 -33
- package/dist/controlplane/token.js +0 -119
- package/dist/direct-client/connect.d.ts +0 -9
- package/dist/direct-client/connect.js +0 -90
- package/dist/direct-client/index.d.ts +0 -1
- package/dist/direct-client/index.js +0 -1
- package/dist/e2ee/constants.d.ts +0 -9
- package/dist/e2ee/constants.js +0 -18
- package/dist/e2ee/errors.d.ts +0 -5
- package/dist/e2ee/errors.js +0 -8
- package/dist/e2ee/framing.d.ts +0 -12
- package/dist/e2ee/framing.js +0 -57
- package/dist/e2ee/handshake.d.ts +0 -88
- package/dist/e2ee/handshake.js +0 -353
- package/dist/e2ee/index.d.ts +0 -7
- package/dist/e2ee/index.js +0 -7
- package/dist/e2ee/kdf.d.ts +0 -15
- package/dist/e2ee/kdf.js +0 -39
- package/dist/e2ee/record.d.ts +0 -11
- package/dist/e2ee/record.js +0 -80
- package/dist/e2ee/secureChannel.d.ts +0 -93
- package/dist/e2ee/secureChannel.js +0 -342
- package/dist/e2ee/transcript.d.ts +0 -23
- package/dist/e2ee/transcript.js +0 -31
- package/dist/endpoint/index.d.ts +0 -84
- package/dist/endpoint/index.js +0 -557
- package/dist/endpoint/node.d.ts +0 -9
- package/dist/endpoint/node.js +0 -51
- package/dist/gen/flowersec/controlplane/v1.gen.d.ts +0 -36
- package/dist/gen/flowersec/controlplane/v1.gen.js +0 -135
- package/dist/gen/flowersec/direct/v1.gen.d.ts +0 -21
- package/dist/gen/flowersec/direct/v1.gen.js +0 -101
- package/dist/gen/flowersec/e2ee/v1.gen.d.ts +0 -68
- package/dist/gen/flowersec/e2ee/v1.gen.js +0 -194
- package/dist/gen/flowersec/rpc/v1.gen.d.ts +0 -30
- package/dist/gen/flowersec/rpc/v1.gen.js +0 -107
- package/dist/gen/flowersec/tunnel/v1.gen.d.ts +0 -23
- package/dist/gen/flowersec/tunnel/v1.gen.js +0 -104
- package/dist/index.d.ts +0 -22
- package/dist/index.js +0 -22
- package/dist/node/connect.d.ts +0 -10
- package/dist/node/connect.js +0 -29
- package/dist/node/reconnectConfig.d.ts +0 -16
- package/dist/node/reconnectConfig.js +0 -20
- package/dist/observability/index.d.ts +0 -1
- package/dist/observability/index.js +0 -1
- package/dist/observability/observer.d.ts +0 -52
- package/dist/observability/observer.js +0 -314
- package/dist/proxy/appWindow.d.ts +0 -37
- package/dist/proxy/appWindow.js +0 -273
- package/dist/proxy/bootstrap.d.ts +0 -35
- package/dist/proxy/bootstrap.js +0 -119
- package/dist/proxy/constants.d.ts +0 -7
- package/dist/proxy/constants.js +0 -8
- package/dist/proxy/controllerWindow.d.ts +0 -12
- package/dist/proxy/controllerWindow.js +0 -384
- package/dist/proxy/cookieJar.d.ts +0 -7
- package/dist/proxy/cookieJar.js +0 -136
- package/dist/proxy/headerPolicy.d.ts +0 -14
- package/dist/proxy/headerPolicy.js +0 -134
- package/dist/proxy/portStream.d.ts +0 -5
- package/dist/proxy/portStream.js +0 -256
- package/dist/proxy/preset.d.ts +0 -29
- package/dist/proxy/preset.js +0 -111
- package/dist/proxy/runtimeScope.d.ts +0 -57
- package/dist/proxy/runtimeScope.js +0 -218
- package/dist/proxy/server.d.ts +0 -28
- package/dist/proxy/server.js +0 -833
- package/dist/proxy/windowBridgeProtocol.d.ts +0 -65
- package/dist/proxy/windowBridgeProtocol.js +0 -11
- package/dist/reconnect/artifactControlplane.d.ts +0 -16
- package/dist/reconnect/artifactControlplane.js +0 -35
- package/dist/reconnect/index.d.ts +0 -36
- package/dist/reconnect/index.js +0 -388
- package/dist/rpc-proxy/index.d.ts +0 -1
- package/dist/rpc-proxy/index.js +0 -1
- package/dist/rpc-proxy/rpcProxy.d.ts +0 -1
- package/dist/rpc-proxy/rpcProxy.js +0 -1
- package/dist/streamhello/index.d.ts +0 -1
- package/dist/streamhello/index.js +0 -1
- package/dist/streamhello/streamHello.d.ts +0 -3
- package/dist/streamhello/streamHello.js +0 -13
- package/dist/streamio/index.d.ts +0 -32
- package/dist/streamio/index.js +0 -87
- package/dist/tunnel-client/connect.d.ts +0 -9
- package/dist/tunnel-client/connect.js +0 -147
- package/dist/tunnel-client/index.d.ts +0 -1
- package/dist/tunnel-client/index.js +0 -1
- package/dist/ws/index.d.ts +0 -1
- package/dist/ws/index.js +0 -1
- /package/dist/{client.js → rpc/wire.js} +0 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { createArtifactLeaseV2 } from "./v2/artifactLease.js";
|
|
4
|
+
import { buildFSB2RequestV2, encodeFSB2RequestV2 } from "./v2/artifact.js";
|
|
5
|
+
import { parseArtifact, unwrapArtifact } from "./v2/opaqueArtifact.js";
|
|
6
|
+
import { connect } from "./node/connectSession.js";
|
|
7
|
+
import { startNodeWebTransportServerV2 } from "./node/webTransportServer.js";
|
|
8
|
+
import { acceptNativeSessionV2 } from "./connector/sessionAcceptor.js";
|
|
9
|
+
import { nodeSessionRuntimeV2 } from "./node/sessionRuntime.js";
|
|
10
|
+
import { claimSpendMarker } from "./cliSpendMarker.js";
|
|
11
|
+
const args = parseArguments(process.argv.slice(2));
|
|
12
|
+
if (args.mode === "client")
|
|
13
|
+
await runClient(args.values);
|
|
14
|
+
else
|
|
15
|
+
await runServer(args.values);
|
|
16
|
+
async function runClient(values) {
|
|
17
|
+
const artifactPath = required(values, "artifact");
|
|
18
|
+
const origin = required(values, "origin");
|
|
19
|
+
const spendMarker = required(values, "spend-marker");
|
|
20
|
+
requireWebTransport(values);
|
|
21
|
+
const artifact = parseArtifact(readFileSync(artifactPath));
|
|
22
|
+
let session;
|
|
23
|
+
const lease = createArtifactLeaseV2(artifact, async () => claimSpendMarker(spendMarker));
|
|
24
|
+
try {
|
|
25
|
+
session = await connect(lease, {
|
|
26
|
+
origin,
|
|
27
|
+
...(values["certificate-hash"] === undefined ? {} : {
|
|
28
|
+
tls: { serverCertificateHash: decodeHash(values["certificate-hash"]) },
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
const stream = await session.openStream("cli");
|
|
32
|
+
await stream.write(new TextEncoder().encode("flowersec-ts-cli"));
|
|
33
|
+
await stream.closeWrite();
|
|
34
|
+
const response = await stream.read();
|
|
35
|
+
if (response === null || new TextDecoder().decode(response) !== "flowersec-ts-cli") {
|
|
36
|
+
throw new Error("CLI server returned an invalid stream response");
|
|
37
|
+
}
|
|
38
|
+
process.stdout.write("GREEN\n");
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
await session?.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function runServer(values) {
|
|
45
|
+
const artifact = unwrapArtifact(parseArtifact(readFileSync(required(values, "artifact"))));
|
|
46
|
+
requireWebTransport(values);
|
|
47
|
+
const server = await startNodeWebTransportServerV2({
|
|
48
|
+
host: values.host ?? "127.0.0.1",
|
|
49
|
+
port: Number.parseInt(required(values, "port"), 10),
|
|
50
|
+
path: values.path ?? "/flowersec/webtransport/v2/direct",
|
|
51
|
+
certificate: readFileSync(required(values, "certificate"), "utf8"),
|
|
52
|
+
privateKey: readFileSync(required(values, "private-key"), "utf8"),
|
|
53
|
+
carrierPath: artifact.path.kind,
|
|
54
|
+
inboundBidirectionalStreamCapacity: artifact.session.max_inbound_streams + 2,
|
|
55
|
+
});
|
|
56
|
+
process.stdout.write(JSON.stringify(server.address()) + "\n");
|
|
57
|
+
const abort = new AbortController();
|
|
58
|
+
const stop = () => abort.abort(new Error("CLI server stopped"));
|
|
59
|
+
process.once("SIGINT", stop);
|
|
60
|
+
process.once("SIGTERM", stop);
|
|
61
|
+
try {
|
|
62
|
+
const session = await acceptNativeSessionV2(await server.accept({ signal: abort.signal }), async (request) => {
|
|
63
|
+
const chosen = artifact.path.candidates.find(({ id }) => id === request.request.chosen_candidate_id);
|
|
64
|
+
if (request.request.pathKind !== artifact.path.kind || chosen?.carrier !== "webtransport") {
|
|
65
|
+
throw new Error("CLI server rejected an unexpected artifact candidate");
|
|
66
|
+
}
|
|
67
|
+
const expected = encodeFSB2RequestV2(buildFSB2RequestV2(artifact, request.request.chosen_candidate_id));
|
|
68
|
+
if (!bytesEqual(expected, request.raw))
|
|
69
|
+
throw new Error("CLI server rejected an unbound artifact");
|
|
70
|
+
return { accepted: true, artifact };
|
|
71
|
+
}, { runtime: nodeSessionRuntimeV2, signal: abort.signal });
|
|
72
|
+
await echoOnce(session);
|
|
73
|
+
await session.close();
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
process.removeListener("SIGINT", stop);
|
|
77
|
+
process.removeListener("SIGTERM", stop);
|
|
78
|
+
await server.close();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function echoOnce(session) {
|
|
82
|
+
const incoming = await session.acceptStream();
|
|
83
|
+
const payload = await incoming.stream.read();
|
|
84
|
+
if (payload === null)
|
|
85
|
+
throw new Error("CLI stream closed before payload");
|
|
86
|
+
await incoming.stream.write(payload);
|
|
87
|
+
await incoming.stream.closeWrite();
|
|
88
|
+
}
|
|
89
|
+
function parseArguments(raw) {
|
|
90
|
+
const [mode, ...rest] = raw;
|
|
91
|
+
if (mode !== "client" && mode !== "server")
|
|
92
|
+
throw new Error("usage: flowersec-ts-cli <client|server> options");
|
|
93
|
+
const values = {};
|
|
94
|
+
for (let index = 0; index < rest.length; index++) {
|
|
95
|
+
const token = rest[index];
|
|
96
|
+
if (!token.startsWith("--") || index + 1 >= rest.length || rest[index + 1].startsWith("--")) {
|
|
97
|
+
throw new Error(`invalid CLI option ${token}`);
|
|
98
|
+
}
|
|
99
|
+
values[token.slice(2)] = rest[++index];
|
|
100
|
+
}
|
|
101
|
+
return { mode, values };
|
|
102
|
+
}
|
|
103
|
+
function required(values, name) {
|
|
104
|
+
const value = values[name];
|
|
105
|
+
if (value === undefined || value === "")
|
|
106
|
+
throw new Error(`missing --${name}`);
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
function decodeHash(value) {
|
|
110
|
+
if (!/^[0-9a-fA-F]{64}$/.test(value))
|
|
111
|
+
throw new Error("--certificate-hash must be 32-byte SHA-256 hex");
|
|
112
|
+
return Uint8Array.from(Buffer.from(value, "hex"));
|
|
113
|
+
}
|
|
114
|
+
function requireWebTransport(values) {
|
|
115
|
+
if (values.transport !== "webtransport") {
|
|
116
|
+
throw new Error("--transport webtransport is required");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function bytesEqual(left, right) {
|
|
120
|
+
if (left.length !== right.length)
|
|
121
|
+
return false;
|
|
122
|
+
let different = 0;
|
|
123
|
+
for (let index = 0; index < left.length; index++)
|
|
124
|
+
different |= left[index] ^ right[index];
|
|
125
|
+
return different === 0;
|
|
126
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function claimSpendMarker(path: string): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { closeSync, fsyncSync, openSync } from "node:fs";
|
|
2
|
+
export function claimSpendMarker(path) {
|
|
3
|
+
let descriptor;
|
|
4
|
+
try {
|
|
5
|
+
descriptor = openSync(path, "wx", 0o600);
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
if (isErrnoException(error) && error.code === "EEXIST") {
|
|
9
|
+
throw new Error(`spend marker already exists: ${path}`, { cause: error });
|
|
10
|
+
}
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
fsyncSync(descriptor);
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
closeSync(descriptor);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function isErrnoException(error) {
|
|
21
|
+
return error instanceof Error && "code" in error;
|
|
22
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ArtifactLease } from "./public/artifactLease.js";
|
|
2
|
+
import type { RetryDisposition, Session, SessionErrorCode } from "./public/contract.js";
|
|
3
|
+
import type { ConnectErrorCode } from "./public/connectError.js";
|
|
4
|
+
export type { RetryDisposition } from "./public/contract.js";
|
|
5
|
+
export type ArtifactSourceResult = Readonly<{
|
|
6
|
+
kind: "lease";
|
|
7
|
+
lease: ArtifactLease;
|
|
8
|
+
}> | Readonly<{
|
|
9
|
+
kind: "failure";
|
|
10
|
+
code: string;
|
|
11
|
+
disposition: RetryDisposition;
|
|
12
|
+
}>;
|
|
13
|
+
export type ArtifactSource = Readonly<{
|
|
14
|
+
acquire(options: Readonly<{
|
|
15
|
+
signal: AbortSignal;
|
|
16
|
+
}>): Promise<ArtifactSourceResult>;
|
|
17
|
+
}>;
|
|
18
|
+
export type ConnectionState = "idle" | "connecting" | "connected" | "waiting" | "failed" | "closed";
|
|
19
|
+
export type ConnectionControllerFailure = Readonly<{
|
|
20
|
+
phase: "artifact";
|
|
21
|
+
code: string;
|
|
22
|
+
}> | Readonly<{
|
|
23
|
+
phase: "connect";
|
|
24
|
+
code: ConnectErrorCode;
|
|
25
|
+
}> | Readonly<{
|
|
26
|
+
phase: "session";
|
|
27
|
+
code: SessionErrorCode;
|
|
28
|
+
}>;
|
|
29
|
+
export type ConnectionSnapshot = Readonly<{
|
|
30
|
+
state: ConnectionState;
|
|
31
|
+
attempt: number;
|
|
32
|
+
currentSession?: Session;
|
|
33
|
+
failure?: ConnectionControllerFailure;
|
|
34
|
+
}>;
|
|
35
|
+
export type ConnectionControllerOptions = Readonly<{
|
|
36
|
+
maximumAttempts?: number;
|
|
37
|
+
}>;
|
|
38
|
+
export interface ConnectionController {
|
|
39
|
+
readonly state: ConnectionState;
|
|
40
|
+
readonly currentSession: Session | undefined;
|
|
41
|
+
readonly failure: ConnectionControllerFailure | undefined;
|
|
42
|
+
start(): void;
|
|
43
|
+
retryNow(): boolean;
|
|
44
|
+
waitForSession(options?: Readonly<{
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
}>): Promise<Session>;
|
|
47
|
+
subscribe(listener: (snapshot: ConnectionSnapshot) => void): () => void;
|
|
48
|
+
close(): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
export declare class ConnectionControllerError extends Error {
|
|
51
|
+
readonly code: "failed" | "closed" | "canceled";
|
|
52
|
+
readonly failure: ConnectionControllerFailure | undefined;
|
|
53
|
+
constructor(code: "failed" | "closed" | "canceled", failure?: ConnectionControllerFailure | undefined);
|
|
54
|
+
}
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { ConnectError } from "./public/connectError.js";
|
|
2
|
+
import { retryDispositionForConnectError, retryDispositionForSessionError, } from "./v2/retryDisposition.js";
|
|
3
|
+
import { SDK_DEFAULTS } from "./defaults.js";
|
|
4
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
5
|
+
export class ConnectionControllerError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
failure;
|
|
8
|
+
constructor(code, failure = undefined) {
|
|
9
|
+
super(`Flowersec connection controller stopped (code=${code})`);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.failure = failure;
|
|
12
|
+
this.name = "ConnectionControllerError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** @internal Runtime facades supply the only runtime-specific operation: one one-shot connection attempt. */
|
|
16
|
+
export function createConnectionControllerV2(source, connectOneShot, options = {}) {
|
|
17
|
+
return new ConnectionControllerV2Impl(source, connectOneShot, options);
|
|
18
|
+
}
|
|
19
|
+
class ConnectionControllerV2Impl {
|
|
20
|
+
source;
|
|
21
|
+
connectOneShot;
|
|
22
|
+
controllerState = "idle";
|
|
23
|
+
session;
|
|
24
|
+
lastFailure;
|
|
25
|
+
currentRetryDisposition;
|
|
26
|
+
attempt = 0;
|
|
27
|
+
consecutiveFailures = 0;
|
|
28
|
+
resetAttemptOnRetry = false;
|
|
29
|
+
lifetime = new AbortController();
|
|
30
|
+
listeners = new Set();
|
|
31
|
+
issuedLeases = new WeakSet();
|
|
32
|
+
issuedSessions = new WeakSet();
|
|
33
|
+
maxAttempts;
|
|
34
|
+
scheduler;
|
|
35
|
+
closeOperation;
|
|
36
|
+
pendingConnectCleanup;
|
|
37
|
+
pendingAcquisitionCleanup;
|
|
38
|
+
waitingWake;
|
|
39
|
+
manualRetryRequested = false;
|
|
40
|
+
retryNotBefore = 0;
|
|
41
|
+
constructor(source, connectOneShot, options) {
|
|
42
|
+
this.source = source;
|
|
43
|
+
this.connectOneShot = connectOneShot;
|
|
44
|
+
if (source === null || typeof source !== "object" || typeof source.acquire !== "function") {
|
|
45
|
+
throw new TypeError("artifact source must provide acquire()");
|
|
46
|
+
}
|
|
47
|
+
if (typeof connectOneShot !== "function")
|
|
48
|
+
throw new TypeError("one-shot connector is required");
|
|
49
|
+
if (options.maximumAttempts !== undefined &&
|
|
50
|
+
(!Number.isSafeInteger(options.maximumAttempts) || options.maximumAttempts < 1)) {
|
|
51
|
+
throw new TypeError("maximumAttempts must be a positive safe integer");
|
|
52
|
+
}
|
|
53
|
+
this.maxAttempts = options.maximumAttempts;
|
|
54
|
+
}
|
|
55
|
+
get state() { return this.controllerState; }
|
|
56
|
+
get currentSession() { return this.connectedSession(); }
|
|
57
|
+
get failure() { return this.lastFailure; }
|
|
58
|
+
start() {
|
|
59
|
+
if (this.controllerState !== "idle")
|
|
60
|
+
return;
|
|
61
|
+
this.scheduler = this.run();
|
|
62
|
+
}
|
|
63
|
+
retryNow() {
|
|
64
|
+
if (this.controllerState !== "waiting" || Date.now() < this.retryNotBefore)
|
|
65
|
+
return false;
|
|
66
|
+
this.manualRetryRequested = true;
|
|
67
|
+
this.waitingWake?.();
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
async waitForSession(options = {}) {
|
|
71
|
+
const ready = this.connectedSession();
|
|
72
|
+
if (ready !== undefined)
|
|
73
|
+
return ready;
|
|
74
|
+
if (this.controllerState === "failed")
|
|
75
|
+
throw new ConnectionControllerError("failed", this.lastFailure);
|
|
76
|
+
if (this.controllerState === "closed")
|
|
77
|
+
throw new ConnectionControllerError("closed", this.lastFailure);
|
|
78
|
+
if (options.signal?.aborted === true)
|
|
79
|
+
throw new ConnectionControllerError("canceled");
|
|
80
|
+
return await new Promise((resolve, reject) => {
|
|
81
|
+
const finish = (result) => {
|
|
82
|
+
this.listeners.delete(listener);
|
|
83
|
+
options.signal?.removeEventListener("abort", canceled);
|
|
84
|
+
if (result.session !== undefined)
|
|
85
|
+
resolve(result.session);
|
|
86
|
+
else
|
|
87
|
+
reject(result.error ?? new ConnectionControllerError("failed", this.lastFailure));
|
|
88
|
+
};
|
|
89
|
+
const listener = (snapshot) => {
|
|
90
|
+
if (snapshot.state === "connected" && snapshot.currentSession !== undefined) {
|
|
91
|
+
finish({ session: snapshot.currentSession });
|
|
92
|
+
}
|
|
93
|
+
else if (snapshot.state === "failed") {
|
|
94
|
+
finish({ error: new ConnectionControllerError("failed", snapshot.failure) });
|
|
95
|
+
}
|
|
96
|
+
else if (snapshot.state === "closed") {
|
|
97
|
+
finish({ error: new ConnectionControllerError("closed", snapshot.failure) });
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const canceled = () => finish({ error: new ConnectionControllerError("canceled") });
|
|
101
|
+
this.listeners.add(listener);
|
|
102
|
+
options.signal?.addEventListener("abort", canceled, { once: true });
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
subscribe(listener) {
|
|
106
|
+
if (typeof listener !== "function")
|
|
107
|
+
throw new TypeError("connection listener must be a function");
|
|
108
|
+
this.listeners.add(listener);
|
|
109
|
+
try {
|
|
110
|
+
listener(this.snapshot());
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
this.listeners.delete(listener);
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
return () => { this.listeners.delete(listener); };
|
|
117
|
+
}
|
|
118
|
+
close() {
|
|
119
|
+
if (this.closeOperation !== undefined)
|
|
120
|
+
return this.closeOperation;
|
|
121
|
+
let resolveClose;
|
|
122
|
+
this.closeOperation = new Promise((resolve) => {
|
|
123
|
+
resolveClose = resolve;
|
|
124
|
+
});
|
|
125
|
+
const active = this.session;
|
|
126
|
+
// A closed controller must never advertise a session that it has just retired.
|
|
127
|
+
this.session = undefined;
|
|
128
|
+
this.lifetime.abort(new ConnectionControllerError("closed", this.lastFailure));
|
|
129
|
+
this.transition("closed");
|
|
130
|
+
void this.finishClose(active).then(resolveClose);
|
|
131
|
+
return this.closeOperation;
|
|
132
|
+
}
|
|
133
|
+
async finishClose(active) {
|
|
134
|
+
this.waitingWake?.();
|
|
135
|
+
const activeClose = active === undefined
|
|
136
|
+
? Promise.resolve()
|
|
137
|
+
: active.close();
|
|
138
|
+
await Promise.allSettled([this.scheduler ?? Promise.resolve(), activeClose]);
|
|
139
|
+
const pendingCleanup = this.pendingConnectCleanup;
|
|
140
|
+
const acquisitionCleanup = this.pendingAcquisitionCleanup;
|
|
141
|
+
await Promise.allSettled([
|
|
142
|
+
...(pendingCleanup === undefined ? [] : [pendingCleanup]),
|
|
143
|
+
...(acquisitionCleanup === undefined ? [] : [acquisitionCleanup]),
|
|
144
|
+
]);
|
|
145
|
+
}
|
|
146
|
+
async run() {
|
|
147
|
+
let disposition;
|
|
148
|
+
while (!this.lifetime.signal.aborted) {
|
|
149
|
+
if (disposition !== undefined) {
|
|
150
|
+
const canContinue = await this.waitForRetry(disposition);
|
|
151
|
+
if (!canContinue)
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (this.maxAttempts !== undefined && this.attempt >= this.maxAttempts) {
|
|
155
|
+
this.currentRetryDisposition = Object.freeze({ kind: "terminal" });
|
|
156
|
+
this.transition("failed");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
this.attempt += 1;
|
|
160
|
+
this.transition("connecting");
|
|
161
|
+
const acquisition = await this.acquireLease();
|
|
162
|
+
if (this.lifetime.signal.aborted)
|
|
163
|
+
return;
|
|
164
|
+
if (acquisition.kind === "failure") {
|
|
165
|
+
this.lastFailure = Object.freeze({ phase: "artifact", code: acquisition.code });
|
|
166
|
+
disposition = acquisition.disposition;
|
|
167
|
+
this.currentRetryDisposition = disposition;
|
|
168
|
+
if (disposition.kind === "terminal") {
|
|
169
|
+
this.transition("failed");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.consecutiveFailures += 1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
let connected;
|
|
176
|
+
try {
|
|
177
|
+
connected = await this.connect(acquisition.lease);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (this.lifetime.signal.aborted)
|
|
181
|
+
return;
|
|
182
|
+
const failure = connectFailure(error);
|
|
183
|
+
this.lastFailure = failure.failure;
|
|
184
|
+
disposition = failure.disposition;
|
|
185
|
+
this.currentRetryDisposition = disposition;
|
|
186
|
+
if (disposition.kind === "terminal") {
|
|
187
|
+
this.transition("failed");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this.consecutiveFailures += 1;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (this.lifetime.signal.aborted) {
|
|
194
|
+
await connected.close().catch(() => undefined);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
this.session = connected;
|
|
198
|
+
this.lastFailure = undefined;
|
|
199
|
+
this.currentRetryDisposition = undefined;
|
|
200
|
+
this.consecutiveFailures = 0;
|
|
201
|
+
disposition = undefined;
|
|
202
|
+
this.transition("connected");
|
|
203
|
+
const termination = await raceTermination(connected, this.lifetime.signal);
|
|
204
|
+
if (termination === undefined || this.lifetime.signal.aborted)
|
|
205
|
+
return;
|
|
206
|
+
// A terminated session is no longer current while the replacement attempt waits.
|
|
207
|
+
this.session = undefined;
|
|
208
|
+
await connected.close().catch(() => undefined);
|
|
209
|
+
if (this.lifetime.signal.aborted)
|
|
210
|
+
return;
|
|
211
|
+
const failure = sessionFailure(termination.error);
|
|
212
|
+
this.lastFailure = failure.failure;
|
|
213
|
+
disposition = failure.disposition;
|
|
214
|
+
this.currentRetryDisposition = disposition;
|
|
215
|
+
if (disposition.kind === "terminal") {
|
|
216
|
+
this.transition("failed");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this.consecutiveFailures = 1;
|
|
220
|
+
this.resetAttemptOnRetry = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async acquireLease() {
|
|
224
|
+
let result;
|
|
225
|
+
let pending;
|
|
226
|
+
try {
|
|
227
|
+
pending = this.source.acquire({ signal: this.lifetime.signal });
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return terminalArtifactFailure("artifact_source_failed");
|
|
231
|
+
}
|
|
232
|
+
const cleanup = pending.then(() => undefined, () => undefined);
|
|
233
|
+
this.pendingAcquisitionCleanup = cleanup;
|
|
234
|
+
void cleanup.finally(() => {
|
|
235
|
+
if (this.pendingAcquisitionCleanup === cleanup)
|
|
236
|
+
this.pendingAcquisitionCleanup = undefined;
|
|
237
|
+
}).catch(() => undefined);
|
|
238
|
+
try {
|
|
239
|
+
result = await raceAbort(pending, this.lifetime.signal);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return terminalArtifactFailure("artifact_source_failed");
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
if (result === null || typeof result !== "object")
|
|
246
|
+
return terminalArtifactFailure("artifact_source_failed");
|
|
247
|
+
if (result.kind === "failure") {
|
|
248
|
+
if (!validFailureCode(result.code) || !validDisposition(result.disposition)) {
|
|
249
|
+
return terminalArtifactFailure("artifact_source_failed");
|
|
250
|
+
}
|
|
251
|
+
return Object.freeze({ kind: "failure", code: result.code, disposition: normalizeDisposition(result.disposition) });
|
|
252
|
+
}
|
|
253
|
+
if (result.kind !== "lease" || result.lease === null || typeof result.lease !== "object") {
|
|
254
|
+
return terminalArtifactFailure("artifact_source_failed");
|
|
255
|
+
}
|
|
256
|
+
if (this.issuedLeases.has(result.lease))
|
|
257
|
+
return terminalArtifactFailure("reused_artifact_lease");
|
|
258
|
+
this.issuedLeases.add(result.lease);
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return terminalArtifactFailure("artifact_source_failed");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async connect(lease) {
|
|
266
|
+
const pending = this.connectOneShot(lease, this.lifetime.signal);
|
|
267
|
+
try {
|
|
268
|
+
const session = await raceAbort(pending, this.lifetime.signal);
|
|
269
|
+
if (session === null || typeof session !== "object" ||
|
|
270
|
+
typeof session.close !== "function" || typeof session.waitTermination !== "function") {
|
|
271
|
+
throw new TypeError("one-shot connector returned an invalid session");
|
|
272
|
+
}
|
|
273
|
+
if (this.issuedSessions.has(session)) {
|
|
274
|
+
await session.close().catch(() => undefined);
|
|
275
|
+
throw new Error("one-shot connector reused a previous session");
|
|
276
|
+
}
|
|
277
|
+
this.issuedSessions.add(session);
|
|
278
|
+
return session;
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (this.lifetime.signal.aborted) {
|
|
282
|
+
const cleanup = pending.then(async (lateSession) => await lateSession.close(), () => undefined);
|
|
283
|
+
this.pendingConnectCleanup = cleanup;
|
|
284
|
+
void cleanup.finally(() => {
|
|
285
|
+
if (this.pendingConnectCleanup === cleanup)
|
|
286
|
+
this.pendingConnectCleanup = undefined;
|
|
287
|
+
}).catch(() => undefined);
|
|
288
|
+
}
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async waitForRetry(disposition) {
|
|
293
|
+
if (this.maxAttempts !== undefined && !this.resetAttemptOnRetry && this.attempt >= this.maxAttempts) {
|
|
294
|
+
this.currentRetryDisposition = Object.freeze({ kind: "terminal" });
|
|
295
|
+
this.transition("failed");
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
const now = Date.now();
|
|
299
|
+
const backoffDeadline = now + retryDelay(this.consecutiveFailures);
|
|
300
|
+
const hardDeadline = disposition.kind === "retry_after"
|
|
301
|
+
? disposition.notBeforeUnixMilliseconds
|
|
302
|
+
: 0;
|
|
303
|
+
this.retryNotBefore = hardDeadline;
|
|
304
|
+
this.manualRetryRequested = false;
|
|
305
|
+
this.transition("waiting");
|
|
306
|
+
while (!this.lifetime.signal.aborted) {
|
|
307
|
+
const deadline = this.manualRetryRequested
|
|
308
|
+
? hardDeadline
|
|
309
|
+
: Math.max(backoffDeadline, hardDeadline);
|
|
310
|
+
const remaining = deadline - Date.now();
|
|
311
|
+
if (remaining <= 0) {
|
|
312
|
+
this.waitingWake = undefined;
|
|
313
|
+
this.retryNotBefore = 0;
|
|
314
|
+
if (this.resetAttemptOnRetry) {
|
|
315
|
+
this.attempt = 0;
|
|
316
|
+
this.resetAttemptOnRetry = false;
|
|
317
|
+
}
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
await this.wait(Math.min(remaining, MAX_TIMER_DELAY_MS));
|
|
321
|
+
}
|
|
322
|
+
this.waitingWake = undefined;
|
|
323
|
+
this.retryNotBefore = 0;
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
async wait(milliseconds) {
|
|
327
|
+
await new Promise((resolve) => {
|
|
328
|
+
let settled = false;
|
|
329
|
+
const finish = () => {
|
|
330
|
+
if (settled)
|
|
331
|
+
return;
|
|
332
|
+
settled = true;
|
|
333
|
+
clearTimeout(timer);
|
|
334
|
+
this.lifetime.signal.removeEventListener("abort", finish);
|
|
335
|
+
if (this.waitingWake === finish)
|
|
336
|
+
this.waitingWake = undefined;
|
|
337
|
+
resolve();
|
|
338
|
+
};
|
|
339
|
+
const timer = setTimeout(finish, milliseconds);
|
|
340
|
+
this.waitingWake = finish;
|
|
341
|
+
this.lifetime.signal.addEventListener("abort", finish, { once: true });
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
connectedSession() {
|
|
345
|
+
return this.controllerState === "connected" ? this.session : undefined;
|
|
346
|
+
}
|
|
347
|
+
transition(state) {
|
|
348
|
+
this.controllerState = state;
|
|
349
|
+
const snapshot = this.snapshot();
|
|
350
|
+
for (const listener of this.listeners) {
|
|
351
|
+
try {
|
|
352
|
+
listener(snapshot);
|
|
353
|
+
}
|
|
354
|
+
catch { /* Listener failures do not own controller lifecycle. */ }
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
snapshot() {
|
|
358
|
+
const currentSession = this.connectedSession();
|
|
359
|
+
return Object.freeze({
|
|
360
|
+
state: this.controllerState,
|
|
361
|
+
attempt: this.attempt,
|
|
362
|
+
...(currentSession === undefined ? {} : { currentSession }),
|
|
363
|
+
...(this.lastFailure === undefined ? {} : { failure: this.lastFailure }),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function connectFailure(error) {
|
|
368
|
+
if (!(error instanceof ConnectError)) {
|
|
369
|
+
return Object.freeze({
|
|
370
|
+
failure: Object.freeze({ phase: "connect", code: "connection_failed" }),
|
|
371
|
+
disposition: Object.freeze({ kind: "terminal" }),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
const disposition = retryDispositionForConnectError(error);
|
|
375
|
+
return Object.freeze({
|
|
376
|
+
failure: Object.freeze({ phase: "connect", code: error.code }),
|
|
377
|
+
disposition,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
function sessionFailure(error) {
|
|
381
|
+
const disposition = retryDispositionForSessionError(error);
|
|
382
|
+
return Object.freeze({
|
|
383
|
+
failure: Object.freeze({ phase: "session", code: error.code }),
|
|
384
|
+
disposition,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
function terminalArtifactFailure(code) {
|
|
388
|
+
return Object.freeze({
|
|
389
|
+
kind: "failure",
|
|
390
|
+
code,
|
|
391
|
+
disposition: Object.freeze({ kind: "terminal" }),
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function validFailureCode(code) {
|
|
395
|
+
return typeof code === "string" && /^[a-z][a-z0-9_]{0,63}$/.test(code);
|
|
396
|
+
}
|
|
397
|
+
function validDisposition(value) {
|
|
398
|
+
if (value === null || typeof value !== "object" || !("kind" in value))
|
|
399
|
+
return false;
|
|
400
|
+
const disposition = value;
|
|
401
|
+
if (disposition.kind === "terminal" || disposition.kind === "retryable")
|
|
402
|
+
return true;
|
|
403
|
+
return disposition.kind === "retry_after" &&
|
|
404
|
+
typeof disposition.notBeforeUnixMilliseconds === "number" &&
|
|
405
|
+
Number.isSafeInteger(disposition.notBeforeUnixMilliseconds) &&
|
|
406
|
+
disposition.notBeforeUnixMilliseconds >= 0;
|
|
407
|
+
}
|
|
408
|
+
function normalizeDisposition(disposition) {
|
|
409
|
+
if (disposition.kind === "retry_after") {
|
|
410
|
+
return Object.freeze({
|
|
411
|
+
kind: "retry_after",
|
|
412
|
+
notBeforeUnixMilliseconds: disposition.notBeforeUnixMilliseconds,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return Object.freeze({ kind: disposition.kind });
|
|
416
|
+
}
|
|
417
|
+
function retryDelay(failures) {
|
|
418
|
+
const exponent = Math.max(0, Math.min(30, failures - 1));
|
|
419
|
+
return Math.min(SDK_DEFAULTS.connectionController.maxDelayMs, SDK_DEFAULTS.connectionController.initialDelayMs *
|
|
420
|
+
(SDK_DEFAULTS.connectionController.factor ** exponent));
|
|
421
|
+
}
|
|
422
|
+
async function raceAbort(promise, signal) {
|
|
423
|
+
if (signal.aborted)
|
|
424
|
+
throw signal.reason;
|
|
425
|
+
return await new Promise((resolve, reject) => {
|
|
426
|
+
const abort = () => reject(signal.reason);
|
|
427
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
428
|
+
void promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
async function raceTermination(session, signal) {
|
|
432
|
+
try {
|
|
433
|
+
return await raceAbort(session.waitTermination(), signal);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CandidateAttemptFactoryV2 } from "../sessionConnector.js";
|
|
2
|
+
import { type WebSocketLike } from "../../ws-client/binaryTransport.js";
|
|
3
|
+
type WebSocketLikeWithProtocol = WebSocketLike & Readonly<{
|
|
4
|
+
protocol?: string;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function createWebSocketCandidateFactoryV2(factory: (url: string, subprotocol: string) => WebSocketLikeWithProtocol): CandidateAttemptFactoryV2;
|
|
7
|
+
export {};
|