@compforge/harness-toolbox 0.4.7 → 0.5.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 +15 -8
- package/dist/{chunk-1ts7pswj.js → chunk-0d8vs5q8.js} +33 -2
- package/dist/{chunk-h8fwx0k7.js → chunk-18gcp0ck.js} +32 -7
- package/dist/{chunk-gm4swndx.js → chunk-43mcyrh4.js} +14 -24
- package/dist/{chunk-zp1s833x.js → chunk-7hbvjzb9.js} +25 -12
- package/dist/{chunk-f3vq900j.js → chunk-92aacbwh.js} +1 -1
- package/dist/{chunk-35cjec9z.js → chunk-ccre3nh1.js} +6 -6
- package/dist/chunk-ehmx0yhp.js +52 -0
- package/dist/{chunk-t4hrnaj0.js → chunk-ex429qjt.js} +22 -20
- package/dist/{chunk-fqrknvr8.js → chunk-kap6ka3n.js} +27 -12
- package/dist/{chunk-m4d29vfd.js → chunk-kxpecgy7.js} +109 -36
- package/dist/{chunk-nftzckdy.js → chunk-mvkxs8d0.js} +17 -2
- package/dist/{chunk-f98qrpx1.js → chunk-njs48b51.js} +28 -27
- package/dist/chunk-sb5qzv37.js +62 -0
- package/dist/{chunk-dx6yvj81.js → chunk-xhfbark7.js} +1 -1
- package/dist/{chunk-m391jmpw.js → chunk-ze1hng62.js} +9 -9
- package/dist/connection/acquire.d.ts +3 -4
- package/dist/connection/acquire.js +2 -4
- package/dist/datasource.d.ts +0 -6
- package/dist/file/client.js +2 -2
- package/dist/file/index.js +4 -4
- package/dist/file/local.js +3 -3
- package/dist/file/pod.js +3 -3
- package/dist/kubernetes/client.js +8 -8
- package/dist/kubernetes/environment.js +8 -8
- package/dist/kubernetes/exec-transport.js +2 -2
- package/dist/kubernetes/pod-log-client.js +2 -2
- package/dist/kubernetes/pod-log-datasource.js +3 -3
- package/dist/kubernetes/port-forward.d.ts +4 -0
- package/dist/kubernetes/port-forward.js +1 -1
- package/dist/kubernetes/resource-transport.js +2 -2
- package/dist/kubernetes/service-port-forward.d.ts +3 -2
- package/dist/kubernetes/service-port-forward.js +2 -2
- package/dist/mysql/client.d.ts +7 -7
- package/dist/mysql/client.js +12 -7
- package/dist/mysql/index.js +18 -13
- package/dist/opensearch/client.d.ts +14 -4
- package/dist/opensearch/client.js +15 -8
- package/dist/opensearch/index.js +15 -8
- package/dist/redis/client.d.ts +7 -6
- package/dist/redis/client.js +12 -7
- package/dist/redis/index.js +16 -11
- package/dist/s3/client.d.ts +5 -3
- package/dist/s3/client.js +14 -1
- package/dist/s3/datasource.js +13 -7
- package/dist/s3/index.js +13 -7
- package/dist/transport/index.d.ts +18 -8
- package/dist/transport/index.js +17 -4
- package/dist/transport/lifecycle.d.ts +22 -0
- package/dist/transport/lifecycle.js +8 -0
- package/dist/transport/pod-relay.d.ts +7 -4
- package/dist/transport/pod-relay.js +6 -3
- package/dist/transport/service-port-forward.d.ts +30 -0
- package/dist/transport/service-port-forward.js +15 -0
- package/package.json +3 -2
- package/dist/{chunk-mnad4zkp.js → chunk-010339ga.js} +3 -3
- package/dist/{chunk-x0yhfg29.js → chunk-5w85c7cs.js} +3 -3
- package/dist/{chunk-ww11q58v.js → chunk-rncagjbc.js} +3 -3
- package/dist/{chunk-r3rnsvwv.js → chunk-xeft7af0.js} +3 -3
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ select `single`, `cluster` or `sentinel`; `endpoints` contains that mode's seed
|
|
|
69
69
|
configuration additionally provides `sentinelMasterName` and optional Sentinel credentials,
|
|
70
70
|
separate from data-node credentials. Cluster connections require database zero.
|
|
71
71
|
|
|
72
|
-
Supply ordered TCP
|
|
72
|
+
Supply one `Transport`, with ordered TCP paths where needed. Only mapping or handshake network
|
|
73
73
|
failures advance to the next transport; authentication and TLS verification failures stop immediately.
|
|
74
74
|
`connectTimeoutMs` bounds each route (mapping and handshake); it defaults to the command
|
|
75
75
|
`timeoutMs`. `acquireTimeoutMs` bounds all candidates together and defaults to the per-route budget
|
|
@@ -194,8 +194,17 @@ The Makefile builds common before typechecking or building toolbox.
|
|
|
194
194
|
A `ClientProvider` identifies a reusable client and constructs it; `DataSource` adds data-access semantics.
|
|
195
195
|
Accessible environments implement ClientProvider directly without becoming data sources. The `Client` owns initialization
|
|
196
196
|
and idempotent disposal; a `ClientManager` joins concurrent initialization for the same key and
|
|
197
|
-
closes owned clients when the caller finishes.
|
|
198
|
-
and
|
|
197
|
+
closes owned clients when the caller finishes. Protocol clients take configuration (or an async resolver)
|
|
198
|
+
and one `Transport`; the transport selects and prepares access paths. `clients.create(factory)` creates
|
|
199
|
+
a fresh client that its caller may close early; transports acquired with `get` stay alive until root disposal.
|
|
200
|
+
|
|
201
|
+
For shared port-forwarding, register a `ServicePortForwardTransport` once per cluster/namespace, then pass the
|
|
202
|
+
borrowed transport to each protocol client. Closing an OpenSearch client leaves the forward available to the next
|
|
203
|
+
OpenSearch, MySQL, Redis or S3 client; root disposal closes the clients before stopping the forward processes.
|
|
204
|
+
Borrower cancellation only stops that borrower's wait. Exited forwards are recreated on the next acquisition.
|
|
205
|
+
Transport mapping changes the dial address, while protocol clients retain the original Host, TLS and signing identity.
|
|
206
|
+
In 0.5, replace `new Client({ resolve, transports }, options)` with
|
|
207
|
+
`new Client(resolve, transport, options)` (S3 retains its explicit limits argument).
|
|
199
208
|
|
|
200
209
|
```ts
|
|
201
210
|
import { ClientManager, clientKey, type DataSource } from "@compforge/harness-toolbox";
|
|
@@ -205,10 +214,8 @@ import { DirectTransport } from "@compforge/harness-toolbox/transport";
|
|
|
205
214
|
const target = { host: "localhost", port: 3306, database: "app", user: "reader", password: "..." };
|
|
206
215
|
const source: DataSource<MysqlClient> = {
|
|
207
216
|
clientKey: clientKey("mysql", target),
|
|
208
|
-
createClient: (_clients, signal) => new MysqlClient(
|
|
209
|
-
|
|
210
|
-
transports: [new DirectTransport()],
|
|
211
|
-
}, { signal, connectTimeoutMs: 5_000, queryTimeoutMs: 10_000 }),
|
|
217
|
+
createClient: (_clients, signal) => new MysqlClient(target, new DirectTransport(),
|
|
218
|
+
{ signal, connectTimeoutMs: 5_000, queryTimeoutMs: 10_000 }),
|
|
212
219
|
};
|
|
213
220
|
const clients = new ClientManager();
|
|
214
221
|
try {
|
|
@@ -351,7 +358,7 @@ try {
|
|
|
351
358
|
maxConnections: 16, maxTargets: 8, maxCandidatePods: 8, signal,
|
|
352
359
|
}),
|
|
353
360
|
});
|
|
354
|
-
//
|
|
361
|
+
// Pass this same transport instance to each protocol client; clients never dispose it.
|
|
355
362
|
const address = await relay.connect({host: 'database.internal', port: 3306});
|
|
356
363
|
// Connect the native protocol client to address; do not dispose relay in a Service.
|
|
357
364
|
} finally {
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "./chunk-6fnhzhdb.js";
|
|
9
9
|
import {
|
|
10
10
|
PortForwardScope
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-mvkxs8d0.js";
|
|
12
12
|
|
|
13
13
|
// src/kubernetes/service-port-forward.ts
|
|
14
14
|
class ServicePortForwarder {
|
|
@@ -17,6 +17,8 @@ class ServicePortForwarder {
|
|
|
17
17
|
pods;
|
|
18
18
|
#cache = new Map;
|
|
19
19
|
#scope;
|
|
20
|
+
#handles = new Map;
|
|
21
|
+
#closed = false;
|
|
20
22
|
constructor(opts, services, pods, starter) {
|
|
21
23
|
this.opts = opts;
|
|
22
24
|
this.services = services;
|
|
@@ -31,14 +33,28 @@ class ServicePortForwarder {
|
|
|
31
33
|
return this.#scope.active;
|
|
32
34
|
}
|
|
33
35
|
forward(endpoint) {
|
|
36
|
+
if (this.#closed)
|
|
37
|
+
throw new Error("Port-forward transport is closed");
|
|
34
38
|
this.opts.signal?.throwIfAborted();
|
|
35
39
|
const key = `${endpoint.host}:${endpoint.port}`;
|
|
36
40
|
let pending = this.#cache.get(key);
|
|
37
41
|
if (!pending) {
|
|
38
42
|
pending = this.#forward(endpoint);
|
|
39
43
|
this.#cache.set(key, pending);
|
|
44
|
+
pending.catch(() => {
|
|
45
|
+
if (this.#cache.get(key) === pending)
|
|
46
|
+
this.#cache.delete(key);
|
|
47
|
+
});
|
|
40
48
|
}
|
|
41
|
-
return pending
|
|
49
|
+
return pending.then((mapped) => {
|
|
50
|
+
const handle = this.#handles.get(key);
|
|
51
|
+
if (handle?.isAlive && !handle.isAlive()) {
|
|
52
|
+
if (this.#cache.get(key) === pending)
|
|
53
|
+
this.#cache.delete(key);
|
|
54
|
+
return this.forward(endpoint);
|
|
55
|
+
}
|
|
56
|
+
return mapped;
|
|
57
|
+
});
|
|
42
58
|
}
|
|
43
59
|
async forwardServiceTargets(endpoint) {
|
|
44
60
|
const service = findService(this.services, endpoint, this.opts.namespace);
|
|
@@ -75,11 +91,26 @@ class ServicePortForwarder {
|
|
|
75
91
|
if (!result.value) {
|
|
76
92
|
throw new Error(`为 ${target.kind}/${target.name} 建立 port-forward 失败:${result.reason ?? "unknown error"}`);
|
|
77
93
|
}
|
|
94
|
+
if (this.#closed || this.opts.signal?.aborted) {
|
|
95
|
+
result.value.stop();
|
|
96
|
+
this.opts.signal?.throwIfAborted();
|
|
97
|
+
throw new Error("Port-forward transport is closed");
|
|
98
|
+
}
|
|
99
|
+
this.#handles.set(`${endpoint.host}:${endpoint.port}`, result.value);
|
|
78
100
|
return { host: "127.0.0.1", port: result.value.localPort, servername: endpoint.host };
|
|
79
101
|
}
|
|
80
102
|
stop() {
|
|
103
|
+
this.#closed = true;
|
|
81
104
|
this.#scope.stop();
|
|
82
105
|
this.#cache.clear();
|
|
106
|
+
this.#handles.clear();
|
|
107
|
+
}
|
|
108
|
+
async dispose() {
|
|
109
|
+
this.#closed = true;
|
|
110
|
+
await Promise.allSettled(this.#cache.values());
|
|
111
|
+
await this.#scope.dispose();
|
|
112
|
+
this.#cache.clear();
|
|
113
|
+
this.#handles.clear();
|
|
83
114
|
}
|
|
84
115
|
}
|
|
85
116
|
|
|
@@ -1,34 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Transport
|
|
3
|
+
} from "./chunk-sb5qzv37.js";
|
|
4
|
+
|
|
1
5
|
// src/transport/index.ts
|
|
2
|
-
class DirectTransport {
|
|
6
|
+
class DirectTransport extends Transport {
|
|
3
7
|
kind = "tcp";
|
|
4
8
|
name = "direct";
|
|
5
|
-
|
|
9
|
+
paths() {
|
|
10
|
+
return [this];
|
|
11
|
+
}
|
|
12
|
+
async connect(endpoint, attempt) {
|
|
13
|
+
this.signal.throwIfAborted();
|
|
14
|
+
attempt?.signal.throwIfAborted();
|
|
6
15
|
return endpoint;
|
|
7
16
|
}
|
|
8
17
|
}
|
|
9
18
|
|
|
10
|
-
class PortForwardTransport {
|
|
19
|
+
class PortForwardTransport extends Transport {
|
|
11
20
|
forward;
|
|
21
|
+
release;
|
|
12
22
|
kind = "tcp";
|
|
13
23
|
name = "port-forward";
|
|
14
|
-
constructor(forward) {
|
|
24
|
+
constructor(forward, release, signal) {
|
|
25
|
+
super([], signal);
|
|
15
26
|
this.forward = forward;
|
|
27
|
+
this.release = release;
|
|
28
|
+
}
|
|
29
|
+
paths() {
|
|
30
|
+
return [this];
|
|
16
31
|
}
|
|
17
|
-
connect(endpoint) {
|
|
18
|
-
|
|
32
|
+
connect(endpoint, attempt) {
|
|
33
|
+
this.signal.throwIfAborted();
|
|
34
|
+
attempt?.signal.throwIfAborted();
|
|
35
|
+
return this.forward(endpoint, attempt);
|
|
36
|
+
}
|
|
37
|
+
async close() {
|
|
38
|
+
await this.release?.();
|
|
19
39
|
}
|
|
20
40
|
}
|
|
21
41
|
|
|
22
|
-
class PodPythonTransport {
|
|
42
|
+
class PodPythonTransport extends Transport {
|
|
23
43
|
exec;
|
|
24
44
|
python;
|
|
25
45
|
kind = "python";
|
|
26
46
|
name = "pod-python";
|
|
27
47
|
constructor(exec, python = "python") {
|
|
48
|
+
super();
|
|
28
49
|
this.exec = exec;
|
|
29
50
|
this.python = python;
|
|
30
51
|
}
|
|
52
|
+
paths() {
|
|
53
|
+
return [this];
|
|
54
|
+
}
|
|
31
55
|
run(script, input, timeoutMs) {
|
|
56
|
+
this.signal.throwIfAborted();
|
|
32
57
|
return this.exec([this.python, "-c", script], { stdin: JSON.stringify(input), timeoutMs });
|
|
33
58
|
}
|
|
34
59
|
}
|
|
@@ -4,26 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-rkt41za2.js";
|
|
5
5
|
|
|
6
6
|
// src/connection/acquire.ts
|
|
7
|
-
|
|
8
|
-
if (signal.aborted) {
|
|
9
|
-
cancel?.();
|
|
10
|
-
pending.catch(() => {});
|
|
11
|
-
signal.throwIfAborted();
|
|
12
|
-
}
|
|
13
|
-
let abort;
|
|
14
|
-
const cancelled = new Promise((_, reject) => {
|
|
15
|
-
abort = () => {
|
|
16
|
-
cancel?.();
|
|
17
|
-
reject(signal.reason);
|
|
18
|
-
};
|
|
19
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
20
|
-
});
|
|
21
|
-
try {
|
|
22
|
-
return await Promise.race([pending, cancelled]);
|
|
23
|
-
} finally {
|
|
24
|
-
signal.removeEventListener("abort", abort);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
7
|
+
import { raceSignal } from "race-signal";
|
|
27
8
|
async function acquireConnection(routes, options) {
|
|
28
9
|
const budget = options.acquireTimeoutMs ?? options.connectTimeoutMs * routes.length;
|
|
29
10
|
if (!routes.length)
|
|
@@ -75,9 +56,14 @@ async function acquireConnection(routes, options) {
|
|
|
75
56
|
}
|
|
76
57
|
return value;
|
|
77
58
|
});
|
|
78
|
-
|
|
59
|
+
options.onPending?.(prepared);
|
|
60
|
+
result = await raceSignal(prepared, attemptSignal);
|
|
79
61
|
} catch (failure) {
|
|
80
|
-
|
|
62
|
+
try {
|
|
63
|
+
await discard();
|
|
64
|
+
} catch (cleanup) {
|
|
65
|
+
throw new AggregateError([failure, cleanup], "Connection acquisition and cleanup failed", { cause: failure });
|
|
66
|
+
}
|
|
81
67
|
signal.throwIfAborted();
|
|
82
68
|
const error = attemptSignal.aborted ? attemptSignal.reason : failure;
|
|
83
69
|
reason = options.retryReason(error);
|
|
@@ -93,7 +79,11 @@ async function acquireConnection(routes, options) {
|
|
|
93
79
|
attemptSignal.throwIfAborted();
|
|
94
80
|
return result.connection;
|
|
95
81
|
} catch (error) {
|
|
96
|
-
|
|
82
|
+
try {
|
|
83
|
+
await discard();
|
|
84
|
+
} catch (cleanup) {
|
|
85
|
+
throw new AggregateError([error, cleanup], "Connection publication and cleanup failed", { cause: error });
|
|
86
|
+
}
|
|
97
87
|
throw error;
|
|
98
88
|
}
|
|
99
89
|
}
|
|
@@ -103,4 +93,4 @@ async function acquireConnection(routes, options) {
|
|
|
103
93
|
}
|
|
104
94
|
}
|
|
105
95
|
|
|
106
|
-
export {
|
|
96
|
+
export { acquireConnection };
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isConnectionNetworkError
|
|
3
|
+
} from "./chunk-18gcp0ck.js";
|
|
1
4
|
import {
|
|
2
5
|
ConcurrencyPool
|
|
3
6
|
} from "./chunk-v3hxvnrc.js";
|
|
@@ -33,7 +36,8 @@ function metadata(value) {
|
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
class S3Client {
|
|
36
|
-
|
|
39
|
+
configuration;
|
|
40
|
+
transport;
|
|
37
41
|
limits;
|
|
38
42
|
lifecycle;
|
|
39
43
|
#controller = new AbortController;
|
|
@@ -44,8 +48,9 @@ class S3Client {
|
|
|
44
48
|
#disposal;
|
|
45
49
|
#sdk;
|
|
46
50
|
#http;
|
|
47
|
-
constructor(
|
|
48
|
-
this.
|
|
51
|
+
constructor(configuration, transport, limits, lifecycle = {}) {
|
|
52
|
+
this.configuration = configuration;
|
|
53
|
+
this.transport = transport;
|
|
49
54
|
this.limits = limits;
|
|
50
55
|
this.lifecycle = lifecycle;
|
|
51
56
|
positive(limits.concurrency, "concurrency");
|
|
@@ -59,19 +64,28 @@ class S3Client {
|
|
|
59
64
|
return this.#initialization ??= this.#initialize();
|
|
60
65
|
}
|
|
61
66
|
async#initialize() {
|
|
62
|
-
const target = await this.
|
|
67
|
+
const target = await (typeof this.configuration === "function" ? this.configuration() : Promise.resolve(this.configuration));
|
|
63
68
|
this.signal.throwIfAborted();
|
|
64
69
|
const url = new URL(target.endpoint);
|
|
65
70
|
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
|
|
66
71
|
throw new Error("S3 endpoint must be an HTTP(S) URL without embedded credentials, query or fragment");
|
|
67
72
|
}
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
const route = await this.transport.acquire({
|
|
74
|
+
name: "S3 route",
|
|
75
|
+
signal: this.signal,
|
|
76
|
+
connectTimeoutMs: this.limits.connectTimeoutMs,
|
|
77
|
+
onRoute: this.lifecycle.onRoute,
|
|
78
|
+
open: async (path, attempt) => {
|
|
79
|
+
if (path.kind !== "tcp")
|
|
80
|
+
throw new Error("S3 requires a TCP transport");
|
|
81
|
+
const connection = await path.connect({
|
|
82
|
+
host: url.hostname,
|
|
83
|
+
port: Number(url.port || (url.protocol === "https:" ? 443 : 80))
|
|
84
|
+
}, attempt);
|
|
85
|
+
return { connection, transport: path.name };
|
|
86
|
+
},
|
|
87
|
+
close: () => {},
|
|
88
|
+
retryReason: (error) => isConnectionNetworkError(error) ? String(error.code) : undefined
|
|
75
89
|
});
|
|
76
90
|
this.signal.throwIfAborted();
|
|
77
91
|
this.#http = s3Handler(target, route, this.limits);
|
|
@@ -85,7 +99,6 @@ class S3Client {
|
|
|
85
99
|
followRegionRedirects: false,
|
|
86
100
|
responseChecksumValidation: "WHEN_REQUIRED"
|
|
87
101
|
});
|
|
88
|
-
this.lifecycle.onRoute?.({ transport: transport.name });
|
|
89
102
|
}
|
|
90
103
|
#run(options, operation) {
|
|
91
104
|
this.signal.throwIfAborted();
|
|
@@ -3,10 +3,13 @@ import {
|
|
|
3
3
|
} from "./chunk-z5pm2emz.js";
|
|
4
4
|
import {
|
|
5
5
|
S3Client
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-7hbvjzb9.js";
|
|
7
7
|
import {
|
|
8
8
|
DirectTransport
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-18gcp0ck.js";
|
|
10
|
+
import {
|
|
11
|
+
Transport
|
|
12
|
+
} from "./chunk-sb5qzv37.js";
|
|
10
13
|
|
|
11
14
|
// src/s3/datasource.ts
|
|
12
15
|
class S3DataSource {
|
|
@@ -25,10 +28,7 @@ class S3DataSource {
|
|
|
25
28
|
});
|
|
26
29
|
}
|
|
27
30
|
createClient(_clients, signal) {
|
|
28
|
-
return new S3Client({
|
|
29
|
-
resolve: async () => this.#target,
|
|
30
|
-
transports: [this.#transport]
|
|
31
|
-
}, this.#limits, { signal });
|
|
31
|
+
return new S3Client(this.#target, this.#transport instanceof Transport ? this.#transport : new Transport([this.#transport]), this.#limits, { signal });
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ServicePortForwarder
|
|
3
|
+
} from "./chunk-0d8vs5q8.js";
|
|
4
|
+
import {
|
|
5
|
+
KubectlExecutor
|
|
6
|
+
} from "./chunk-rq5fc007.js";
|
|
7
|
+
import {
|
|
8
|
+
Transport
|
|
9
|
+
} from "./chunk-sb5qzv37.js";
|
|
10
|
+
|
|
11
|
+
// src/transport/service-port-forward.ts
|
|
12
|
+
import { raceSignal } from "race-signal";
|
|
13
|
+
class ServicePortForwardTransport extends Transport {
|
|
14
|
+
options;
|
|
15
|
+
dependencies;
|
|
16
|
+
kind = "tcp";
|
|
17
|
+
name = "port-forward";
|
|
18
|
+
#initialization;
|
|
19
|
+
#forwarder;
|
|
20
|
+
constructor(options, dependencies) {
|
|
21
|
+
super([], options.signal);
|
|
22
|
+
this.options = options;
|
|
23
|
+
this.dependencies = dependencies;
|
|
24
|
+
}
|
|
25
|
+
paths() {
|
|
26
|
+
return [this];
|
|
27
|
+
}
|
|
28
|
+
initialize() {
|
|
29
|
+
this.signal.throwIfAborted();
|
|
30
|
+
const pending = this.#initialization ??= (async () => {
|
|
31
|
+
this.#forwarder = await ServicePortForwarder.create(this.dependencies?.executor ?? new KubectlExecutor(this.options), { ...this.options, signal: this.resourceSignal }, this.dependencies?.startForward);
|
|
32
|
+
this.signal.throwIfAborted();
|
|
33
|
+
})();
|
|
34
|
+
return raceSignal(pending, this.signal);
|
|
35
|
+
}
|
|
36
|
+
get activeForwards() {
|
|
37
|
+
return this.#forwarder?.activeForwards ?? [];
|
|
38
|
+
}
|
|
39
|
+
async connect(endpoint, attempt) {
|
|
40
|
+
this.signal.throwIfAborted();
|
|
41
|
+
const signal = attempt ? AbortSignal.any([this.signal, attempt.signal]) : this.signal;
|
|
42
|
+
await raceSignal(this.initialize(), signal);
|
|
43
|
+
const mapped = await raceSignal(this.#forwarder.forward(endpoint), signal);
|
|
44
|
+
return { host: mapped.host, port: mapped.port };
|
|
45
|
+
}
|
|
46
|
+
async close() {
|
|
47
|
+
await this.#initialization?.catch(() => {});
|
|
48
|
+
await this.#forwarder?.dispose();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { ServicePortForwardTransport };
|
|
@@ -11,20 +11,20 @@ import {
|
|
|
11
11
|
destroyMysqlConnection
|
|
12
12
|
} from "./chunk-835fssgz.js";
|
|
13
13
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} from "./chunk-gm4swndx.js";
|
|
17
|
-
import {
|
|
18
|
-
ToolboxError
|
|
19
|
-
} from "./chunk-rkt41za2.js";
|
|
14
|
+
isConnectionNetworkError
|
|
15
|
+
} from "./chunk-18gcp0ck.js";
|
|
20
16
|
import {
|
|
21
17
|
ConcurrencyPool
|
|
22
18
|
} from "./chunk-v3hxvnrc.js";
|
|
23
19
|
import {
|
|
24
|
-
|
|
25
|
-
} from "./chunk-
|
|
20
|
+
Transport
|
|
21
|
+
} from "./chunk-sb5qzv37.js";
|
|
22
|
+
import {
|
|
23
|
+
ToolboxError
|
|
24
|
+
} from "./chunk-rkt41za2.js";
|
|
26
25
|
|
|
27
26
|
// src/mysql/client.ts
|
|
27
|
+
import { raceSignal } from "race-signal";
|
|
28
28
|
function mysqlOperationError(error, phase) {
|
|
29
29
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "PROTOCOL_CONNECTION_LOST")
|
|
30
30
|
return error;
|
|
@@ -36,15 +36,15 @@ function mysqlOperationError(error, phase) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
class MysqlDatabase {
|
|
39
|
-
transports;
|
|
40
39
|
options;
|
|
41
40
|
connect;
|
|
42
41
|
#queries = new ConcurrencyPool(1);
|
|
43
42
|
#connections = new Map;
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
#transport;
|
|
44
|
+
constructor(transport, options, connect = connectMysql) {
|
|
46
45
|
this.options = options;
|
|
47
46
|
this.connect = connect;
|
|
47
|
+
this.#transport = transport instanceof Transport ? transport : new Transport(transport);
|
|
48
48
|
}
|
|
49
49
|
query(target, sql, values) {
|
|
50
50
|
return this.#queries.run(() => this.#query(target, sql, values), this.options.signal);
|
|
@@ -125,14 +125,14 @@ class MysqlDatabase {
|
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
127
|
#open(target) {
|
|
128
|
-
return
|
|
128
|
+
return this.#transport.acquire({
|
|
129
129
|
name: "MySQL connection",
|
|
130
130
|
...this.options,
|
|
131
131
|
open: async (transport, attempt) => {
|
|
132
132
|
if (transport.kind === "python")
|
|
133
133
|
return { connection: { kind: "python", transport }, transport: transport.name };
|
|
134
134
|
try {
|
|
135
|
-
const endpoint = await transport.connect(target);
|
|
135
|
+
const endpoint = await transport.connect(target, attempt);
|
|
136
136
|
const connection = await this.connect({
|
|
137
137
|
host: endpoint.host,
|
|
138
138
|
port: endpoint.port,
|
|
@@ -175,7 +175,8 @@ class MysqlDatabase {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
class MysqlClient {
|
|
178
|
-
|
|
178
|
+
configuration;
|
|
179
|
+
transport;
|
|
179
180
|
options;
|
|
180
181
|
#controller = new AbortController;
|
|
181
182
|
signal;
|
|
@@ -183,17 +184,18 @@ class MysqlClient {
|
|
|
183
184
|
#disposal;
|
|
184
185
|
#database;
|
|
185
186
|
#target;
|
|
186
|
-
constructor(
|
|
187
|
-
this.
|
|
187
|
+
constructor(configuration, transport, options) {
|
|
188
|
+
this.configuration = configuration;
|
|
189
|
+
this.transport = transport;
|
|
188
190
|
this.options = options;
|
|
189
191
|
this.signal = options.signal ? AbortSignal.any([options.signal, this.#controller.signal]) : this.#controller.signal;
|
|
190
192
|
}
|
|
191
193
|
initialize() {
|
|
192
194
|
this.signal.throwIfAborted();
|
|
193
195
|
return this.#initialization ??= (async () => {
|
|
194
|
-
this.#target = await
|
|
196
|
+
this.#target = await raceSignal(typeof this.configuration === "function" ? this.configuration() : Promise.resolve(this.configuration), this.signal);
|
|
195
197
|
this.signal.throwIfAborted();
|
|
196
|
-
this.#database = new MysqlDatabase(this.
|
|
198
|
+
this.#database = new MysqlDatabase(this.transport, { ...this.options, signal: this.signal });
|
|
197
199
|
await this.#database.initialize(this.#target);
|
|
198
200
|
})();
|
|
199
201
|
}
|
|
@@ -215,8 +217,8 @@ class MysqlClient {
|
|
|
215
217
|
})();
|
|
216
218
|
}
|
|
217
219
|
}
|
|
218
|
-
async function openMysql(
|
|
219
|
-
const client = new MysqlClient(
|
|
220
|
+
async function openMysql(configuration, transport, options) {
|
|
221
|
+
const client = new MysqlClient(configuration, transport, options);
|
|
220
222
|
try {
|
|
221
223
|
await client.initialize();
|
|
222
224
|
} catch (error) {
|
|
@@ -1,25 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
POD_RELAY_SCRIPT
|
|
3
|
+
} from "./chunk-ssj93brx.js";
|
|
1
4
|
import {
|
|
2
5
|
startPortForward
|
|
3
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-mvkxs8d0.js";
|
|
4
7
|
import {
|
|
5
8
|
KubectlExecutor
|
|
6
9
|
} from "./chunk-rq5fc007.js";
|
|
7
10
|
import {
|
|
8
|
-
|
|
9
|
-
} from "./chunk-
|
|
11
|
+
Transport
|
|
12
|
+
} from "./chunk-sb5qzv37.js";
|
|
10
13
|
|
|
11
14
|
// src/transport/pod-relay.ts
|
|
12
|
-
|
|
15
|
+
import { raceSignal } from "race-signal";
|
|
16
|
+
class PodRelayTransport extends Transport {
|
|
13
17
|
options;
|
|
14
18
|
kind = "tcp";
|
|
15
19
|
name = "pod-relay";
|
|
16
|
-
#controller = new AbortController;
|
|
17
20
|
#signal;
|
|
18
21
|
#executor;
|
|
19
22
|
#startForward;
|
|
20
23
|
#targets = new Map;
|
|
21
24
|
#pending = new Map;
|
|
22
25
|
#forwards = [];
|
|
26
|
+
#exits = [];
|
|
23
27
|
#startup;
|
|
24
28
|
#execution;
|
|
25
29
|
#input;
|
|
@@ -28,6 +32,7 @@ class PodRelayTransport {
|
|
|
28
32
|
#disposal;
|
|
29
33
|
#sequence = 0;
|
|
30
34
|
constructor(options, dependencies) {
|
|
35
|
+
super([], options.signal);
|
|
31
36
|
this.options = options;
|
|
32
37
|
for (const key of ["startupTimeoutMs", "connectTimeoutMs", "maxConnections", "maxTargets", "maxCandidatePods"]) {
|
|
33
38
|
if (!Number.isSafeInteger(options[key]) || options[key] <= 0)
|
|
@@ -35,10 +40,14 @@ class PodRelayTransport {
|
|
|
35
40
|
}
|
|
36
41
|
this.#executor = dependencies?.executor ?? new KubectlExecutor(options);
|
|
37
42
|
this.#startForward = dependencies?.startForward ?? startPortForward;
|
|
38
|
-
this.#signal =
|
|
43
|
+
this.#signal = this.resourceSignal;
|
|
39
44
|
this.#signal.addEventListener("abort", this.#onAbort, { once: true });
|
|
40
45
|
}
|
|
46
|
+
paths() {
|
|
47
|
+
return [this];
|
|
48
|
+
}
|
|
41
49
|
async initialize() {
|
|
50
|
+
this.signal.throwIfAborted();
|
|
42
51
|
this.#check();
|
|
43
52
|
}
|
|
44
53
|
#check() {
|
|
@@ -175,7 +184,8 @@ class PodRelayTransport {
|
|
|
175
184
|
this.#input.enqueue(new TextEncoder().encode(`${JSON.stringify(value)}
|
|
176
185
|
`));
|
|
177
186
|
}
|
|
178
|
-
connect(endpoint) {
|
|
187
|
+
connect(endpoint, attempt) {
|
|
188
|
+
this.signal.throwIfAborted();
|
|
179
189
|
this.#check();
|
|
180
190
|
if (!endpoint.host || !Number.isInteger(endpoint.port) || endpoint.port < 1 || endpoint.port > 65535) {
|
|
181
191
|
return Promise.reject(new Error("Invalid relay endpoint"));
|
|
@@ -192,7 +202,7 @@ class PodRelayTransport {
|
|
|
192
202
|
pending = this.#connect(endpoint, this.#startup);
|
|
193
203
|
this.#targets.set(key, pending);
|
|
194
204
|
}
|
|
195
|
-
return pending
|
|
205
|
+
return raceSignal(pending, attempt ? AbortSignal.any([this.signal, attempt.signal]) : this.signal);
|
|
196
206
|
}
|
|
197
207
|
async#connect(endpoint, startup) {
|
|
198
208
|
const pod = await startup;
|
|
@@ -225,10 +235,13 @@ class PodRelayTransport {
|
|
|
225
235
|
...this.options,
|
|
226
236
|
target: { kind: "pod", name: pod.target.pod },
|
|
227
237
|
remotePort,
|
|
228
|
-
timeoutMs: this.options.startupTimeoutMs
|
|
238
|
+
timeoutMs: this.options.startupTimeoutMs,
|
|
239
|
+
signal: this.#signal
|
|
229
240
|
});
|
|
230
241
|
if (!forwarded.ok || !forwarded.value)
|
|
231
242
|
throw new Error("Pod relay port-forward unavailable");
|
|
243
|
+
if (forwarded.value.exited)
|
|
244
|
+
this.#exits.push(forwarded.value.exited);
|
|
232
245
|
try {
|
|
233
246
|
this.#check();
|
|
234
247
|
} catch (error) {
|
|
@@ -238,14 +251,16 @@ class PodRelayTransport {
|
|
|
238
251
|
this.#forwards.push(forwarded.value);
|
|
239
252
|
return { host: "127.0.0.1", port: forwarded.value.localPort };
|
|
240
253
|
}
|
|
241
|
-
|
|
254
|
+
close() {
|
|
242
255
|
return this.#disposal ??= (async () => {
|
|
243
|
-
this.#controller.abort(new Error("Pod relay disposed"));
|
|
244
256
|
this.#fail(new Error("Pod relay disposed"));
|
|
245
257
|
await Promise.allSettled(this.#targets.values());
|
|
246
|
-
await this.#execution;
|
|
258
|
+
const completed = await Promise.allSettled([this.#execution, ...this.#exits]);
|
|
247
259
|
this.#signal.removeEventListener("abort", this.#onAbort);
|
|
248
260
|
this.#targets.clear();
|
|
261
|
+
const errors = completed.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
262
|
+
if (errors.length)
|
|
263
|
+
throw new AggregateError(errors, "Pod relay cleanup failed");
|
|
249
264
|
})();
|
|
250
265
|
}
|
|
251
266
|
}
|