@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
@@ -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;
@@ -19,8 +23,10 @@ function describeOpenSearchError(error) {
19
23
  seen.add(error);
20
24
  const value = error;
21
25
  const safe = (value2) => typeof value2 === "number" ? String(value2) : typeof value2 === "string" && /^[a-zA-Z0-9_.-]{1,80}$/.test(value2) ? value2 : undefined;
26
+ const argument = (value.code === "ERR_INVALID_ARG_VALUE" || value.code === "ERR_INVALID_ARG_TYPE") && typeof value.message === "string" ? value.message.match(/^The ["']([a-zA-Z][a-zA-Z0-9_.]{0,79})["'] (?:argument|option|property)/)?.[1] ?? value.message.match(/^The (?:argument|option|property) ["']([a-zA-Z][a-zA-Z0-9_.]{0,79})["']/)?.[1] : undefined;
22
27
  parts.push([
23
28
  `error=${safe(value.name) ?? "Error"}`,
29
+ argument && `argument=${argument}`,
24
30
  safe(value.code) && `code=${safe(value.code)}`,
25
31
  safe(value.kind) && `kind=${safe(value.kind)}`,
26
32
  safe(value.statusCode ?? value.meta?.statusCode) && `status=${safe(value.statusCode ?? value.meta?.statusCode)}`
@@ -34,16 +40,30 @@ function isOpenSearchReadApi(search) {
34
40
  }
35
41
 
36
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
+ }
37
56
  buildRequestObject(params) {
38
- return { ...super.buildRequestObject(params), servername: this.ssl?.servername };
57
+ return { ...super.buildRequestObject(params), servername: openSearchServername(this.ssl?.servername) };
39
58
  }
40
59
  }
41
60
  function createClient(options) {
42
61
  return new Client({
43
62
  node: options.node,
44
63
  Connection: OpenSearchConnection,
64
+ headers: options.authority ? { host: options.authority } : undefined,
45
65
  auth: options.auth.username && options.auth.password ? { username: options.auth.username, password: options.auth.password } : undefined,
46
- ssl: { rejectUnauthorized: false, servername: options.servername },
66
+ ssl: { rejectUnauthorized: false, servername: openSearchServername(options.servername) },
47
67
  requestTimeout: options.requestTimeoutMs ?? 60000,
48
68
  maxRetries: 0,
49
69
  agent: { keepAlive: !process.versions.bun }
@@ -53,26 +73,36 @@ function createClient(options) {
53
73
  class OpenSearchEngine {
54
74
  options;
55
75
  client;
76
+ #requests = new Set;
77
+ #closed = false;
56
78
  constructor(options, client) {
57
79
  this.options = options;
58
80
  this.client = client ?? createClient(options);
59
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
+ }
60
90
  async count(index, query) {
61
- const response = await this.client.count({ index, body: { query } });
91
+ const response = await this.#run(() => this.client.count({ index, body: { query } }));
62
92
  return Number(response.body.count ?? 0);
63
93
  }
64
94
  async search(index, body) {
65
- const response = await this.client.search({ index, body });
95
+ const response = await this.#run(() => this.client.search({ index, body }));
66
96
  return response.body;
67
97
  }
68
98
  async request(path, query) {
69
99
  if (!this.client.transport)
70
100
  throw new Error("OpenSearch client 不支持通用只读请求");
71
- const response = await this.client.transport.request({
101
+ const response = await this.#run(() => this.client.transport.request({
72
102
  method: "GET",
73
103
  path,
74
104
  querystring: query
75
- });
105
+ }));
76
106
  return response.body;
77
107
  }
78
108
  async ping(signal, timeoutMs = this.options.requestTimeoutMs ?? 60000) {
@@ -85,9 +115,10 @@ class OpenSearchEngine {
85
115
  const controller = new AbortController;
86
116
  const timer = setTimeout(() => controller.abort(new TimeoutError(`OpenSearch probe timed out after ${timeoutMs}ms`)), timeoutMs);
87
117
  const active = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
118
+ let request;
88
119
  try {
89
- const request = this.client.ping({}, { requestTimeout: timeoutMs });
90
- const result = await awaitWithSignal(request, active, () => request.abort?.());
120
+ request = this.#run(() => this.client.ping({}, { requestTimeout: timeoutMs }));
121
+ const result = await raceSignal(request, active);
91
122
  log(`OpenSearch probe ok target=${target} elapsed_ms=${Date.now() - started}`);
92
123
  return result;
93
124
  } catch (error) {
@@ -95,13 +126,18 @@ class OpenSearchEngine {
95
126
  throw error;
96
127
  } finally {
97
128
  clearTimeout(timer);
129
+ if (active.aborted)
130
+ request?.abort?.();
98
131
  }
99
132
  }
100
133
  async close() {
134
+ this.#closed = true;
135
+ for (const request of this.#requests)
136
+ request.abort?.();
101
137
  const controller = new AbortController;
102
138
  const timer = setTimeout(() => controller.abort(new TimeoutError("OpenSearch close timed out after 2000ms")), 2000);
103
139
  try {
104
- await awaitWithSignal(this.client.close(), controller.signal);
140
+ await raceSignal(this.client.close(), controller.signal);
105
141
  } catch (error) {
106
142
  this.options.onDiagnostic?.(`OpenSearch close failed ${describeOpenSearchError(error)}`);
107
143
  throw error;
@@ -121,27 +157,39 @@ function openSearchRetryReason(error) {
121
157
  }
122
158
 
123
159
  class OpenSearchClient {
124
- source;
160
+ configuration;
161
+ transport;
125
162
  lifecycle;
126
163
  #controller = new AbortController;
127
164
  signal;
128
165
  #initialization;
129
166
  #disposal;
130
167
  #engine;
168
+ #node;
169
+ get node() {
170
+ return this.#node;
171
+ }
172
+ #cleanupErrors = [];
131
173
  #operations = new Set;
132
- constructor(source, lifecycle = {}) {
133
- this.source = source;
174
+ constructor(configuration, transport, lifecycle = {}) {
175
+ this.configuration = configuration;
176
+ this.transport = transport;
134
177
  this.lifecycle = lifecycle;
135
178
  this.signal = lifecycle.signal ? AbortSignal.any([lifecycle.signal, this.#controller.signal]) : this.#controller.signal;
136
179
  }
137
180
  initialize() {
138
181
  this.signal.throwIfAborted();
139
182
  return this.#initialization ??= (async () => {
140
- 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);
141
184
  this.signal.throwIfAborted();
142
- 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:"];
143
191
  try {
144
- this.#engine = await acquireConnection(this.source.transports, {
192
+ this.#engine = await this.transport.acquire({
145
193
  name: "OpenSearch connection",
146
194
  signal: this.signal,
147
195
  connectTimeoutMs: target.connectTimeoutMs ?? target.requestTimeoutMs ?? 1e4,
@@ -153,21 +201,40 @@ class OpenSearchClient {
153
201
  this.lifecycle.onDiagnostic?.(`OpenSearch route start transport=${transport.name} target=${original.protocol}//${original.host} timeout_ms=${attempt.remainingMs()}`);
154
202
  const endpoint = await transport.connect({
155
203
  host: original.hostname,
156
- port: Number(original.port || (original.protocol === "https:" ? 443 : 80)),
157
- servername: target.servername ?? original.hostname
158
- });
204
+ port: Number(original.port || (original.protocol === "https:" ? 443 : 80))
205
+ }, attempt);
159
206
  attempt.signal.throwIfAborted();
160
207
  const url = new URL(original);
161
208
  url.hostname = endpoint.host;
162
209
  url.port = String(endpoint.port);
163
- const engine = new OpenSearchEngine({ ...target, onDiagnostic: this.lifecycle.onDiagnostic, node: url.toString(), servername: endpoint.servername ?? target.servername ?? original.hostname });
164
- try {
165
- await engine.ping(attempt.signal, attempt.remainingMs());
166
- return { connection: engine, transport: transport.name };
167
- } catch (error) {
168
- await engine.close();
169
- throw 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
+ });
219
+ try {
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
+ }
170
236
  }
237
+ throw new Error("OpenSearch has no protocol candidate");
171
238
  },
172
239
  close: (engine) => engine.close(),
173
240
  retryReason: openSearchRetryReason
@@ -216,21 +283,27 @@ class OpenSearchClient {
216
283
  return this.#disposal ??= (async () => {
217
284
  this.#controller.abort(new Error("OpenSearch client disposed"));
218
285
  await this.#initialization?.catch(() => {});
219
- await Promise.allSettled(this.#operations);
220
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");
221
290
  })();
222
291
  }
223
292
  }
224
- async function openOpenSearch(source, lifecycle = {}) {
225
- const client = new OpenSearchClient(source, lifecycle);
293
+ async function openOpenSearch(configuration, transport, lifecycle = {}) {
294
+ const client = new OpenSearchClient(configuration, transport, lifecycle);
226
295
  try {
227
296
  await client.initialize();
228
297
  } catch (error) {
229
- 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
+ }
230
303
  throw error;
231
304
  }
232
305
  lifecycle.onDispose?.(() => client.dispose());
233
306
  return client;
234
307
  }
235
308
 
236
- 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