@compforge/harness-toolbox 0.3.8 → 0.3.10

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.
package/README.md CHANGED
@@ -250,3 +250,13 @@ Disposal closes forwards and the exec stream; EOF closes remote listeners/connec
250
250
  connection is lost without EOF, a 30-second heartbeat lease bounds remote process lifetime.
251
251
  A dead relay invalidates its routes and fails callers; no SQL or other protocol operation is replayed.
252
252
  The root should finish or cancel protocol work before disposing the transport.
253
+
254
+ ### OpenSearch connection preparation
255
+
256
+ `openOpenSearch` probes the resolved endpoint before returning. It tries the supplied TCP transports
257
+ in order only for connection failures; HTTP authorization/server errors stop preparation, and failed
258
+ reads are never replayed on another route. `onRoute` reports the selected transport and safe fallback
259
+ reason. The caller owns shared transports such as `PodRelayTransport` and their cleanup.
260
+
261
+ Routed HTTPS preserves the original TLS server name. Bun disables HTTP keep-alive because its reused
262
+ socket recovery can replay a failed request below the driver; Node retains connection pooling.
@@ -9,6 +9,9 @@ import {
9
9
  queryMysqlViaPod,
10
10
  queryMysqlViaPodBatch
11
11
  } from "./chunk-m91t44jh.js";
12
+ import {
13
+ ToolboxError
14
+ } from "./chunk-zhkrydeg.js";
12
15
  import {
13
16
  ConcurrencyPool
14
17
  } from "./chunk-v3hxvnrc.js";
@@ -18,6 +21,16 @@ import {
18
21
 
19
22
  // src/mysql/client.ts
20
23
  import { createConnection } from "mysql2/promise";
24
+ function mysqlOperationError(error, phase) {
25
+ if (!error || typeof error !== "object" || !("code" in error) || error.code !== "PROTOCOL_CONNECTION_LOST")
26
+ return error;
27
+ return new ToolboxError(`MySQL ${phase} failed: connection lost (PROTOCOL_CONNECTION_LOST)`, {
28
+ kind: "connection_lost",
29
+ code: error.code,
30
+ cause: error
31
+ });
32
+ }
33
+
21
34
  class MysqlDatabase {
22
35
  transports;
23
36
  options;
@@ -56,6 +69,8 @@ class MysqlDatabase {
56
69
  throw new Error("Bounded readonly MySQL queries require a native TCP transport");
57
70
  try {
58
71
  return await queryReadonlySession(session.connection, sql, values, limits, this.options.signal);
72
+ } catch (error) {
73
+ throw mysqlOperationError(error, "query");
59
74
  } finally {
60
75
  destroyMysqlConnection(session.connection);
61
76
  }
@@ -94,7 +109,7 @@ class MysqlDatabase {
94
109
  this.#connections.delete(key);
95
110
  if (session?.kind === "tcp")
96
111
  destroyMysqlConnection(session.connection);
97
- throw error;
112
+ throw session ? mysqlOperationError(error, "query") : error;
98
113
  }
99
114
  }
100
115
  async#query(target, sql, values) {
@@ -130,9 +145,11 @@ class MysqlDatabase {
130
145
  this.options.onRoute?.({ transport: endpoint.host === target.host && endpoint.port === target.port ? "direct" : transport.name, reason });
131
146
  return { kind: "tcp", connection };
132
147
  } catch (error) {
133
- if (!isConnectionNetworkError(error) || i === this.transports.length - 1)
134
- throw error;
135
- reason = String(error.code);
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);
136
153
  }
137
154
  }
138
155
  throw new Error("MySQL DataSource has no transport");
@@ -1,15 +1,30 @@
1
+ import {
2
+ ToolboxError
3
+ } from "./chunk-zhkrydeg.js";
4
+ import {
5
+ isConnectionNetworkError
6
+ } from "./chunk-h8fwx0k7.js";
7
+
1
8
  // src/opensearch/client.ts
2
- import { Client } from "@opensearch-project/opensearch";
9
+ import { Client, Connection, errors } from "@opensearch-project/opensearch";
3
10
  function isOpenSearchReadApi(search) {
4
11
  return typeof search.request === "function";
5
12
  }
13
+
14
+ class OpenSearchConnection extends Connection {
15
+ buildRequestObject(params) {
16
+ return { ...super.buildRequestObject(params), servername: this.ssl?.servername };
17
+ }
18
+ }
6
19
  function createClient(options) {
7
20
  return new Client({
8
21
  node: options.node,
22
+ Connection: OpenSearchConnection,
9
23
  auth: options.auth.username && options.auth.password ? { username: options.auth.username, password: options.auth.password } : undefined,
10
- ssl: { rejectUnauthorized: false },
24
+ ssl: { rejectUnauthorized: false, servername: options.servername },
11
25
  requestTimeout: options.requestTimeoutMs ?? 60000,
12
- maxRetries: 0
26
+ maxRetries: 0,
27
+ agent: { keepAlive: !process.versions.bun }
13
28
  });
14
29
  }
15
30
 
@@ -36,8 +51,18 @@ class OpenSearchEngine {
36
51
  });
37
52
  return response.body;
38
53
  }
39
- ping() {
40
- return this.client.ping();
54
+ async ping(signal, timeoutMs) {
55
+ signal?.throwIfAborted();
56
+ const request = this.client.ping({}, timeoutMs === undefined ? undefined : { requestTimeout: timeoutMs });
57
+ const abort = () => {
58
+ request.abort?.();
59
+ };
60
+ signal?.addEventListener("abort", abort, { once: true });
61
+ try {
62
+ return await request;
63
+ } finally {
64
+ signal?.removeEventListener("abort", abort);
65
+ }
41
66
  }
42
67
  close() {
43
68
  return this.client.close();
@@ -46,6 +71,7 @@ class OpenSearchEngine {
46
71
 
47
72
  class OpenSearchClient {
48
73
  source;
74
+ lifecycle;
49
75
  #controller = new AbortController;
50
76
  signal;
51
77
  #initialization;
@@ -54,6 +80,7 @@ class OpenSearchClient {
54
80
  #operations = new Set;
55
81
  constructor(source, lifecycle = {}) {
56
82
  this.source = source;
83
+ this.lifecycle = lifecycle;
57
84
  this.signal = lifecycle.signal ? AbortSignal.any([lifecycle.signal, this.#controller.signal]) : this.#controller.signal;
58
85
  }
59
86
  initialize() {
@@ -61,15 +88,42 @@ class OpenSearchClient {
61
88
  return this.#initialization ??= (async () => {
62
89
  const target = await this.source.resolve();
63
90
  this.signal.throwIfAborted();
64
- const transport = this.source.transports[0];
65
- if (!transport || transport.kind !== "tcp")
66
- throw new Error("OpenSearch requires a TCP transport");
67
- const url = new URL(target.node);
68
- const endpoint = await transport.connect({ host: url.hostname, port: Number(url.port || (url.protocol === "https:" ? 443 : 80)) });
69
- this.signal.throwIfAborted();
70
- url.hostname = endpoint.host;
71
- url.port = String(endpoint.port);
72
- this.#engine = new OpenSearchEngine({ ...target, node: url.toString() });
91
+ const original = new URL(target.node);
92
+ let reason;
93
+ for (const [index, transport] of this.source.transports.entries()) {
94
+ 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
+ }
125
+ }
126
+ throw new Error("OpenSearch DataSource has no transport");
73
127
  })();
74
128
  }
75
129
  #ready() {
@@ -8,7 +8,11 @@ export interface MysqlDatabaseOptions extends ClientLifecycle {
8
8
  queryTimeoutMs: number;
9
9
  }
10
10
  type ConnectionFactory = (options: ConnectionOptions) => Promise<Connection>;
11
- /** SQL is executed once; only native connection establishment may advance to another transport. */
11
+ /**
12
+ * @spec SQL is executed once; only native connection establishment may advance to another transport.
13
+ * @why mysql2 reports a socket closed during handshake as PROTOCOL_CONNECTION_LOST, before any SQL is sent.
14
+ * @case id=mysql_handshake_closed,desc=`Peer closes during handshake`,expect=`Try the next transport`,forbid=`Replay an interrupted query`
15
+ */
12
16
  export declare class MysqlDatabase implements Database {
13
17
  #private;
14
18
  private readonly transports;
@@ -2,10 +2,11 @@ import {
2
2
  MysqlClient,
3
3
  MysqlDatabase,
4
4
  openMysql
5
- } from "../chunk-qqntng20.js";
5
+ } from "../chunk-ahc00qs5.js";
6
6
  import"../chunk-age65q4n.js";
7
7
  import"../chunk-136egyff.js";
8
8
  import"../chunk-m91t44jh.js";
9
+ import"../chunk-zhkrydeg.js";
9
10
  import"../chunk-v3hxvnrc.js";
10
11
  import"../chunk-h8fwx0k7.js";
11
12
  import"../chunk-wv4jwhvj.js";
@@ -14,10 +14,11 @@ import {
14
14
  MysqlClient,
15
15
  MysqlDatabase,
16
16
  openMysql
17
- } from "../chunk-qqntng20.js";
17
+ } from "../chunk-ahc00qs5.js";
18
18
  import"../chunk-age65q4n.js";
19
19
  import"../chunk-136egyff.js";
20
20
  import"../chunk-m91t44jh.js";
21
+ import"../chunk-zhkrydeg.js";
21
22
  import"../chunk-v3hxvnrc.js";
22
23
  import"../chunk-h8fwx0k7.js";
23
24
  import"../chunk-wv4jwhvj.js";
@@ -9,6 +9,10 @@ export interface OpenSearchOptions {
9
9
  node: string;
10
10
  auth: OpenSearchAuth;
11
11
  requestTimeoutMs?: number;
12
+ /** Deadline for the initialization probe; query deadlines remain independent. */
13
+ connectTimeoutMs?: number;
14
+ /** Original TLS identity when a transport maps the socket to localhost. */
15
+ servername?: string;
12
16
  }
13
17
  export interface OpenSearchClientApi {
14
18
  count(params: Record<string, unknown>): Promise<{
@@ -19,7 +23,11 @@ export interface OpenSearchClientApi {
19
23
  search(params: Record<string, unknown>): Promise<{
20
24
  body: SearchResult;
21
25
  }>;
22
- ping(params?: Record<string, unknown>): Promise<unknown>;
26
+ ping(params?: Record<string, unknown>, options?: {
27
+ requestTimeout?: number;
28
+ }): Promise<unknown> & {
29
+ abort?(): void;
30
+ };
23
31
  transport?: {
24
32
  request(params: {
25
33
  method: string;
@@ -43,12 +51,18 @@ export declare class OpenSearchEngine implements OpenSearchReadApi {
43
51
  count(index: string, query: SearchQuery): Promise<number>;
44
52
  search(index: string, body: SearchQuery): Promise<SearchResult>;
45
53
  request(path: string, query?: SearchQuery): Promise<unknown>;
46
- ping(): Promise<unknown>;
54
+ ping(signal?: AbortSignal, timeoutMs?: number): Promise<unknown>;
47
55
  close(): Promise<void>;
48
56
  }
57
+ /**
58
+ * @spec Only connection preparation may advance to another Transport; reads are never replayed.
59
+ * @why The official client connects lazily and wraps socket errors, so creating it does not prove reachability.
60
+ * @case id=opensearch_connect_fallback,desc=`Host cannot reach target`,expect=`Probe next TCP route`,forbid=`Replay a failed search`
61
+ */
49
62
  export declare class OpenSearchClient implements ManagedClient, OpenSearchReadApi {
50
63
  #private;
51
64
  private readonly source;
65
+ private readonly lifecycle;
52
66
  readonly signal: AbortSignal;
53
67
  constructor(source: ConnectionSource<OpenSearchOptions>, lifecycle?: ClientLifecycle);
54
68
  initialize(): Promise<void>;
@@ -3,7 +3,14 @@ import {
3
3
  OpenSearchEngine,
4
4
  isOpenSearchReadApi,
5
5
  openOpenSearch
6
- } from "../chunk-w3a4p6jm.js";
6
+ } from "../chunk-b7f39m4v.js";
7
+ import"../chunk-zhkrydeg.js";
8
+ import"../chunk-h8fwx0k7.js";
9
+ import"../chunk-wv4jwhvj.js";
10
+ import"../chunk-631vce5z.js";
11
+ import"../chunk-hmah9t8e.js";
12
+ import"../chunk-mn6dwsvh.js";
13
+ import"../chunk-ssj93brx.js";
7
14
  export {
8
15
  openOpenSearch,
9
16
  isOpenSearchReadApi,
@@ -3,7 +3,7 @@ import {
3
3
  OpenSearchEngine,
4
4
  isOpenSearchReadApi,
5
5
  openOpenSearch
6
- } from "../chunk-w3a4p6jm.js";
6
+ } from "../chunk-b7f39m4v.js";
7
7
  import {
8
8
  searchAfterPages
9
9
  } from "../chunk-99p0zt4f.js";
@@ -11,6 +11,13 @@ import {
11
11
  parseOpenSearchEndpoint
12
12
  } from "../chunk-s1sjyem7.js";
13
13
  import"../chunk-djk4kmxb.js";
14
+ import"../chunk-zhkrydeg.js";
15
+ import"../chunk-h8fwx0k7.js";
16
+ import"../chunk-wv4jwhvj.js";
17
+ import"../chunk-631vce5z.js";
18
+ import"../chunk-hmah9t8e.js";
19
+ import"../chunk-mn6dwsvh.js";
20
+ import"../chunk-ssj93brx.js";
14
21
  export {
15
22
  searchAfterPages,
16
23
  parseOpenSearchEndpoint,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compforge/harness-toolbox",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Shared infrastructure clients, transports and bounded data capture",
5
5
  "type": "module",
6
6
  "scripts": {