@compforge/harness-toolbox 0.3.9 → 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.
@@ -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() {
@@ -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.9",
3
+ "version": "0.3.10",
4
4
  "description": "Shared infrastructure clients, transports and bounded data capture",
5
5
  "type": "module",
6
6
  "scripts": {