@compforge/harness-toolbox 0.4.8 → 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-vymm4g9h.js → chunk-kxpecgy7.js} +106 -37
  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
@@ -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 };
@@ -1,18 +1,18 @@
1
- import {
2
- acquireConnection,
3
- awaitWithSignal
4
- } from "./chunk-gm4swndx.js";
5
1
  import {
6
2
  discoverRedisTopology
7
3
  } from "./chunk-5tbzf9wd.js";
4
+ import {
5
+ isConnectionNetworkError
6
+ } from "./chunk-18gcp0ck.js";
7
+ import {
8
+ Transport
9
+ } from "./chunk-sb5qzv37.js";
8
10
  import {
9
11
  TimeoutError
10
12
  } from "./chunk-rkt41za2.js";
11
- import {
12
- isConnectionNetworkError
13
- } from "./chunk-h8fwx0k7.js";
14
13
 
15
14
  // src/redis/client.ts
15
+ import { raceSignal } from "race-signal";
16
16
  import { createClient, SocketClosedUnexpectedlyError, ConnectionTimeoutError } from "@redis/client";
17
17
  function stringValue(value) {
18
18
  if (Buffer.isBuffer(value))
@@ -96,7 +96,7 @@ class RedisConnection {
96
96
  client.destroy();
97
97
  };
98
98
  try {
99
- await awaitWithSignal(client.connect(), signal, close);
99
+ await raceSignal(client.connect(), signal);
100
100
  signal.throwIfAborted();
101
101
  return new RedisConnection(endpoint, client, config.timeoutMs, () => lastError, config.signal);
102
102
  } catch (error) {
@@ -119,12 +119,14 @@ class RedisConnection {
119
119
  const timer = setTimeout(() => timeout.abort(new TimeoutError(`Redis ${operationName} timed out after ${this.timeoutMs}ms (${this.endpoint.host}:${this.endpoint.port})`)), this.timeoutMs);
120
120
  const signal = this.signal ? AbortSignal.any([this.signal, timeout.signal]) : timeout.signal;
121
121
  try {
122
- return await awaitWithSignal(Promise.resolve().then(() => {
122
+ return await raceSignal(Promise.resolve().then(() => {
123
123
  signal.throwIfAborted();
124
124
  return operation();
125
- }), signal, () => this.close());
125
+ }), signal);
126
126
  } finally {
127
127
  clearTimeout(timer);
128
+ if (signal.aborted)
129
+ this.close();
128
130
  }
129
131
  }
130
132
  ping() {
@@ -171,19 +173,17 @@ class RedisAccess {
171
173
  #connections = new Map;
172
174
  #controller = new AbortController;
173
175
  #signal;
174
- #transports;
176
+ #transport;
175
177
  #closing;
176
178
  constructor(transports, baseConfig, connectionFactory = RedisConnection.connect, lifecycle = {}) {
177
179
  this.baseConfig = baseConfig;
178
180
  this.connectionFactory = connectionFactory;
179
181
  this.lifecycle = lifecycle;
180
- this.#transports = Array.isArray(transports) ? transports : [transports];
181
- if (!this.#transports.length)
182
- throw new Error("Redis requires a TCP transport");
182
+ this.#transport = transports instanceof Transport ? transports : new Transport(Array.isArray(transports) ? transports : [transports]);
183
183
  for (const value of [
184
184
  baseConfig.timeoutMs,
185
185
  baseConfig.connectTimeoutMs ?? baseConfig.timeoutMs,
186
- baseConfig.acquireTimeoutMs ?? (baseConfig.connectTimeoutMs ?? baseConfig.timeoutMs) * this.#transports.length
186
+ baseConfig.acquireTimeoutMs ?? (baseConfig.connectTimeoutMs ?? baseConfig.timeoutMs)
187
187
  ]) {
188
188
  if (!Number.isFinite(value) || value <= 0)
189
189
  throw new Error("Redis timeouts must be positive milliseconds");
@@ -214,14 +214,16 @@ class RedisAccess {
214
214
  return pending;
215
215
  }
216
216
  #open(endpoint, database, credentials) {
217
- return acquireConnection(this.#transports, {
217
+ return this.#transport.acquire({
218
218
  name: "Redis connection",
219
219
  signal: this.#signal,
220
220
  connectTimeoutMs: this.baseConfig.connectTimeoutMs ?? this.baseConfig.timeoutMs,
221
221
  acquireTimeoutMs: this.baseConfig.acquireTimeoutMs,
222
222
  onRoute: this.lifecycle.onRoute,
223
223
  open: async (transport, attempt) => {
224
- const mapped = await transport.connect(endpoint);
224
+ if (transport.kind !== "tcp")
225
+ throw new Error("Redis requires a TCP transport");
226
+ const mapped = await transport.connect(endpoint, attempt);
225
227
  const connection = await this.connectionFactory(endpoint, mapped, {
226
228
  ...this.baseConfig,
227
229
  ...credentials,
@@ -263,7 +265,8 @@ class RedisAccess {
263
265
  }
264
266
 
265
267
  class RedisClient {
266
- source;
268
+ configuration;
269
+ transport;
267
270
  lifecycle;
268
271
  #controller = new AbortController;
269
272
  signal;
@@ -271,22 +274,20 @@ class RedisClient {
271
274
  #disposal;
272
275
  #access;
273
276
  #target;
274
- constructor(source, lifecycle = {}) {
275
- this.source = source;
277
+ constructor(configuration, transport, lifecycle = {}) {
278
+ this.configuration = configuration;
279
+ this.transport = transport;
276
280
  this.lifecycle = lifecycle;
277
281
  this.signal = lifecycle.signal ? AbortSignal.any([lifecycle.signal, this.#controller.signal]) : this.#controller.signal;
278
282
  }
279
283
  initialize() {
280
284
  this.signal.throwIfAborted();
281
285
  return this.#initialization ??= (async () => {
282
- this.#target = await awaitWithSignal(this.source.resolve(), this.signal);
286
+ this.#target = await raceSignal(typeof this.configuration === "function" ? this.configuration() : Promise.resolve(this.configuration), this.signal);
283
287
  this.signal.throwIfAborted();
284
- if (!this.source.transports.length || this.source.transports.some((transport) => transport.kind !== "tcp")) {
285
- throw new Error("Redis requires TCP transports");
286
- }
287
288
  if (this.#target.clusterType === "cluster" && this.#target.database !== 0)
288
289
  throw new Error("Redis Cluster requires database 0");
289
- this.#access = new RedisAccess(this.source.transports, this.#target, undefined, { ...this.lifecycle, signal: this.signal });
290
+ this.#access = new RedisAccess(this.transport, this.#target, undefined, { ...this.lifecycle, signal: this.signal });
290
291
  })();
291
292
  }
292
293
  get access() {
@@ -311,8 +312,8 @@ class RedisClient {
311
312
  })();
312
313
  }
313
314
  }
314
- async function openRedis(source, lifecycle = {}) {
315
- const client = new RedisClient(source, lifecycle);
315
+ async function openRedis(configuration, transport, lifecycle = {}) {
316
+ const client = new RedisClient(configuration, transport, lifecycle);
316
317
  try {
317
318
  await client.initialize();
318
319
  } catch (error) {
@@ -0,0 +1,62 @@
1
+ import {
2
+ acquireConnection
3
+ } from "./chunk-43mcyrh4.js";
4
+
5
+ // src/transport/lifecycle.ts
6
+ class Transport {
7
+ candidates;
8
+ #controller = new AbortController;
9
+ resourceSignal = this.#controller.signal;
10
+ signal;
11
+ #pending = new Set;
12
+ #cleanupErrors = [];
13
+ #disposal;
14
+ constructor(candidates = [], signal) {
15
+ this.candidates = candidates;
16
+ this.signal = signal ? AbortSignal.any([signal, this.#controller.signal]) : this.#controller.signal;
17
+ }
18
+ paths() {
19
+ return this.candidates;
20
+ }
21
+ async initialize() {
22
+ this.signal.throwIfAborted();
23
+ }
24
+ acquire(options) {
25
+ this.signal.throwIfAborted();
26
+ const signal = options.signal ? AbortSignal.any([options.signal, this.signal]) : this.signal;
27
+ return acquireConnection(this.paths(), {
28
+ ...options,
29
+ signal,
30
+ onPending: (pending) => {
31
+ this.#pending.add(pending);
32
+ pending.then(() => this.#pending.delete(pending), () => this.#pending.delete(pending));
33
+ },
34
+ close: async (connection) => {
35
+ try {
36
+ await options.close(connection);
37
+ } catch (error) {
38
+ this.#cleanupErrors.push(error);
39
+ throw error;
40
+ }
41
+ }
42
+ });
43
+ }
44
+ async close() {}
45
+ dispose() {
46
+ return this.#disposal ??= (async () => {
47
+ this.#controller.abort(new Error("Transport disposed"));
48
+ const errors = [];
49
+ try {
50
+ await this.close();
51
+ } catch (error) {
52
+ errors.push(error);
53
+ }
54
+ await Promise.allSettled([...this.#pending]);
55
+ errors.push(...this.#cleanupErrors);
56
+ if (errors.length)
57
+ throw new AggregateError(errors, "Transport cleanup failed");
58
+ })();
59
+ }
60
+ }
61
+
62
+ export { Transport };
@@ -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";
@@ -1,21 +1,21 @@
1
+ import {
2
+ KubernetesResourceTransport
3
+ } from "./chunk-5w85c7cs.js";
1
4
  import {
2
5
  resolveWorkload
3
6
  } from "./chunk-ae84rzq5.js";
4
- import {
5
- ServicePortForwarder
6
- } from "./chunk-1ts7pswj.js";
7
- import {
8
- KubernetesResourceTransport
9
- } from "./chunk-x0yhfg29.js";
10
7
  import {
11
8
  KubernetesExecTransport
12
- } from "./chunk-r3rnsvwv.js";
9
+ } from "./chunk-xeft7af0.js";
13
10
  import {
14
- KubernetesError
15
- } from "./chunk-rkt41za2.js";
11
+ ServicePortForwarder
12
+ } from "./chunk-0d8vs5q8.js";
16
13
  import {
17
14
  KubectlExecutor
18
15
  } from "./chunk-rq5fc007.js";
16
+ import {
17
+ KubernetesError
18
+ } from "./chunk-rkt41za2.js";
19
19
 
20
20
  // src/kubernetes/client.ts
21
21
  class KubernetesClient {
@@ -1,12 +1,10 @@
1
1
  import type { ClientLifecycle } from "../datasource";
2
- /** Stop waiting without leaving rejected preparation promises unobserved. */
3
- export declare function awaitWithSignal<T>(pending: Promise<T>, signal: AbortSignal, cancel?: () => void): Promise<T>;
4
2
  export interface ConnectionAttempt {
5
3
  readonly signal: AbortSignal;
6
4
  /** Remaining mapping + handshake budget, not a fresh timeout for each phase. */
7
5
  remainingMs(): number;
8
6
  }
9
- interface Acquisition<R, C> {
7
+ export interface Acquisition<R, C> {
10
8
  name: string;
11
9
  connectTimeoutMs: number;
12
10
  acquireTimeoutMs?: number;
@@ -19,6 +17,8 @@ interface Acquisition<R, C> {
19
17
  /** Undefined means terminal; authentication and protocol errors must not change route. */
20
18
  retryReason(error: unknown): string | undefined;
21
19
  onRoute?: ClientLifecycle["onRoute"];
20
+ /** Owner tracks late completion through its rollback, not only the initial open promise. */
21
+ onPending?: (pending: Promise<unknown>) => void;
22
22
  }
23
23
  /**
24
24
  * @spec One acquisition owns ordered attempts and a deadline covering mapping and handshake.
@@ -31,4 +31,3 @@ interface Acquisition<R, C> {
31
31
  export declare function acquireConnection<R extends {
32
32
  readonly name: string;
33
33
  }, C>(routes: readonly R[], options: Acquisition<R, C>): Promise<C>;
34
- export {};
@@ -1,9 +1,7 @@
1
1
  import {
2
- acquireConnection,
3
- awaitWithSignal
4
- } from "../chunk-gm4swndx.js";
2
+ acquireConnection
3
+ } from "../chunk-43mcyrh4.js";
5
4
  import"../chunk-rkt41za2.js";
6
5
  export {
7
- awaitWithSignal,
8
6
  acquireConnection
9
7
  };
@@ -1,11 +1,5 @@
1
1
  export { clientKey, ClientManager, EnvironmentContext, type Client, type ClientProvider, type DataSource, type ServiceDataSource } from "@compforge/harness-common";
2
- import type { Transport } from "./transport";
3
2
  export { ToolboxError, KubernetesError, type ErrorKind } from "./errors";
4
- /** Resolution owns configuration semantics; transports preserve the resolved target and identity. */
5
- export interface ConnectionSource<Target> {
6
- resolve(): Promise<Target>;
7
- readonly transports: readonly Transport[];
8
- }
9
3
  export interface ClientLifecycle {
10
4
  signal?: AbortSignal;
11
5
  onDispose?: (dispose: () => Promise<void>) => void;
@@ -2,9 +2,9 @@ import {
2
2
  BoundedFileClient,
3
3
  fileClientOptions,
4
4
  filePaths
5
- } from "../chunk-ww11q58v.js";
6
- import"../chunk-rkt41za2.js";
5
+ } from "../chunk-rncagjbc.js";
7
6
  import"../chunk-v3hxvnrc.js";
7
+ import"../chunk-rkt41za2.js";
8
8
  export {
9
9
  filePaths,
10
10
  fileClientOptions,
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  PodFileClient,
3
3
  PodFileDataSource
4
- } from "../chunk-dx6yvj81.js";
4
+ } from "../chunk-xhfbark7.js";
5
5
  import {
6
6
  LocalFileClient,
7
7
  LocalFileDataSource
8
- } from "../chunk-f3vq900j.js";
9
- import"../chunk-ww11q58v.js";
8
+ } from "../chunk-92aacbwh.js";
9
+ import"../chunk-rncagjbc.js";
10
+ import"../chunk-v3hxvnrc.js";
10
11
  import {
11
12
  FileError
12
13
  } from "../chunk-rkt41za2.js";
13
- import"../chunk-v3hxvnrc.js";
14
14
 
15
15
  // src/file/index.ts
16
16
  import { FileDataSource } from "@compforge/harness-common";
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  LocalFileClient,
3
3
  LocalFileDataSource
4
- } from "../chunk-f3vq900j.js";
5
- import"../chunk-ww11q58v.js";
6
- import"../chunk-rkt41za2.js";
4
+ } from "../chunk-92aacbwh.js";
5
+ import"../chunk-rncagjbc.js";
7
6
  import"../chunk-v3hxvnrc.js";
7
+ import"../chunk-rkt41za2.js";
8
8
  export {
9
9
  LocalFileDataSource,
10
10
  LocalFileClient
package/dist/file/pod.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  PodFileClient,
3
3
  PodFileDataSource
4
- } from "../chunk-dx6yvj81.js";
5
- import"../chunk-ww11q58v.js";
6
- import"../chunk-rkt41za2.js";
4
+ } from "../chunk-xhfbark7.js";
5
+ import"../chunk-rncagjbc.js";
7
6
  import"../chunk-v3hxvnrc.js";
7
+ import"../chunk-rkt41za2.js";
8
8
  export {
9
9
  PodFileDataSource,
10
10
  PodFileClient