@compforge/harness-toolbox 0.4.3 → 0.4.5

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 (55) hide show
  1. package/README.md +18 -0
  2. package/dist/chunk-835fssgz.js +37 -0
  3. package/dist/{chunk-0s27bgga.js → chunk-ae84rzq5.js} +1 -1
  4. package/dist/{chunk-age65q4n.js → chunk-db4an9td.js} +1 -1
  5. package/dist/{chunk-ftkrjqkc.js → chunk-dpcge1xt.js} +1 -1
  6. package/dist/{chunk-grpygk6f.js → chunk-dx6yvj81.js} +2 -2
  7. package/dist/{chunk-sqbew04a.js → chunk-f3vq900j.js} +2 -2
  8. package/dist/{chunk-yp6tcw22.js → chunk-f98qrpx1.js} +38 -78
  9. package/dist/{chunk-8kts470p.js → chunk-g2bac2jd.js} +12 -6
  10. package/dist/chunk-gm4swndx.js +106 -0
  11. package/dist/{chunk-shsabpc1.js → chunk-r3rnsvwv.js} +1 -1
  12. package/dist/{chunk-20kv4wc3.js → chunk-rkt41za2.js} +7 -1
  13. package/dist/{chunk-8mb6natq.js → chunk-sbt20n84.js} +53 -35
  14. package/dist/{chunk-h73h2b7y.js → chunk-t4hrnaj0.js} +44 -39
  15. package/dist/{chunk-kkts16w9.js → chunk-ww11q58v.js} +1 -1
  16. package/dist/{chunk-4ms037k2.js → chunk-x0yhfg29.js} +2 -2
  17. package/dist/chunk-z15gwjvg.js +145 -0
  18. package/dist/connection/acquire.d.ts +34 -0
  19. package/dist/connection/acquire.js +9 -0
  20. package/dist/datasource.js +1 -1
  21. package/dist/errors.d.ts +4 -0
  22. package/dist/errors.js +3 -1
  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/kubernetes/client.d.ts +4 -0
  28. package/dist/kubernetes/client.js +6 -6
  29. package/dist/kubernetes/compute-model.d.ts +115 -0
  30. package/dist/kubernetes/compute-model.js +0 -0
  31. package/dist/kubernetes/compute-quantity.d.ts +18 -0
  32. package/dist/kubernetes/compute-quantity.js +23 -0
  33. package/dist/kubernetes/compute.d.ts +17 -0
  34. package/dist/kubernetes/compute.js +211 -0
  35. package/dist/kubernetes/environment.js +6 -6
  36. package/dist/kubernetes/exec-transport.js +2 -2
  37. package/dist/kubernetes/pod-log-datasource.js +1 -1
  38. package/dist/kubernetes/resource-transport.js +3 -3
  39. package/dist/kubernetes/resources.js +2 -2
  40. package/dist/kubernetes/workload.js +2 -2
  41. package/dist/mysql/client.d.ts +5 -2
  42. package/dist/mysql/client.js +5 -4
  43. package/dist/mysql/connection.d.ts +3 -0
  44. package/dist/mysql/connection.js +4 -2
  45. package/dist/mysql/index.js +6 -5
  46. package/dist/mysql/readonly.js +2 -2
  47. package/dist/opensearch/client.d.ts +3 -1
  48. package/dist/opensearch/client.js +3 -2
  49. package/dist/opensearch/index.js +3 -2
  50. package/dist/redis/client.js +3 -2
  51. package/dist/redis/index.js +7 -6
  52. package/dist/s3/datasource.js +1 -1
  53. package/dist/s3/index.js +1 -1
  54. package/package.json +4 -1
  55. package/dist/chunk-136egyff.js +0 -11
package/README.md CHANGED
@@ -173,6 +173,9 @@ the root owns cancellation and must join child work before disposing clients.
173
173
 
174
174
  Catch ToolboxError from the root entry or `@compforge/harness-toolbox/errors`; use `kind` and
175
175
  `code` for decisions. Native `cause` may contain sensitive details and is debug-only.
176
+ Connection acquisition deadlines and Redis handshake/command deadlines raise `TimeoutError`
177
+ from `@compforge/harness-toolbox/errors`. It extends `ToolboxError`, retaining
178
+ `kind: "timeout"` and `code: "ETIMEDOUT"`; OpenSearch preserves it unchanged.
176
179
 
177
180
  For coordinated common/toolbox development, install and build common before installing toolbox:
178
181
 
@@ -376,3 +379,18 @@ reason. The caller owns shared transports such as `PodRelayTransport` and their
376
379
 
377
380
  Routed HTTPS preserves the original TLS server name. Bun disables HTTP keep-alive because its reused
378
381
  socket recovery can replay a failed request below the driver; Node retains connection pooling.
382
+
383
+ ## Kubernetes compute evidence
384
+
385
+ `kubernetes/compute` exports `collectClusterCompute` and `collectWorkloadCompute`. They borrow
386
+ ResourceAccess, use explicit deadlines/item/page budgets, and retain read coverage independently.
387
+ Use `client.clusterResources()` for Nodes and all-namespace Pods; normal client methods retain their
388
+ configured namespace. A forbidden cluster read records `permission_denied`; it does not block a
389
+ separate namespace-scoped Workload read. See [compute collection](../../../docs/toolbox.md#kubernetes-compute-evidence).
390
+
391
+ ### Connection acquisition deadlines
392
+
393
+ MySQL, OpenSearch and Redis use the same acquisition lifecycle. `connectTimeoutMs` includes route mapping
394
+ and handshake/probe; optional `acquireTimeoutMs` bounds all routes together (default: per-route budget × route count).
395
+ Cancellation preserves the caller reason and prevents late mappings from starting a handshake. Query and command
396
+ timeouts remain separate, and failed operations are never replayed on another transport.
@@ -0,0 +1,37 @@
1
+ // src/mysql/connection.ts
2
+ import { createConnection } from "mysql2";
3
+ async function connectMysql(options, signal) {
4
+ signal?.throwIfAborted();
5
+ const native = createConnection(options);
6
+ const connection = native.promise();
7
+ let abort;
8
+ try {
9
+ await new Promise((resolve, reject) => {
10
+ abort = () => {
11
+ destroyMysqlConnection(connection);
12
+ reject(signal.reason);
13
+ };
14
+ signal?.addEventListener("abort", abort, { once: true });
15
+ native.connect((error) => error ? reject(error) : resolve());
16
+ if (signal?.aborted)
17
+ abort();
18
+ });
19
+ signal?.throwIfAborted();
20
+ return connection;
21
+ } catch (error) {
22
+ destroyMysqlConnection(connection);
23
+ throw error;
24
+ } finally {
25
+ signal?.removeEventListener("abort", abort);
26
+ }
27
+ }
28
+ function destroyMysqlConnection(connection) {
29
+ const native = connection.connection;
30
+ try {
31
+ connection.destroy();
32
+ } finally {
33
+ native.stream.destroy();
34
+ }
35
+ }
36
+
37
+ export { connectMysql, destroyMysqlConnection };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  KubernetesError
3
- } from "./chunk-20kv4wc3.js";
3
+ } from "./chunk-rkt41za2.js";
4
4
 
5
5
  // src/kubernetes/workload.ts
6
6
  function failure(message, kind) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  destroyMysqlConnection
3
- } from "./chunk-136egyff.js";
3
+ } from "./chunk-835fssgz.js";
4
4
 
5
5
  // src/mysql/readonly.ts
6
6
  function validateQueryLimits(limits) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  KubernetesError
3
- } from "./chunk-20kv4wc3.js";
3
+ } from "./chunk-rkt41za2.js";
4
4
 
5
5
  // src/kubernetes/resources.ts
6
6
  function resourcePath(query, name) {
@@ -2,10 +2,10 @@ import {
2
2
  BoundedFileClient,
3
3
  fileClientOptions,
4
4
  filePaths
5
- } from "./chunk-kkts16w9.js";
5
+ } from "./chunk-ww11q58v.js";
6
6
  import {
7
7
  FileError
8
- } from "./chunk-20kv4wc3.js";
8
+ } from "./chunk-rkt41za2.js";
9
9
 
10
10
  // src/file/pod.ts
11
11
  import { posix } from "node:path";
@@ -2,10 +2,10 @@ import {
2
2
  BoundedFileClient,
3
3
  fileClientOptions,
4
4
  filePaths
5
- } from "./chunk-kkts16w9.js";
5
+ } from "./chunk-ww11q58v.js";
6
6
  import {
7
7
  FileError
8
- } from "./chunk-20kv4wc3.js";
8
+ } from "./chunk-rkt41za2.js";
9
9
 
10
10
  // src/file/local.ts
11
11
  import { constants } from "node:fs";
@@ -1,9 +1,13 @@
1
+ import {
2
+ acquireConnection,
3
+ awaitWithSignal
4
+ } from "./chunk-gm4swndx.js";
1
5
  import {
2
6
  discoverRedisTopology
3
7
  } from "./chunk-5tbzf9wd.js";
4
8
  import {
5
- ToolboxError
6
- } from "./chunk-20kv4wc3.js";
9
+ TimeoutError
10
+ } from "./chunk-rkt41za2.js";
7
11
  import {
8
12
  isConnectionNetworkError
9
13
  } from "./chunk-h8fwx0k7.js";
@@ -41,26 +45,6 @@ function parseRedisInfo(raw) {
41
45
  }
42
46
  return result;
43
47
  }
44
- async function abortable(pending, signal, cancel) {
45
- if (signal.aborted) {
46
- cancel?.();
47
- pending.catch(() => {});
48
- signal.throwIfAborted();
49
- }
50
- let abort;
51
- const cancelled = new Promise((_, reject) => {
52
- abort = () => {
53
- cancel?.();
54
- reject(signal.reason);
55
- };
56
- signal.addEventListener("abort", abort, { once: true });
57
- });
58
- try {
59
- return await Promise.race([pending, cancelled]);
60
- } finally {
61
- signal.removeEventListener("abort", abort);
62
- }
63
- }
64
48
 
65
49
  class RedisConnection {
66
50
  endpoint;
@@ -101,7 +85,7 @@ class RedisConnection {
101
85
  lastError = err instanceof Error ? err : new Error(String(err));
102
86
  });
103
87
  const timeout = new AbortController;
104
- const timer = setTimeout(() => timeout.abort(new ToolboxError("Redis handshake timed out", { kind: "timeout", code: "ETIMEDOUT" })), config.connectTimeoutMs ?? config.timeoutMs);
88
+ const timer = setTimeout(() => timeout.abort(new TimeoutError("Redis handshake timed out")), config.connectTimeoutMs ?? config.timeoutMs);
105
89
  const signal = AbortSignal.any([
106
90
  timeout.signal,
107
91
  ...config.signal ? [config.signal] : [],
@@ -112,7 +96,7 @@ class RedisConnection {
112
96
  client.destroy();
113
97
  };
114
98
  try {
115
- await abortable(client.connect(), signal, close);
99
+ await awaitWithSignal(client.connect(), signal, close);
116
100
  signal.throwIfAborted();
117
101
  return new RedisConnection(endpoint, client, config.timeoutMs, () => lastError, config.signal);
118
102
  } catch (error) {
@@ -132,10 +116,10 @@ class RedisConnection {
132
116
  throw new Error(`Redis connection is closed${reason ? `: ${reason}` : ""}`);
133
117
  }
134
118
  const timeout = new AbortController;
135
- const timer = setTimeout(() => timeout.abort(new ToolboxError(`Redis ${operationName} timed out after ${this.timeoutMs}ms (${this.endpoint.host}:${this.endpoint.port})`, { kind: "timeout", code: "ETIMEDOUT" })), this.timeoutMs);
119
+ const timer = setTimeout(() => timeout.abort(new TimeoutError(`Redis ${operationName} timed out after ${this.timeoutMs}ms (${this.endpoint.host}:${this.endpoint.port})`)), this.timeoutMs);
136
120
  const signal = this.signal ? AbortSignal.any([this.signal, timeout.signal]) : timeout.signal;
137
121
  try {
138
- return await abortable(Promise.resolve().then(() => {
122
+ return await awaitWithSignal(Promise.resolve().then(() => {
139
123
  signal.throwIfAborted();
140
124
  return operation();
141
125
  }), signal, () => this.close());
@@ -229,58 +213,34 @@ class RedisAccess {
229
213
  });
230
214
  return pending;
231
215
  }
232
- async#open(endpoint, database, credentials) {
233
- const timeout = new AbortController;
234
- const attemptBudget = this.baseConfig.connectTimeoutMs ?? this.baseConfig.timeoutMs;
235
- const budget = this.baseConfig.acquireTimeoutMs ?? attemptBudget * this.#transports.length;
236
- const deadline = performance.now() + budget;
237
- const timer = setTimeout(() => timeout.abort(new ToolboxError(`Redis connection timed out after ${budget}ms (${endpoint.host}:${endpoint.port})`, { kind: "timeout", code: "ETIMEDOUT" })), budget);
238
- const signal = AbortSignal.any([this.#signal, timeout.signal]);
239
- let reason;
240
- try {
241
- for (let index = 0;index < this.#transports.length; index++) {
242
- signal.throwIfAborted();
243
- const transport = this.#transports[index];
244
- const attempt = new AbortController;
245
- const remaining = Math.min(attemptBudget, Math.max(1, Math.ceil(deadline - performance.now())));
246
- const attemptTimer = setTimeout(() => attempt.abort(new ToolboxError(`Redis connection attempt timed out (${transport.name})`, { kind: "timeout", code: "ETIMEDOUT" })), remaining);
247
- const attemptSignal = AbortSignal.any([signal, attempt.signal]);
248
- try {
249
- const mapped = await abortable(transport.connect(endpoint), attemptSignal);
250
- attemptSignal.throwIfAborted();
251
- const pending = this.connectionFactory(endpoint, mapped, {
252
- ...this.baseConfig,
253
- ...credentials,
254
- database,
255
- connectTimeoutMs: remaining,
256
- signal: this.#signal,
257
- connectSignal: attemptSignal
258
- });
259
- const prepared = pending.then((connection2) => {
260
- if (attemptSignal.aborted) {
261
- connection2.close();
262
- attemptSignal.throwIfAborted();
263
- }
264
- return connection2;
265
- });
266
- const connection = await abortable(prepared, attemptSignal);
267
- this.lifecycle.onRoute?.({ transport: mapped.host === endpoint.host && mapped.port === endpoint.port ? "direct" : transport.name, reason });
268
- return connection;
269
- } catch (failure) {
270
- signal.throwIfAborted();
271
- const error = attemptSignal.aborted ? attemptSignal.reason : failure;
272
- const network = isConnectionNetworkError(error) || error instanceof SocketClosedUnexpectedlyError || error instanceof ConnectionTimeoutError;
273
- if (!network || index === this.#transports.length - 1)
274
- throw error;
275
- reason = error instanceof SocketClosedUnexpectedlyError ? "connection_lost" : error instanceof ConnectionTimeoutError ? "ETIMEDOUT" : String(error.code);
276
- } finally {
277
- clearTimeout(attemptTimer);
278
- }
216
+ #open(endpoint, database, credentials) {
217
+ return acquireConnection(this.#transports, {
218
+ name: "Redis connection",
219
+ signal: this.#signal,
220
+ connectTimeoutMs: this.baseConfig.connectTimeoutMs ?? this.baseConfig.timeoutMs,
221
+ acquireTimeoutMs: this.baseConfig.acquireTimeoutMs,
222
+ onRoute: this.lifecycle.onRoute,
223
+ open: async (transport, attempt) => {
224
+ const mapped = await transport.connect(endpoint);
225
+ const connection = await this.connectionFactory(endpoint, mapped, {
226
+ ...this.baseConfig,
227
+ ...credentials,
228
+ database,
229
+ connectTimeoutMs: attempt.remainingMs(),
230
+ signal: this.#signal,
231
+ connectSignal: attempt.signal
232
+ });
233
+ return { connection, transport: mapped.host === endpoint.host && mapped.port === endpoint.port ? "direct" : transport.name };
234
+ },
235
+ close: (connection) => connection.close(),
236
+ retryReason: (error) => {
237
+ if (error instanceof SocketClosedUnexpectedlyError)
238
+ return "connection_lost";
239
+ if (error instanceof ConnectionTimeoutError)
240
+ return "ETIMEDOUT";
241
+ return isConnectionNetworkError(error) ? String(error.code) : undefined;
279
242
  }
280
- throw new Error("Redis requires a TCP transport");
281
- } finally {
282
- clearTimeout(timer);
283
- }
243
+ });
284
244
  }
285
245
  close() {
286
246
  return this.#closing ??= (async () => {
@@ -319,7 +279,7 @@ class RedisClient {
319
279
  initialize() {
320
280
  this.signal.throwIfAborted();
321
281
  return this.#initialization ??= (async () => {
322
- this.#target = await this.source.resolve();
282
+ this.#target = await awaitWithSignal(this.source.resolve(), this.signal);
323
283
  this.signal.throwIfAborted();
324
284
  if (!this.source.transports.length || this.source.transports.some((transport) => transport.kind !== "tcp")) {
325
285
  throw new Error("Redis requires TCP transports");
@@ -1,18 +1,18 @@
1
- import {
2
- KubernetesResourceTransport
3
- } from "./chunk-4ms037k2.js";
4
1
  import {
5
2
  resolveWorkload
6
- } from "./chunk-0s27bgga.js";
3
+ } from "./chunk-ae84rzq5.js";
7
4
  import {
8
5
  ServicePortForwarder
9
6
  } from "./chunk-2171tajs.js";
7
+ import {
8
+ KubernetesResourceTransport
9
+ } from "./chunk-x0yhfg29.js";
10
10
  import {
11
11
  KubernetesExecTransport
12
- } from "./chunk-shsabpc1.js";
12
+ } from "./chunk-r3rnsvwv.js";
13
13
  import {
14
14
  KubernetesError
15
- } from "./chunk-20kv4wc3.js";
15
+ } from "./chunk-rkt41za2.js";
16
16
  import {
17
17
  KubectlExecutor
18
18
  } from "./chunk-hmah9t8e.js";
@@ -83,6 +83,12 @@ class KubernetesClient {
83
83
  list(query, options) {
84
84
  return this.#track(() => this.#resourceAccess().list({ ...query, namespace: query.namespace ?? this.kube.namespace }, this.#options(options)));
85
85
  }
86
+ clusterResources() {
87
+ return {
88
+ get: (ref, options) => this.#track(() => this.#resourceAccess().get(ref, this.#options(options))),
89
+ list: (query, options) => this.#track(() => this.#resourceAccess().list(query, this.#options(options)))
90
+ };
91
+ }
86
92
  inNamespace(namespace) {
87
93
  if (!namespace.trim())
88
94
  throw new KubernetesError("Namespace is required", { kind: "invalid_argument" });
@@ -0,0 +1,106 @@
1
+ import {
2
+ TimeoutError,
3
+ ToolboxError
4
+ } from "./chunk-rkt41za2.js";
5
+
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
+ }
27
+ async function acquireConnection(routes, options) {
28
+ const budget = options.acquireTimeoutMs ?? options.connectTimeoutMs * routes.length;
29
+ if (!routes.length)
30
+ throw new ToolboxError(`${options.name} requires a transport`, { kind: "invalid_argument" });
31
+ if ([options.connectTimeoutMs, budget].some((value) => !Number.isFinite(value) || value <= 0 || value > 2147483647)) {
32
+ throw new ToolboxError(`${options.name} timeouts must be positive milliseconds within timer range`, { kind: "invalid_argument" });
33
+ }
34
+ options.signal?.throwIfAborted();
35
+ const timeout = new AbortController;
36
+ const timedOut = (message) => new TimeoutError(message);
37
+ const deadline = performance.now() + budget;
38
+ const timer = setTimeout(() => timeout.abort(timedOut(`${options.name} timed out after ${budget}ms`)), budget);
39
+ const signal = options.signal ? AbortSignal.any([options.signal, timeout.signal]) : timeout.signal;
40
+ let reason;
41
+ try {
42
+ for (const [index, route] of routes.entries()) {
43
+ if (performance.now() >= deadline)
44
+ timeout.abort(timedOut(`${options.name} timed out after ${budget}ms`));
45
+ signal.throwIfAborted();
46
+ const attempt = new AbortController;
47
+ const end = Math.min(deadline, performance.now() + options.connectTimeoutMs);
48
+ const expire = () => attempt.abort(timedOut(`${options.name} attempt timed out (${route.name})`));
49
+ const attemptTimer = setTimeout(expire, Math.max(1, Math.ceil(end - performance.now())));
50
+ const attemptSignal = AbortSignal.any([signal, attempt.signal]);
51
+ const context = {
52
+ signal: attemptSignal,
53
+ remainingMs: () => {
54
+ if (performance.now() >= end)
55
+ expire();
56
+ attemptSignal.throwIfAborted();
57
+ return Math.max(1, Math.ceil(end - performance.now()));
58
+ }
59
+ };
60
+ let candidate;
61
+ let discarded = false;
62
+ const discard = async () => {
63
+ if (!candidate || discarded)
64
+ return;
65
+ discarded = true;
66
+ await options.close(candidate.connection);
67
+ };
68
+ let result;
69
+ try {
70
+ const prepared = options.open(route, context).then(async (value) => {
71
+ candidate = value;
72
+ if (attemptSignal.aborted) {
73
+ await discard();
74
+ attemptSignal.throwIfAborted();
75
+ }
76
+ return value;
77
+ });
78
+ result = await awaitWithSignal(prepared, attemptSignal);
79
+ } catch (failure) {
80
+ await discard();
81
+ signal.throwIfAborted();
82
+ const error = attemptSignal.aborted ? attemptSignal.reason : failure;
83
+ reason = options.retryReason(error);
84
+ if (reason === undefined || index === routes.length - 1)
85
+ throw error;
86
+ continue;
87
+ } finally {
88
+ clearTimeout(attemptTimer);
89
+ }
90
+ try {
91
+ attemptSignal.throwIfAborted();
92
+ options.onRoute?.({ transport: result.transport, reason });
93
+ attemptSignal.throwIfAborted();
94
+ return result.connection;
95
+ } catch (error) {
96
+ await discard();
97
+ throw error;
98
+ }
99
+ }
100
+ throw new Error("Connection routes exhausted");
101
+ } finally {
102
+ clearTimeout(timer);
103
+ }
104
+ }
105
+
106
+ export { awaitWithSignal, acquireConnection };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  KubernetesError,
3
3
  kubernetesHttpError
4
- } from "./chunk-20kv4wc3.js";
4
+ } from "./chunk-rkt41za2.js";
5
5
  import {
6
6
  ConcurrencyPool
7
7
  } from "./chunk-v3hxvnrc.js";
@@ -15,6 +15,12 @@ class KubernetesError extends ToolboxError {
15
15
 
16
16
  class FileError extends ToolboxError {
17
17
  }
18
+
19
+ class TimeoutError extends ToolboxError {
20
+ constructor(message, options) {
21
+ super(message, { kind: "timeout", code: "ETIMEDOUT", cause: options?.cause });
22
+ }
23
+ }
18
24
  function kubernetesHttpError(status) {
19
25
  const kind = {
20
26
  401: "authentication_failed",
@@ -41,4 +47,4 @@ function kubernetesCommandError(result) {
41
47
  return status ? kubernetesHttpError(status) : new KubernetesError("Kubernetes resource request failed", { kind: "operation_failed" });
42
48
  }
43
49
 
44
- export { ToolboxError, KubernetesError, FileError, kubernetesHttpError, kubernetesCommandError };
50
+ export { ToolboxError, KubernetesError, FileError, TimeoutError, kubernetesHttpError, kubernetesCommandError };
@@ -1,6 +1,10 @@
1
+ import {
2
+ acquireConnection,
3
+ awaitWithSignal
4
+ } from "./chunk-gm4swndx.js";
1
5
  import {
2
6
  ToolboxError
3
- } from "./chunk-20kv4wc3.js";
7
+ } from "./chunk-rkt41za2.js";
4
8
  import {
5
9
  isConnectionNetworkError
6
10
  } from "./chunk-h8fwx0k7.js";
@@ -68,6 +72,15 @@ class OpenSearchEngine {
68
72
  return this.client.close();
69
73
  }
70
74
  }
75
+ function openSearchRetryReason(error) {
76
+ if (isConnectionNetworkError(error))
77
+ return String(error.code);
78
+ if (error instanceof errors.TimeoutError)
79
+ return "ETIMEDOUT";
80
+ if (error instanceof errors.ConnectionError)
81
+ return error.name;
82
+ return;
83
+ }
71
84
 
72
85
  class OpenSearchClient {
73
86
  source;
@@ -86,44 +99,49 @@ class OpenSearchClient {
86
99
  initialize() {
87
100
  this.signal.throwIfAborted();
88
101
  return this.#initialization ??= (async () => {
89
- const target = await this.source.resolve();
102
+ const target = await awaitWithSignal(this.source.resolve(), this.signal);
90
103
  this.signal.throwIfAborted();
91
104
  const original = new URL(target.node);
92
- let reason;
93
- for (const [index, transport] of this.source.transports.entries()) {
105
+ try {
106
+ this.#engine = await acquireConnection(this.source.transports, {
107
+ name: "OpenSearch connection",
108
+ signal: this.signal,
109
+ connectTimeoutMs: target.connectTimeoutMs ?? target.requestTimeoutMs ?? 1e4,
110
+ acquireTimeoutMs: target.acquireTimeoutMs,
111
+ onRoute: this.lifecycle.onRoute,
112
+ open: async (transport, attempt) => {
113
+ if (transport.kind !== "tcp")
114
+ throw new Error("OpenSearch requires a TCP transport");
115
+ const endpoint = await transport.connect({
116
+ host: original.hostname,
117
+ port: Number(original.port || (original.protocol === "https:" ? 443 : 80)),
118
+ servername: target.servername ?? original.hostname
119
+ });
120
+ attempt.signal.throwIfAborted();
121
+ const url = new URL(original);
122
+ url.hostname = endpoint.host;
123
+ url.port = String(endpoint.port);
124
+ const engine = new OpenSearchEngine({ ...target, node: url.toString(), servername: endpoint.servername ?? target.servername ?? original.hostname });
125
+ try {
126
+ await engine.ping(attempt.signal, attempt.remainingMs());
127
+ return { connection: engine, transport: transport.name };
128
+ } catch (error) {
129
+ await engine.close();
130
+ throw error;
131
+ }
132
+ },
133
+ close: (engine) => engine.close(),
134
+ retryReason: openSearchRetryReason
135
+ });
136
+ } catch (error) {
94
137
  this.signal.throwIfAborted();
95
- if (transport.kind !== "tcp")
96
- throw new Error("OpenSearch requires a TCP transport");
97
- let engine;
98
- try {
99
- const endpoint = await transport.connect({
100
- host: original.hostname,
101
- port: Number(original.port || (original.protocol === "https:" ? 443 : 80)),
102
- servername: target.servername ?? original.hostname
103
- });
104
- this.signal.throwIfAborted();
105
- const url = new URL(original);
106
- url.hostname = endpoint.host;
107
- url.port = String(endpoint.port);
108
- engine = new OpenSearchEngine({ ...target, node: url.toString(), servername: endpoint.servername ?? target.servername ?? original.hostname });
109
- await engine.ping(this.signal, target.connectTimeoutMs ?? target.requestTimeoutMs ?? 1e4);
110
- this.signal.throwIfAborted();
111
- this.lifecycle.onRoute?.({ transport: transport.name, reason });
112
- this.#engine = engine;
113
- return;
114
- } catch (error) {
115
- await engine?.close();
116
- this.signal.throwIfAborted();
117
- const network = isConnectionNetworkError(error);
118
- if (!network && !(error instanceof errors.ConnectionError) && !(error instanceof errors.TimeoutError))
119
- throw error;
120
- reason = network ? String(error.code) : error.name;
121
- if (index === this.source.transports.length - 1) {
122
- throw new ToolboxError(`OpenSearch connect failed (${reason})`, { kind: "connection_failed", code: reason, cause: error });
123
- }
124
- }
138
+ if (error instanceof ToolboxError && error.kind === "timeout")
139
+ throw error;
140
+ const reason = openSearchRetryReason(error);
141
+ if (reason === undefined)
142
+ throw error;
143
+ throw new ToolboxError(`OpenSearch connect failed (${reason})`, { kind: "connection_failed", code: reason, cause: error });
125
144
  }
126
- throw new Error("OpenSearch DataSource has no transport");
127
145
  })();
128
146
  }
129
147
  #ready() {