@compforge/harness-toolbox 0.4.4 → 0.4.7

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 (69) hide show
  1. package/README.md +10 -0
  2. package/dist/{chunk-2171tajs.js → chunk-1ts7pswj.js} +4 -3
  3. package/dist/chunk-835fssgz.js +37 -0
  4. package/dist/{chunk-mn6dwsvh.js → chunk-9shz0bw1.js} +3 -1
  5. package/dist/{chunk-0s27bgga.js → chunk-ae84rzq5.js} +1 -1
  6. package/dist/{chunk-age65q4n.js → chunk-db4an9td.js} +1 -1
  7. package/dist/{chunk-ftkrjqkc.js → chunk-dpcge1xt.js} +1 -1
  8. package/dist/{chunk-grpygk6f.js → chunk-dx6yvj81.js} +2 -2
  9. package/dist/{chunk-sqbew04a.js → chunk-f3vq900j.js} +2 -2
  10. package/dist/{chunk-yp6tcw22.js → chunk-f98qrpx1.js} +38 -78
  11. package/dist/{chunk-wv4jwhvj.js → chunk-fqrknvr8.js} +2 -2
  12. package/dist/chunk-gm4swndx.js +106 -0
  13. package/dist/{chunk-c2n8zvyn.js → chunk-m391jmpw.js} +8 -8
  14. package/dist/chunk-m4d29vfd.js +236 -0
  15. package/dist/{chunk-631vce5z.js → chunk-nftzckdy.js} +57 -7
  16. package/dist/{chunk-shsabpc1.js → chunk-r3rnsvwv.js} +1 -1
  17. package/dist/{chunk-20kv4wc3.js → chunk-rkt41za2.js} +7 -1
  18. package/dist/{chunk-hmah9t8e.js → chunk-rq5fc007.js} +1 -1
  19. package/dist/{chunk-h73h2b7y.js → chunk-t4hrnaj0.js} +44 -39
  20. package/dist/{chunk-kkts16w9.js → chunk-ww11q58v.js} +1 -1
  21. package/dist/{chunk-4ms037k2.js → chunk-x0yhfg29.js} +2 -2
  22. package/dist/{chunk-j46x7j7c.js → chunk-xgnz10sr.js} +5 -3
  23. package/dist/{chunk-5zgz1z41.js → chunk-z15gwjvg.js} +1 -1
  24. package/dist/connection/acquire.d.ts +34 -0
  25. package/dist/connection/acquire.js +9 -0
  26. package/dist/datasource.js +1 -1
  27. package/dist/errors.d.ts +4 -0
  28. package/dist/errors.js +3 -1
  29. package/dist/file/client.js +2 -2
  30. package/dist/file/index.js +4 -4
  31. package/dist/file/local.js +3 -3
  32. package/dist/file/pod.js +3 -3
  33. package/dist/kubernetes/client.js +11 -11
  34. package/dist/kubernetes/compute-quantity.js +2 -2
  35. package/dist/kubernetes/compute.js +2 -2
  36. package/dist/kubernetes/environment.js +11 -11
  37. package/dist/kubernetes/exec-transport.js +2 -2
  38. package/dist/kubernetes/executor.js +2 -2
  39. package/dist/kubernetes/pod-log-datasource.js +1 -1
  40. package/dist/kubernetes/pod-log.js +1 -1
  41. package/dist/kubernetes/port-forward.d.ts +5 -0
  42. package/dist/kubernetes/port-forward.js +3 -3
  43. package/dist/kubernetes/resource-transport.js +3 -3
  44. package/dist/kubernetes/resources.js +2 -2
  45. package/dist/kubernetes/service-port-forward.d.ts +2 -0
  46. package/dist/kubernetes/service-port-forward.js +5 -5
  47. package/dist/kubernetes/service.d.ts +1 -1
  48. package/dist/kubernetes/service.js +1 -1
  49. package/dist/kubernetes/workload.js +2 -2
  50. package/dist/mysql/client.d.ts +5 -2
  51. package/dist/mysql/client.js +9 -8
  52. package/dist/mysql/connection.d.ts +3 -0
  53. package/dist/mysql/connection.js +4 -2
  54. package/dist/mysql/index.js +10 -9
  55. package/dist/mysql/readonly.js +2 -2
  56. package/dist/opensearch/client.d.ts +14 -3
  57. package/dist/opensearch/client.js +9 -6
  58. package/dist/opensearch/index.js +9 -6
  59. package/dist/process/index.d.ts +1 -1
  60. package/dist/process/index.js +1 -1
  61. package/dist/redis/client.js +7 -6
  62. package/dist/redis/index.js +11 -10
  63. package/dist/s3/datasource.js +5 -5
  64. package/dist/s3/index.js +5 -5
  65. package/dist/transport/index.js +4 -4
  66. package/dist/transport/pod-relay.js +4 -4
  67. package/package.json +1 -4
  68. package/dist/chunk-136egyff.js +0 -11
  69. package/dist/chunk-8mb6natq.js +0 -178
@@ -0,0 +1,236 @@
1
+ import {
2
+ acquireConnection,
3
+ awaitWithSignal
4
+ } from "./chunk-gm4swndx.js";
5
+ import {
6
+ TimeoutError,
7
+ ToolboxError
8
+ } from "./chunk-rkt41za2.js";
9
+ import {
10
+ isConnectionNetworkError
11
+ } from "./chunk-h8fwx0k7.js";
12
+
13
+ // src/opensearch/client.ts
14
+ import { Client, Connection, errors } from "@opensearch-project/opensearch";
15
+ function describeOpenSearchError(error) {
16
+ const parts = [];
17
+ const seen = new Set;
18
+ while (error && typeof error === "object" && !seen.has(error) && parts.length < 4) {
19
+ seen.add(error);
20
+ const value = error;
21
+ const safe = (value2) => typeof value2 === "number" ? String(value2) : typeof value2 === "string" && /^[a-zA-Z0-9_.-]{1,80}$/.test(value2) ? value2 : undefined;
22
+ parts.push([
23
+ `error=${safe(value.name) ?? "Error"}`,
24
+ safe(value.code) && `code=${safe(value.code)}`,
25
+ safe(value.kind) && `kind=${safe(value.kind)}`,
26
+ safe(value.statusCode ?? value.meta?.statusCode) && `status=${safe(value.statusCode ?? value.meta?.statusCode)}`
27
+ ].filter(Boolean).join(" "));
28
+ error = value.cause;
29
+ }
30
+ return parts.join(" cause=") || "error=UnknownError";
31
+ }
32
+ function isOpenSearchReadApi(search) {
33
+ return typeof search.request === "function";
34
+ }
35
+
36
+ class OpenSearchConnection extends Connection {
37
+ buildRequestObject(params) {
38
+ return { ...super.buildRequestObject(params), servername: this.ssl?.servername };
39
+ }
40
+ }
41
+ function createClient(options) {
42
+ return new Client({
43
+ node: options.node,
44
+ Connection: OpenSearchConnection,
45
+ auth: options.auth.username && options.auth.password ? { username: options.auth.username, password: options.auth.password } : undefined,
46
+ ssl: { rejectUnauthorized: false, servername: options.servername },
47
+ requestTimeout: options.requestTimeoutMs ?? 60000,
48
+ maxRetries: 0,
49
+ agent: { keepAlive: !process.versions.bun }
50
+ });
51
+ }
52
+
53
+ class OpenSearchEngine {
54
+ options;
55
+ client;
56
+ constructor(options, client) {
57
+ this.options = options;
58
+ this.client = client ?? createClient(options);
59
+ }
60
+ async count(index, query) {
61
+ const response = await this.client.count({ index, body: { query } });
62
+ return Number(response.body.count ?? 0);
63
+ }
64
+ async search(index, body) {
65
+ const response = await this.client.search({ index, body });
66
+ return response.body;
67
+ }
68
+ async request(path, query) {
69
+ if (!this.client.transport)
70
+ throw new Error("OpenSearch client 不支持通用只读请求");
71
+ const response = await this.client.transport.request({
72
+ method: "GET",
73
+ path,
74
+ querystring: query
75
+ });
76
+ return response.body;
77
+ }
78
+ async ping(signal, timeoutMs = this.options.requestTimeoutMs ?? 60000) {
79
+ signal?.throwIfAborted();
80
+ const started = Date.now();
81
+ const url = new URL(this.options.node);
82
+ const target = `${url.protocol}//${url.host}`;
83
+ const log = this.options.onDiagnostic ?? (() => {});
84
+ log(`OpenSearch probe start target=${target} timeout_ms=${timeoutMs}`);
85
+ const controller = new AbortController;
86
+ const timer = setTimeout(() => controller.abort(new TimeoutError(`OpenSearch probe timed out after ${timeoutMs}ms`)), timeoutMs);
87
+ const active = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
88
+ try {
89
+ const request = this.client.ping({}, { requestTimeout: timeoutMs });
90
+ const result = await awaitWithSignal(request, active, () => request.abort?.());
91
+ log(`OpenSearch probe ok target=${target} elapsed_ms=${Date.now() - started}`);
92
+ return result;
93
+ } catch (error) {
94
+ log(`OpenSearch probe failed target=${target} elapsed_ms=${Date.now() - started} ${describeOpenSearchError(error)}`);
95
+ throw error;
96
+ } finally {
97
+ clearTimeout(timer);
98
+ }
99
+ }
100
+ async close() {
101
+ const controller = new AbortController;
102
+ const timer = setTimeout(() => controller.abort(new TimeoutError("OpenSearch close timed out after 2000ms")), 2000);
103
+ try {
104
+ await awaitWithSignal(this.client.close(), controller.signal);
105
+ } catch (error) {
106
+ this.options.onDiagnostic?.(`OpenSearch close failed ${describeOpenSearchError(error)}`);
107
+ throw error;
108
+ } finally {
109
+ clearTimeout(timer);
110
+ }
111
+ }
112
+ }
113
+ function openSearchRetryReason(error) {
114
+ if (isConnectionNetworkError(error))
115
+ return String(error.code);
116
+ if (error instanceof errors.TimeoutError)
117
+ return "ETIMEDOUT";
118
+ if (error instanceof errors.ConnectionError)
119
+ return error.name;
120
+ return;
121
+ }
122
+
123
+ class OpenSearchClient {
124
+ source;
125
+ lifecycle;
126
+ #controller = new AbortController;
127
+ signal;
128
+ #initialization;
129
+ #disposal;
130
+ #engine;
131
+ #operations = new Set;
132
+ constructor(source, lifecycle = {}) {
133
+ this.source = source;
134
+ this.lifecycle = lifecycle;
135
+ this.signal = lifecycle.signal ? AbortSignal.any([lifecycle.signal, this.#controller.signal]) : this.#controller.signal;
136
+ }
137
+ initialize() {
138
+ this.signal.throwIfAborted();
139
+ return this.#initialization ??= (async () => {
140
+ const target = await awaitWithSignal(this.source.resolve(), this.signal);
141
+ this.signal.throwIfAborted();
142
+ const original = new URL(target.node);
143
+ try {
144
+ this.#engine = await acquireConnection(this.source.transports, {
145
+ name: "OpenSearch connection",
146
+ signal: this.signal,
147
+ connectTimeoutMs: target.connectTimeoutMs ?? target.requestTimeoutMs ?? 1e4,
148
+ acquireTimeoutMs: target.acquireTimeoutMs,
149
+ onRoute: this.lifecycle.onRoute,
150
+ open: async (transport, attempt) => {
151
+ if (transport.kind !== "tcp")
152
+ throw new Error("OpenSearch requires a TCP transport");
153
+ this.lifecycle.onDiagnostic?.(`OpenSearch route start transport=${transport.name} target=${original.protocol}//${original.host} timeout_ms=${attempt.remainingMs()}`);
154
+ const endpoint = await transport.connect({
155
+ host: original.hostname,
156
+ port: Number(original.port || (original.protocol === "https:" ? 443 : 80)),
157
+ servername: target.servername ?? original.hostname
158
+ });
159
+ attempt.signal.throwIfAborted();
160
+ const url = new URL(original);
161
+ url.hostname = endpoint.host;
162
+ 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;
170
+ }
171
+ },
172
+ close: (engine) => engine.close(),
173
+ retryReason: openSearchRetryReason
174
+ });
175
+ } catch (error) {
176
+ this.lifecycle.onDiagnostic?.(`OpenSearch connect failed ${describeOpenSearchError(error)}`);
177
+ this.signal.throwIfAborted();
178
+ if (error instanceof ToolboxError && error.kind === "timeout")
179
+ throw error;
180
+ const reason = openSearchRetryReason(error);
181
+ if (reason === undefined)
182
+ throw error;
183
+ throw new ToolboxError(`OpenSearch connect failed (${reason})`, { kind: "connection_failed", code: reason, cause: error });
184
+ }
185
+ })();
186
+ }
187
+ #ready() {
188
+ this.signal.throwIfAborted();
189
+ if (!this.#engine)
190
+ throw new Error("OpenSearch client is not initialized");
191
+ return this.#engine;
192
+ }
193
+ #run(operation) {
194
+ const engine = this.#ready();
195
+ const pending = operation(engine);
196
+ this.#operations.add(pending);
197
+ pending.then(() => this.#operations.delete(pending), () => this.#operations.delete(pending));
198
+ return pending;
199
+ }
200
+ count(index, query) {
201
+ return this.#run((engine) => engine.count(index, query));
202
+ }
203
+ search(index, body) {
204
+ return this.#run((engine) => engine.search(index, body));
205
+ }
206
+ request(path, query) {
207
+ return this.#run((engine) => engine.request(path, query));
208
+ }
209
+ ping() {
210
+ return this.#run((engine) => engine.ping());
211
+ }
212
+ close() {
213
+ return this.dispose();
214
+ }
215
+ dispose() {
216
+ return this.#disposal ??= (async () => {
217
+ this.#controller.abort(new Error("OpenSearch client disposed"));
218
+ await this.#initialization?.catch(() => {});
219
+ await Promise.allSettled(this.#operations);
220
+ await this.#engine?.close();
221
+ })();
222
+ }
223
+ }
224
+ async function openOpenSearch(source, lifecycle = {}) {
225
+ const client = new OpenSearchClient(source, lifecycle);
226
+ try {
227
+ await client.initialize();
228
+ } catch (error) {
229
+ await client.dispose();
230
+ throw error;
231
+ }
232
+ lifecycle.onDispose?.(() => client.dispose());
233
+ return client;
234
+ }
235
+
236
+ export { describeOpenSearchError, isOpenSearchReadApi, OpenSearchEngine, OpenSearchClient, openOpenSearch };
@@ -1,12 +1,13 @@
1
1
  import {
2
2
  buildKubectlArgs
3
- } from "./chunk-hmah9t8e.js";
3
+ } from "./chunk-rq5fc007.js";
4
4
  import {
5
5
  spawnProcess
6
- } from "./chunk-mn6dwsvh.js";
6
+ } from "./chunk-9shz0bw1.js";
7
7
 
8
8
  // src/kubernetes/port-forward.ts
9
9
  async function startPortForward(opts) {
10
+ opts.signal?.throwIfAborted();
10
11
  const target = opts.target ?? (opts.service ? { kind: "service", name: opts.service } : undefined);
11
12
  if (!target)
12
13
  return { ok: false, reason: "port-forward 缺少 Service/Pod 目标" };
@@ -24,8 +25,51 @@ async function startPortForward(opts) {
24
25
  return { ok: false, reason: err instanceof Error ? err.message : String(err) };
25
26
  }
26
27
  const timeoutMs = opts.timeoutMs ?? 15000;
27
- const stderrPromise = new Response(proc.stderr).text();
28
+ const log = opts.onDiagnostic ?? (() => {});
29
+ const started = Date.now();
30
+ log(`port-forward start target=${target.kind}/${target.name} namespace=${opts.namespace ?? "default"} remote_port=${opts.remotePort} timeout_ms=${timeoutMs}`);
31
+ let stderr = "";
32
+ let loggedBytes = 0;
33
+ const stderrReader = proc.stderr.getReader();
34
+ const stderrDone = (async () => {
35
+ const decoder2 = new TextDecoder;
36
+ for (;; ) {
37
+ const chunk = await stderrReader.read();
38
+ if (chunk.done)
39
+ break;
40
+ const text = decoder2.decode(chunk.value, { stream: true }).replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/g, "$1[redacted]@").replace(/([?&](?:token|password|secret|key)=)[^&\s]+/gi, "$1[redacted]").replace(/((?:authorization|password|token)[:=]\s*)[^\r\n]+/gi, "$1[redacted]");
41
+ stderr = (stderr + text).slice(-8192);
42
+ if (loggedBytes < 8192) {
43
+ const diagnostic = text.slice(0, 8192 - loggedBytes);
44
+ loggedBytes += diagnostic.length;
45
+ log(`port-forward stderr: ${diagnostic.trim()}`);
46
+ }
47
+ }
48
+ })().catch(() => {});
49
+ let stopped = false;
50
+ let cancelWait;
51
+ const cancelled = new Promise((resolve) => {
52
+ cancelWait = () => resolve("cancelled");
53
+ });
54
+ const abort = () => {
55
+ log(`port-forward cancelled elapsed_ms=${Date.now() - started}`);
56
+ stop();
57
+ cancelWait();
58
+ };
59
+ const stop = () => {
60
+ if (stopped)
61
+ return;
62
+ stopped = true;
63
+ opts.signal?.removeEventListener("abort", abort);
64
+ proc.kill("SIGKILL");
65
+ stderrReader.cancel().catch(() => {});
66
+ reader.cancel().catch(() => {});
67
+ };
28
68
  const reader = proc.stdout.getReader();
69
+ opts.signal?.addEventListener("abort", abort, { once: true });
70
+ if (opts.signal?.aborted)
71
+ abort();
72
+ proc.exited.then(() => opts.signal?.removeEventListener("abort", abort), () => opts.signal?.removeEventListener("abort", abort));
29
73
  const decoder = new TextDecoder;
30
74
  const deadline = Date.now() + timeoutMs;
31
75
  let out = "";
@@ -36,6 +80,7 @@ async function startPortForward(opts) {
36
80
  let timer;
37
81
  const chunk = await Promise.race([
38
82
  reader.read(),
83
+ cancelled,
39
84
  new Promise((resolve) => {
40
85
  timer = setTimeout(() => resolve("timeout"), remain);
41
86
  }),
@@ -43,10 +88,13 @@ async function startPortForward(opts) {
43
88
  ]).finally(() => {
44
89
  clearTimeout(timer);
45
90
  });
91
+ opts.signal?.throwIfAborted();
92
+ if (chunk === "cancelled")
93
+ throw opts.signal?.reason;
46
94
  if (chunk === "timeout")
47
95
  break;
48
96
  if (typeof chunk === "object" && "error" in chunk) {
49
- proc.kill();
97
+ stop();
50
98
  return { ok: false, reason: chunk.error instanceof Error ? chunk.error.message : String(chunk.error) };
51
99
  }
52
100
  if (chunk === "exited" || chunk.done)
@@ -54,16 +102,18 @@ async function startPortForward(opts) {
54
102
  out += decoder.decode(chunk.value);
55
103
  const m = out.match(/Forwarding from 127\.0\.0\.1:(\d+)/);
56
104
  if (m) {
105
+ log(`port-forward listening local=127.0.0.1:${m[1]} elapsed_ms=${Date.now() - started}`);
57
106
  return {
58
107
  ok: true,
59
- value: { target, localPort: Number(m[1]), command: argv, stop: () => proc.kill() }
108
+ value: { target, localPort: Number(m[1]), command: argv, stop }
60
109
  };
61
110
  }
62
111
  }
63
- proc.kill();
64
- const stderr = await stderrPromise.catch(() => "");
112
+ await Promise.resolve();
113
+ stop();
65
114
  const reason = stderr.trim().split(`
66
115
  `)[0] || `port-forward 未就绪(${timeoutMs}ms 超时)`;
116
+ log(`port-forward failed elapsed_ms=${Date.now() - started} reason=${reason}`);
67
117
  return { ok: false, reason };
68
118
  }
69
119
 
@@ -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,6 @@
1
1
  import {
2
2
  spawnProcess
3
- } from "./chunk-mn6dwsvh.js";
3
+ } from "./chunk-9shz0bw1.js";
4
4
 
5
5
  // src/kubernetes/executor.ts
6
6
  function buildKubectlArgs(opts, sub) {
@@ -5,13 +5,18 @@ import {
5
5
  import {
6
6
  queryReadonlySession,
7
7
  validateQueryLimits
8
- } from "./chunk-age65q4n.js";
8
+ } from "./chunk-db4an9td.js";
9
9
  import {
10
+ connectMysql,
10
11
  destroyMysqlConnection
11
- } from "./chunk-136egyff.js";
12
+ } from "./chunk-835fssgz.js";
13
+ import {
14
+ acquireConnection,
15
+ awaitWithSignal
16
+ } from "./chunk-gm4swndx.js";
12
17
  import {
13
18
  ToolboxError
14
- } from "./chunk-20kv4wc3.js";
19
+ } from "./chunk-rkt41za2.js";
15
20
  import {
16
21
  ConcurrencyPool
17
22
  } from "./chunk-v3hxvnrc.js";
@@ -20,7 +25,6 @@ import {
20
25
  } from "./chunk-h8fwx0k7.js";
21
26
 
22
27
  // src/mysql/client.ts
23
- import { createConnection } from "mysql2/promise";
24
28
  function mysqlOperationError(error, phase) {
25
29
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "PROTOCOL_CONNECTION_LOST")
26
30
  return error;
@@ -37,7 +41,7 @@ class MysqlDatabase {
37
41
  connect;
38
42
  #queries = new ConcurrencyPool(1);
39
43
  #connections = new Map;
40
- constructor(transports, options, connect = createConnection) {
44
+ constructor(transports, options, connect = connectMysql) {
41
45
  this.transports = transports;
42
46
  this.options = options;
43
47
  this.connect = connect;
@@ -120,39 +124,40 @@ class MysqlDatabase {
120
124
  return rows;
121
125
  });
122
126
  }
123
- async#open(target) {
124
- let reason;
125
- for (let i = 0;i < this.transports.length; i++) {
126
- const transport = this.transports[i];
127
- this.options.signal?.throwIfAborted();
128
- if (transport.kind === "python") {
129
- this.options.onRoute?.({ transport: transport.name, reason });
130
- return { kind: "python", transport };
131
- }
132
- try {
133
- const endpoint = await transport.connect(target);
134
- const connection = await this.connect({
135
- host: endpoint.host,
136
- port: endpoint.port,
137
- user: target.user,
138
- password: target.password,
139
- database: target.database,
140
- connectTimeout: this.options.connectTimeoutMs,
141
- dateStrings: true,
142
- supportBigNumbers: true,
143
- bigNumberStrings: true
144
- });
145
- this.options.onRoute?.({ transport: endpoint.host === target.host && endpoint.port === target.port ? "direct" : transport.name, reason });
146
- return { kind: "tcp", connection };
147
- } catch (error) {
148
- const failure = mysqlOperationError(error, "connect");
149
- const connectionLost = failure instanceof ToolboxError && failure.kind === "connection_lost";
150
- if (!isConnectionNetworkError(failure) && !connectionLost || i === this.transports.length - 1)
151
- throw failure;
152
- reason = String(failure.code);
153
- }
154
- }
155
- throw new Error("MySQL DataSource has no transport");
127
+ #open(target) {
128
+ return acquireConnection(this.transports, {
129
+ name: "MySQL connection",
130
+ ...this.options,
131
+ open: async (transport, attempt) => {
132
+ if (transport.kind === "python")
133
+ return { connection: { kind: "python", transport }, transport: transport.name };
134
+ try {
135
+ const endpoint = await transport.connect(target);
136
+ const connection = await this.connect({
137
+ host: endpoint.host,
138
+ port: endpoint.port,
139
+ user: target.user,
140
+ password: target.password,
141
+ database: target.database,
142
+ connectTimeout: attempt.remainingMs(),
143
+ dateStrings: true,
144
+ supportBigNumbers: true,
145
+ bigNumberStrings: true
146
+ }, attempt.signal);
147
+ return {
148
+ connection: { kind: "tcp", connection },
149
+ transport: endpoint.host === target.host && endpoint.port === target.port ? "direct" : transport.name
150
+ };
151
+ } catch (error) {
152
+ throw mysqlOperationError(error, "connect");
153
+ }
154
+ },
155
+ close: (session) => {
156
+ if (session.kind === "tcp")
157
+ destroyMysqlConnection(session.connection);
158
+ },
159
+ retryReason: (error) => isConnectionNetworkError(error) || error instanceof ToolboxError && error.kind === "connection_lost" ? String(error.code) : undefined
160
+ });
156
161
  }
157
162
  async queryOne(target, sql, values) {
158
163
  return (await this.query(target, sql, values))[0];
@@ -186,7 +191,7 @@ class MysqlClient {
186
191
  initialize() {
187
192
  this.signal.throwIfAborted();
188
193
  return this.#initialization ??= (async () => {
189
- this.#target = await this.source.resolve();
194
+ this.#target = await awaitWithSignal(this.source.resolve(), this.signal);
190
195
  this.signal.throwIfAborted();
191
196
  this.#database = new MysqlDatabase(this.source.transports, { ...this.options, signal: this.signal });
192
197
  await this.#database.initialize(this.#target);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  FileError,
3
3
  ToolboxError
4
- } from "./chunk-20kv4wc3.js";
4
+ } from "./chunk-rkt41za2.js";
5
5
  import {
6
6
  ConcurrencyPool
7
7
  } from "./chunk-v3hxvnrc.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  resourcePath
3
- } from "./chunk-ftkrjqkc.js";
3
+ } from "./chunk-dpcge1xt.js";
4
4
  import {
5
5
  clientKey
6
6
  } from "./chunk-a9htb6cr.js";
@@ -8,7 +8,7 @@ import {
8
8
  KubernetesError,
9
9
  ToolboxError,
10
10
  kubernetesHttpError
11
- } from "./chunk-20kv4wc3.js";
11
+ } from "./chunk-rkt41za2.js";
12
12
  import {
13
13
  ConcurrencyPool
14
14
  } from "./chunk-v3hxvnrc.js";
@@ -53,11 +53,13 @@ function findPodsForService(services, pods, serviceName, namespace) {
53
53
  return [];
54
54
  return pods.filter((pod) => pod.namespace === namespace && pod.phase === "Running" && selector.every(([name, value]) => pod.labels[name] === value)).sort((left, right) => left.name.localeCompare(right.name));
55
55
  }
56
- async function listServiceNetwork(executor, namespace) {
56
+ async function listServiceNetwork(executor, namespace, signal) {
57
+ signal?.throwIfAborted();
57
58
  const [serviceResult, podResult] = await Promise.all([
58
- executor.run(["get", "services", "-o", "json"], { timeoutMs: 20000 }),
59
- executor.run(["get", "pods", "-o", "json"], { timeoutMs: 20000 })
59
+ executor.run(["get", "services", "-o", "json"], { timeoutMs: 20000, signal }),
60
+ executor.run(["get", "pods", "-o", "json"], { timeoutMs: 20000, signal })
60
61
  ]);
62
+ signal?.throwIfAborted();
61
63
  if (!serviceResult.ok) {
62
64
  throw new Error(`读取 Service 失败:${serviceResult.stderr.trim() || `exit=${serviceResult.exitCode}`}`);
63
65
  }
@@ -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/compute-quantity.ts
6
6
  function object(value) {
@@ -0,0 +1,34 @@
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
+ export interface ConnectionAttempt {
5
+ readonly signal: AbortSignal;
6
+ /** Remaining mapping + handshake budget, not a fresh timeout for each phase. */
7
+ remainingMs(): number;
8
+ }
9
+ interface Acquisition<R, C> {
10
+ name: string;
11
+ connectTimeoutMs: number;
12
+ acquireTimeoutMs?: number;
13
+ signal?: AbortSignal;
14
+ open(route: R, attempt: ConnectionAttempt): Promise<{
15
+ connection: C;
16
+ transport: string;
17
+ }>;
18
+ close(connection: C): void | Promise<void>;
19
+ /** Undefined means terminal; authentication and protocol errors must not change route. */
20
+ retryReason(error: unknown): string | undefined;
21
+ onRoute?: ClientLifecycle["onRoute"];
22
+ }
23
+ /**
24
+ * @spec One acquisition owns ordered attempts and a deadline covering mapping and handshake.
25
+ * @spec Cancellation keeps its original reason; a late successful connection is closed, never published.
26
+ * @rule Only connection preparation enters this function. Commands and queries must never be replayed.
27
+ * @why Protocol adapters own partial handles and error classification; all protocols share the same lifecycle.
28
+ * @see {@link component://docs/toolbox.md}
29
+ * @case id=connection_mapping_cancel,desc=`Cancel while mapping a route`,expect=`Acquisition exits without a late handshake`
30
+ */
31
+ export declare function acquireConnection<R extends {
32
+ readonly name: string;
33
+ }, C>(routes: readonly R[], options: Acquisition<R, C>): Promise<C>;
34
+ export {};
@@ -0,0 +1,9 @@
1
+ import {
2
+ acquireConnection,
3
+ awaitWithSignal
4
+ } from "../chunk-gm4swndx.js";
5
+ import"../chunk-rkt41za2.js";
6
+ export {
7
+ awaitWithSignal,
8
+ acquireConnection
9
+ };
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  KubernetesError,
8
8
  ToolboxError
9
- } from "./chunk-20kv4wc3.js";
9
+ } from "./chunk-rkt41za2.js";
10
10
  export {
11
11
  clientKey,
12
12
  ToolboxError,
package/dist/errors.d.ts CHANGED
@@ -13,6 +13,10 @@ export declare class KubernetesError extends ToolboxError {
13
13
  }
14
14
  export declare class FileError extends ToolboxError {
15
15
  }
16
+ /** A local deadline expiry, distinct from a connection failure reported by a driver. */
17
+ export declare class TimeoutError extends ToolboxError {
18
+ constructor(message: string, options?: ErrorOptions);
19
+ }
16
20
  export declare function kubernetesHttpError(status: number): KubernetesError;
17
21
  /**
18
22
  * Normalize a failed kubectl resource request at the transport boundary.
package/dist/errors.js CHANGED
@@ -1,14 +1,16 @@
1
1
  import {
2
2
  FileError,
3
3
  KubernetesError,
4
+ TimeoutError,
4
5
  ToolboxError,
5
6
  kubernetesCommandError,
6
7
  kubernetesHttpError
7
- } from "./chunk-20kv4wc3.js";
8
+ } from "./chunk-rkt41za2.js";
8
9
  export {
9
10
  kubernetesHttpError,
10
11
  kubernetesCommandError,
11
12
  ToolboxError,
13
+ TimeoutError,
12
14
  KubernetesError,
13
15
  FileError
14
16
  };
@@ -2,8 +2,8 @@ import {
2
2
  BoundedFileClient,
3
3
  fileClientOptions,
4
4
  filePaths
5
- } from "../chunk-kkts16w9.js";
6
- import"../chunk-20kv4wc3.js";
5
+ } from "../chunk-ww11q58v.js";
6
+ import"../chunk-rkt41za2.js";
7
7
  import"../chunk-v3hxvnrc.js";
8
8
  export {
9
9
  filePaths,