@compforge/harness-toolbox 0.4.8 → 0.5.1

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 (70) hide show
  1. package/README.md +41 -9
  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-35cjec9z.js → chunk-3b877a0y.js} +6 -6
  5. package/dist/{chunk-gm4swndx.js → chunk-43mcyrh4.js} +14 -24
  6. package/dist/chunk-56fev9np.js +142 -0
  7. package/dist/{chunk-zp1s833x.js → chunk-7m95c7pn.js} +26 -13
  8. package/dist/{chunk-f3vq900j.js → chunk-92aacbwh.js} +1 -1
  9. package/dist/{chunk-x4ntb898.js → chunk-c2b2jwm4.js} +18 -18
  10. package/dist/chunk-ehmx0yhp.js +52 -0
  11. package/dist/{chunk-t4hrnaj0.js → chunk-ex429qjt.js} +22 -20
  12. package/dist/{chunk-m391jmpw.js → chunk-hbhbragw.js} +9 -9
  13. package/dist/{chunk-fqrknvr8.js → chunk-kap6ka3n.js} +27 -12
  14. package/dist/{chunk-vymm4g9h.js → chunk-kxpecgy7.js} +106 -37
  15. package/dist/{chunk-nftzckdy.js → chunk-mvkxs8d0.js} +17 -2
  16. package/dist/{chunk-f98qrpx1.js → chunk-njs48b51.js} +28 -27
  17. package/dist/chunk-sb5qzv37.js +62 -0
  18. package/dist/{chunk-dx6yvj81.js → chunk-xhfbark7.js} +1 -1
  19. package/dist/chunk-z8e1ckkq.js +16 -0
  20. package/dist/connection/acquire.d.ts +3 -4
  21. package/dist/connection/acquire.js +2 -4
  22. package/dist/datasource.d.ts +0 -6
  23. package/dist/file/client.js +2 -2
  24. package/dist/file/index.js +4 -4
  25. package/dist/file/local.js +3 -3
  26. package/dist/file/pod.js +3 -3
  27. package/dist/http/client.d.ts +15 -0
  28. package/dist/http/client.js +21 -0
  29. package/dist/http/handler.d.ts +10 -0
  30. package/dist/http/handler.js +6 -0
  31. package/dist/http/index.d.ts +2 -0
  32. package/dist/http/index.js +21 -0
  33. package/dist/http/types.d.ts +34 -0
  34. package/dist/http/types.js +0 -0
  35. package/dist/kubernetes/client.js +9 -9
  36. package/dist/kubernetes/environment.js +9 -9
  37. package/dist/kubernetes/exec-transport.js +2 -2
  38. package/dist/kubernetes/pod-log-datasource.js +1 -1
  39. package/dist/kubernetes/port-forward.d.ts +4 -0
  40. package/dist/kubernetes/port-forward.js +1 -1
  41. package/dist/kubernetes/resource-transport.js +2 -2
  42. package/dist/kubernetes/service-port-forward.d.ts +3 -2
  43. package/dist/kubernetes/service-port-forward.js +2 -2
  44. package/dist/mysql/client.d.ts +7 -7
  45. package/dist/mysql/client.js +12 -7
  46. package/dist/mysql/index.js +16 -11
  47. package/dist/opensearch/client.d.ts +14 -4
  48. package/dist/opensearch/client.js +15 -8
  49. package/dist/opensearch/index.js +15 -8
  50. package/dist/redis/client.d.ts +7 -6
  51. package/dist/redis/client.js +12 -7
  52. package/dist/redis/index.js +16 -11
  53. package/dist/s3/client.d.ts +5 -3
  54. package/dist/s3/client.js +16 -2
  55. package/dist/s3/datasource.js +15 -8
  56. package/dist/s3/handler.d.ts +2 -3
  57. package/dist/s3/handler.js +2 -1
  58. package/dist/s3/index.js +15 -8
  59. package/dist/transport/index.d.ts +18 -8
  60. package/dist/transport/index.js +17 -4
  61. package/dist/transport/lifecycle.d.ts +22 -0
  62. package/dist/transport/lifecycle.js +8 -0
  63. package/dist/transport/pod-relay.d.ts +7 -4
  64. package/dist/transport/pod-relay.js +6 -3
  65. package/dist/transport/service-port-forward.d.ts +30 -0
  66. package/dist/transport/service-port-forward.js +15 -0
  67. package/package.json +9 -3
  68. package/dist/{chunk-x0yhfg29.js → chunk-5w85c7cs.js} +3 -3
  69. package/dist/{chunk-ww11q58v.js → chunk-rncagjbc.js} +3 -3
  70. package/dist/{chunk-r3rnsvwv.js → chunk-xeft7af0.js} +3 -3
@@ -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,21 +1,21 @@
1
+ import {
2
+ KubernetesResourceTransport
3
+ } from "./chunk-5w85c7cs.js";
4
+ import {
5
+ KubernetesExecTransport
6
+ } from "./chunk-xeft7af0.js";
1
7
  import {
2
8
  resolveWorkload
3
9
  } from "./chunk-ae84rzq5.js";
4
10
  import {
5
11
  ServicePortForwarder
6
- } from "./chunk-1ts7pswj.js";
7
- import {
8
- KubernetesResourceTransport
9
- } from "./chunk-x0yhfg29.js";
12
+ } from "./chunk-0d8vs5q8.js";
10
13
  import {
11
- KubernetesExecTransport
12
- } from "./chunk-r3rnsvwv.js";
14
+ KubectlExecutor
15
+ } from "./chunk-rq5fc007.js";
13
16
  import {
14
17
  KubernetesError
15
18
  } from "./chunk-rkt41za2.js";
16
- import {
17
- KubectlExecutor
18
- } from "./chunk-rq5fc007.js";
19
19
 
20
20
  // src/kubernetes/client.ts
21
21
  class KubernetesClient {
@@ -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
  }
@@ -1,17 +1,21 @@
1
1
  import {
2
- acquireConnection,
3
- awaitWithSignal
4
- } from "./chunk-gm4swndx.js";
2
+ isConnectionNetworkError
3
+ } from "./chunk-18gcp0ck.js";
5
4
  import {
6
5
  TimeoutError,
7
6
  ToolboxError
8
7
  } from "./chunk-rkt41za2.js";
9
- import {
10
- isConnectionNetworkError
11
- } from "./chunk-h8fwx0k7.js";
12
8
 
13
9
  // src/opensearch/client.ts
10
+ import { raceSignal } from "race-signal";
14
11
  import { Client, Connection, errors } from "@opensearch-project/opensearch";
12
+ import { isIP } from "node:net";
13
+ function openSearchServername(host) {
14
+ if (!host)
15
+ return;
16
+ const unwrapped = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
17
+ return isIP(unwrapped) ? undefined : host;
18
+ }
15
19
  function describeOpenSearchError(error) {
16
20
  const parts = [];
17
21
  const seen = new Set;
@@ -36,16 +40,30 @@ function isOpenSearchReadApi(search) {
36
40
  }
37
41
 
38
42
  class OpenSearchConnection extends Connection {
43
+ request(params, callback) {
44
+ let completed = false;
45
+ try {
46
+ return super.request(params, (error, response) => {
47
+ completed = true;
48
+ callback(error, response);
49
+ });
50
+ } catch (error) {
51
+ if (!completed)
52
+ this._openRequests--;
53
+ throw error;
54
+ }
55
+ }
39
56
  buildRequestObject(params) {
40
- return { ...super.buildRequestObject(params), servername: this.ssl?.servername };
57
+ return { ...super.buildRequestObject(params), servername: openSearchServername(this.ssl?.servername) };
41
58
  }
42
59
  }
43
60
  function createClient(options) {
44
61
  return new Client({
45
62
  node: options.node,
46
63
  Connection: OpenSearchConnection,
64
+ headers: options.authority ? { host: options.authority } : undefined,
47
65
  auth: options.auth.username && options.auth.password ? { username: options.auth.username, password: options.auth.password } : undefined,
48
- ssl: { rejectUnauthorized: false, servername: options.servername },
66
+ ssl: { rejectUnauthorized: false, servername: openSearchServername(options.servername) },
49
67
  requestTimeout: options.requestTimeoutMs ?? 60000,
50
68
  maxRetries: 0,
51
69
  agent: { keepAlive: !process.versions.bun }
@@ -55,26 +73,36 @@ function createClient(options) {
55
73
  class OpenSearchEngine {
56
74
  options;
57
75
  client;
76
+ #requests = new Set;
77
+ #closed = false;
58
78
  constructor(options, client) {
59
79
  this.options = options;
60
80
  this.client = client ?? createClient(options);
61
81
  }
82
+ #run(start) {
83
+ if (this.#closed)
84
+ throw new Error("OpenSearch engine is closed");
85
+ const request = start();
86
+ this.#requests.add(request);
87
+ request.then(() => this.#requests.delete(request), () => this.#requests.delete(request));
88
+ return request;
89
+ }
62
90
  async count(index, query) {
63
- const response = await this.client.count({ index, body: { query } });
91
+ const response = await this.#run(() => this.client.count({ index, body: { query } }));
64
92
  return Number(response.body.count ?? 0);
65
93
  }
66
94
  async search(index, body) {
67
- const response = await this.client.search({ index, body });
95
+ const response = await this.#run(() => this.client.search({ index, body }));
68
96
  return response.body;
69
97
  }
70
98
  async request(path, query) {
71
99
  if (!this.client.transport)
72
100
  throw new Error("OpenSearch client 不支持通用只读请求");
73
- const response = await this.client.transport.request({
101
+ const response = await this.#run(() => this.client.transport.request({
74
102
  method: "GET",
75
103
  path,
76
104
  querystring: query
77
- });
105
+ }));
78
106
  return response.body;
79
107
  }
80
108
  async ping(signal, timeoutMs = this.options.requestTimeoutMs ?? 60000) {
@@ -87,9 +115,10 @@ class OpenSearchEngine {
87
115
  const controller = new AbortController;
88
116
  const timer = setTimeout(() => controller.abort(new TimeoutError(`OpenSearch probe timed out after ${timeoutMs}ms`)), timeoutMs);
89
117
  const active = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
118
+ let request;
90
119
  try {
91
- const request = this.client.ping({}, { requestTimeout: timeoutMs });
92
- const result = await awaitWithSignal(request, active, () => request.abort?.());
120
+ request = this.#run(() => this.client.ping({}, { requestTimeout: timeoutMs }));
121
+ const result = await raceSignal(request, active);
93
122
  log(`OpenSearch probe ok target=${target} elapsed_ms=${Date.now() - started}`);
94
123
  return result;
95
124
  } catch (error) {
@@ -97,13 +126,18 @@ class OpenSearchEngine {
97
126
  throw error;
98
127
  } finally {
99
128
  clearTimeout(timer);
129
+ if (active.aborted)
130
+ request?.abort?.();
100
131
  }
101
132
  }
102
133
  async close() {
134
+ this.#closed = true;
135
+ for (const request of this.#requests)
136
+ request.abort?.();
103
137
  const controller = new AbortController;
104
138
  const timer = setTimeout(() => controller.abort(new TimeoutError("OpenSearch close timed out after 2000ms")), 2000);
105
139
  try {
106
- await awaitWithSignal(this.client.close(), controller.signal);
140
+ await raceSignal(this.client.close(), controller.signal);
107
141
  } catch (error) {
108
142
  this.options.onDiagnostic?.(`OpenSearch close failed ${describeOpenSearchError(error)}`);
109
143
  throw error;
@@ -123,27 +157,39 @@ function openSearchRetryReason(error) {
123
157
  }
124
158
 
125
159
  class OpenSearchClient {
126
- source;
160
+ configuration;
161
+ transport;
127
162
  lifecycle;
128
163
  #controller = new AbortController;
129
164
  signal;
130
165
  #initialization;
131
166
  #disposal;
132
167
  #engine;
168
+ #node;
169
+ get node() {
170
+ return this.#node;
171
+ }
172
+ #cleanupErrors = [];
133
173
  #operations = new Set;
134
- constructor(source, lifecycle = {}) {
135
- this.source = source;
174
+ constructor(configuration, transport, lifecycle = {}) {
175
+ this.configuration = configuration;
176
+ this.transport = transport;
136
177
  this.lifecycle = lifecycle;
137
178
  this.signal = lifecycle.signal ? AbortSignal.any([lifecycle.signal, this.#controller.signal]) : this.#controller.signal;
138
179
  }
139
180
  initialize() {
140
181
  this.signal.throwIfAborted();
141
182
  return this.#initialization ??= (async () => {
142
- const target = await awaitWithSignal(this.source.resolve(), this.signal);
183
+ const target = await raceSignal(typeof this.configuration === "function" ? this.configuration() : Promise.resolve(this.configuration), this.signal);
143
184
  this.signal.throwIfAborted();
144
- const original = new URL(target.node);
185
+ const explicit = target.node.includes("://");
186
+ const preferred = target.preferredScheme ?? "http";
187
+ const original = new URL(explicit ? target.node : `${preferred}://${target.node}`);
188
+ if (!explicit && !original.port)
189
+ original.port = "9200";
190
+ const schemes = explicit ? [original.protocol] : preferred === "https" ? ["https:", "http:"] : ["http:", "https:"];
145
191
  try {
146
- this.#engine = await acquireConnection(this.source.transports, {
192
+ this.#engine = await this.transport.acquire({
147
193
  name: "OpenSearch connection",
148
194
  signal: this.signal,
149
195
  connectTimeoutMs: target.connectTimeoutMs ?? target.requestTimeoutMs ?? 1e4,
@@ -155,23 +201,40 @@ class OpenSearchClient {
155
201
  this.lifecycle.onDiagnostic?.(`OpenSearch route start transport=${transport.name} target=${original.protocol}//${original.host} timeout_ms=${attempt.remainingMs()}`);
156
202
  const endpoint = await transport.connect({
157
203
  host: original.hostname,
158
- port: Number(original.port || (original.protocol === "https:" ? 443 : 80)),
159
- servername: target.servername ?? original.hostname
160
- });
204
+ port: Number(original.port || (original.protocol === "https:" ? 443 : 80))
205
+ }, attempt);
161
206
  attempt.signal.throwIfAborted();
162
207
  const url = new URL(original);
163
208
  url.hostname = endpoint.host;
164
209
  url.port = String(endpoint.port);
165
- const engine = new OpenSearchEngine({ ...target, onDiagnostic: this.lifecycle.onDiagnostic, node: url.toString(), servername: endpoint.servername ?? target.servername ?? original.hostname });
166
- try {
167
- await engine.ping(attempt.signal, attempt.remainingMs());
168
- return { connection: engine, transport: transport.name };
169
- } catch (error) {
210
+ for (const [index, scheme] of schemes.entries()) {
211
+ url.protocol = scheme;
212
+ const engine = new OpenSearchEngine({
213
+ ...target,
214
+ onDiagnostic: this.lifecycle.onDiagnostic,
215
+ node: url.toString(),
216
+ authority: original.host,
217
+ servername: target.servername ?? original.hostname
218
+ });
170
219
  try {
171
- await engine.close();
172
- } catch {}
173
- throw error;
220
+ await engine.ping(attempt.signal, attempt.remainingMs());
221
+ const selected = new URL(original);
222
+ selected.protocol = scheme;
223
+ this.#node = selected.toString().replace(/\/$/, "");
224
+ return { connection: engine, transport: transport.name };
225
+ } catch (error) {
226
+ try {
227
+ await engine.close();
228
+ } catch (cleanup) {
229
+ this.#cleanupErrors.push(cleanup);
230
+ throw new AggregateError([error, cleanup], "OpenSearch initialization and cleanup failed", { cause: error });
231
+ }
232
+ attempt.signal.throwIfAborted();
233
+ if (index === schemes.length - 1 || openSearchRetryReason(error) === undefined)
234
+ throw error;
235
+ }
174
236
  }
237
+ throw new Error("OpenSearch has no protocol candidate");
175
238
  },
176
239
  close: (engine) => engine.close(),
177
240
  retryReason: openSearchRetryReason
@@ -220,21 +283,27 @@ class OpenSearchClient {
220
283
  return this.#disposal ??= (async () => {
221
284
  this.#controller.abort(new Error("OpenSearch client disposed"));
222
285
  await this.#initialization?.catch(() => {});
223
- await Promise.allSettled(this.#operations);
224
286
  await this.#engine?.close();
287
+ await Promise.allSettled(this.#operations);
288
+ if (this.#cleanupErrors.length)
289
+ throw new AggregateError(this.#cleanupErrors, "OpenSearch cleanup failed");
225
290
  })();
226
291
  }
227
292
  }
228
- async function openOpenSearch(source, lifecycle = {}) {
229
- const client = new OpenSearchClient(source, lifecycle);
293
+ async function openOpenSearch(configuration, transport, lifecycle = {}) {
294
+ const client = new OpenSearchClient(configuration, transport, lifecycle);
230
295
  try {
231
296
  await client.initialize();
232
297
  } catch (error) {
233
- await client.dispose();
298
+ try {
299
+ await client.dispose();
300
+ } catch (cleanup) {
301
+ throw new AggregateError([error, cleanup], "OpenSearch initialization and cleanup failed", { cause: error });
302
+ }
234
303
  throw error;
235
304
  }
236
305
  lifecycle.onDispose?.(() => client.dispose());
237
306
  return client;
238
307
  }
239
308
 
240
- export { describeOpenSearchError, isOpenSearchReadApi, OpenSearchEngine, OpenSearchClient, openOpenSearch };
309
+ export { openSearchServername, describeOpenSearchError, isOpenSearchReadApi, OpenSearchEngine, OpenSearchClient, openOpenSearch };
@@ -69,7 +69,14 @@ async function startPortForward(opts) {
69
69
  opts.signal?.addEventListener("abort", abort, { once: true });
70
70
  if (opts.signal?.aborted)
71
71
  abort();
72
- proc.exited.then(() => opts.signal?.removeEventListener("abort", abort), () => opts.signal?.removeEventListener("abort", abort));
72
+ let alive = true;
73
+ proc.exited.then(() => {
74
+ alive = false;
75
+ opts.signal?.removeEventListener("abort", abort);
76
+ }, () => {
77
+ alive = false;
78
+ opts.signal?.removeEventListener("abort", abort);
79
+ });
73
80
  const decoder = new TextDecoder;
74
81
  const deadline = Date.now() + timeoutMs;
75
82
  let out = "";
@@ -105,7 +112,7 @@ async function startPortForward(opts) {
105
112
  log(`port-forward listening local=127.0.0.1:${m[1]} elapsed_ms=${Date.now() - started}`);
106
113
  return {
107
114
  ok: true,
108
- value: { target, localPort: Number(m[1]), command: argv, stop }
115
+ value: { target, localPort: Number(m[1]), command: argv, stop, exited: proc.exited, isAlive: () => alive && !stopped }
109
116
  };
110
117
  }
111
118
  }
@@ -141,6 +148,14 @@ class PortForwardScope {
141
148
  for (const forward of this.#forwards.splice(0).reverse())
142
149
  forward.stop();
143
150
  }
151
+ async dispose() {
152
+ const forwards = [...this.#forwards];
153
+ this.stop();
154
+ const settled = await Promise.allSettled(forwards.map((forward) => forward.exited));
155
+ const errors = settled.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
156
+ if (errors.length)
157
+ throw new AggregateError(errors, "Port-forward cleanup failed");
158
+ }
144
159
  }
145
160
 
146
161
  export { startPortForward, PortForwardScope };