@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.
Files changed (59) hide show
  1. package/README.md +15 -8
  2. package/dist/{chunk-1ts7pswj.js → chunk-0d8vs5q8.js} +33 -2
  3. package/dist/{chunk-h8fwx0k7.js → chunk-18gcp0ck.js} +32 -7
  4. package/dist/{chunk-gm4swndx.js → chunk-43mcyrh4.js} +14 -24
  5. package/dist/{chunk-zp1s833x.js → chunk-7hbvjzb9.js} +25 -12
  6. package/dist/{chunk-f3vq900j.js → chunk-92aacbwh.js} +1 -1
  7. package/dist/{chunk-35cjec9z.js → chunk-ccre3nh1.js} +6 -6
  8. package/dist/chunk-ehmx0yhp.js +52 -0
  9. package/dist/{chunk-t4hrnaj0.js → chunk-ex429qjt.js} +22 -20
  10. package/dist/{chunk-fqrknvr8.js → chunk-kap6ka3n.js} +27 -12
  11. package/dist/{chunk-m4d29vfd.js → chunk-kxpecgy7.js} +109 -36
  12. package/dist/{chunk-nftzckdy.js → chunk-mvkxs8d0.js} +17 -2
  13. package/dist/{chunk-f98qrpx1.js → chunk-njs48b51.js} +28 -27
  14. package/dist/chunk-sb5qzv37.js +62 -0
  15. package/dist/{chunk-dx6yvj81.js → chunk-xhfbark7.js} +1 -1
  16. package/dist/{chunk-m391jmpw.js → chunk-ze1hng62.js} +9 -9
  17. package/dist/connection/acquire.d.ts +3 -4
  18. package/dist/connection/acquire.js +2 -4
  19. package/dist/datasource.d.ts +0 -6
  20. package/dist/file/client.js +2 -2
  21. package/dist/file/index.js +4 -4
  22. package/dist/file/local.js +3 -3
  23. package/dist/file/pod.js +3 -3
  24. package/dist/kubernetes/client.js +8 -8
  25. package/dist/kubernetes/environment.js +8 -8
  26. package/dist/kubernetes/exec-transport.js +2 -2
  27. package/dist/kubernetes/pod-log-client.js +2 -2
  28. package/dist/kubernetes/pod-log-datasource.js +3 -3
  29. package/dist/kubernetes/port-forward.d.ts +4 -0
  30. package/dist/kubernetes/port-forward.js +1 -1
  31. package/dist/kubernetes/resource-transport.js +2 -2
  32. package/dist/kubernetes/service-port-forward.d.ts +3 -2
  33. package/dist/kubernetes/service-port-forward.js +2 -2
  34. package/dist/mysql/client.d.ts +7 -7
  35. package/dist/mysql/client.js +12 -7
  36. package/dist/mysql/index.js +18 -13
  37. package/dist/opensearch/client.d.ts +14 -4
  38. package/dist/opensearch/client.js +15 -8
  39. package/dist/opensearch/index.js +15 -8
  40. package/dist/redis/client.d.ts +7 -6
  41. package/dist/redis/client.js +12 -7
  42. package/dist/redis/index.js +16 -11
  43. package/dist/s3/client.d.ts +5 -3
  44. package/dist/s3/client.js +14 -1
  45. package/dist/s3/datasource.js +13 -7
  46. package/dist/s3/index.js +13 -7
  47. package/dist/transport/index.d.ts +18 -8
  48. package/dist/transport/index.js +17 -4
  49. package/dist/transport/lifecycle.d.ts +22 -0
  50. package/dist/transport/lifecycle.js +8 -0
  51. package/dist/transport/pod-relay.d.ts +7 -4
  52. package/dist/transport/pod-relay.js +6 -3
  53. package/dist/transport/service-port-forward.d.ts +30 -0
  54. package/dist/transport/service-port-forward.js +15 -0
  55. package/package.json +3 -2
  56. package/dist/{chunk-mnad4zkp.js → chunk-010339ga.js} +3 -3
  57. package/dist/{chunk-x0yhfg29.js → chunk-5w85c7cs.js} +3 -3
  58. package/dist/{chunk-ww11q58v.js → chunk-rncagjbc.js} +3 -3
  59. 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 transports through `ConnectionSource`. Only mapping or handshake network
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. A `ConnectionSource` resolves connection settings
198
- and available `Transport` paths. Clients execute protocol operations through those paths.
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
- resolve: async () => target,
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
- // The same instance belongs in each protocol client's ConnectionSource.transports.
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-nftzckdy.js";
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
- async connect(endpoint) {
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
- return this.forward(endpoint);
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
- async function awaitWithSignal(pending, signal, cancel) {
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
- result = await awaitWithSignal(prepared, attemptSignal);
59
+ options.onPending?.(prepared);
60
+ result = await raceSignal(prepared, attemptSignal);
79
61
  } catch (failure) {
80
- await discard();
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
- await discard();
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 { awaitWithSignal, acquireConnection };
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
- source;
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(source, limits, lifecycle = {}) {
48
- this.source = source;
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.source.resolve();
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 transport = this.source.transports[0];
69
- if (!transport || transport.kind !== "tcp")
70
- throw new Error("S3 requires a TCP transport");
71
- const route = await transport.connect({
72
- host: url.hostname,
73
- port: Number(url.port || (url.protocol === "https:" ? 443 : 80)),
74
- servername: url.hostname
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();
@@ -2,7 +2,7 @@ import {
2
2
  BoundedFileClient,
3
3
  fileClientOptions,
4
4
  filePaths
5
- } from "./chunk-ww11q58v.js";
5
+ } from "./chunk-rncagjbc.js";
6
6
  import {
7
7
  FileError
8
8
  } from "./chunk-rkt41za2.js";
@@ -3,10 +3,13 @@ import {
3
3
  } from "./chunk-z5pm2emz.js";
4
4
  import {
5
5
  S3Client
6
- } from "./chunk-zp1s833x.js";
6
+ } from "./chunk-7hbvjzb9.js";
7
7
  import {
8
8
  DirectTransport
9
- } from "./chunk-h8fwx0k7.js";
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
- acquireConnection,
15
- awaitWithSignal
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
- isConnectionNetworkError
25
- } from "./chunk-h8fwx0k7.js";
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
- constructor(transports, options, connect = connectMysql) {
45
- this.transports = transports;
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 acquireConnection(this.transports, {
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
- source;
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(source, options) {
187
- this.source = source;
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 awaitWithSignal(this.source.resolve(), this.signal);
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.source.transports, { ...this.options, signal: this.signal });
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(source, options) {
219
- const client = new MysqlClient(source, options);
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-nftzckdy.js";
6
+ } from "./chunk-mvkxs8d0.js";
4
7
  import {
5
8
  KubectlExecutor
6
9
  } from "./chunk-rq5fc007.js";
7
10
  import {
8
- POD_RELAY_SCRIPT
9
- } from "./chunk-ssj93brx.js";
11
+ Transport
12
+ } from "./chunk-sb5qzv37.js";
10
13
 
11
14
  // src/transport/pod-relay.ts
12
- class PodRelayTransport {
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 = options.signal ? AbortSignal.any([options.signal, this.#controller.signal]) : this.#controller.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.then((local) => ({ ...local, servername: endpoint.servername ?? endpoint.host }));
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
- dispose() {
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
  }