@compforge/harness-toolbox 0.3.2 → 0.3.4
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 +90 -0
- package/dist/chunk-1gyshqd9.js +20 -0
- package/dist/{chunk-0cq1gx86.js → chunk-35cjec9z.js} +1 -1
- package/dist/chunk-4sqvppz7.js +51 -0
- package/dist/{chunk-ssdxknsk.js → chunk-56t4qyp7.js} +5 -5
- package/dist/chunk-5fgbhx2r.js +253 -0
- package/dist/{chunk-0rs1aacd.js → chunk-hmah9t8e.js} +1 -1
- package/dist/{chunk-ztenxgjv.js → chunk-mn6dwsvh.js} +8 -1
- package/dist/{chunk-205a4vma.js → chunk-qhpqse1y.js} +3 -3
- package/dist/chunk-ssj93brx.js +89 -0
- package/dist/{chunk-4pdwsfcx.js → chunk-td11zjfb.js} +8 -8
- package/dist/{chunk-hv2kx47y.js → chunk-xh3zwhjh.js} +2 -2
- package/dist/data-loader.d.ts +21 -0
- package/dist/data-loader.js +91 -0
- package/dist/kubernetes/client.js +8 -8
- package/dist/kubernetes/environment.js +8 -8
- package/dist/kubernetes/executor.d.ts +1 -1
- package/dist/kubernetes/executor.js +2 -2
- package/dist/kubernetes/pod-log-datasource.js +1 -1
- package/dist/kubernetes/port-forward.js +3 -3
- package/dist/kubernetes/resources.js +2 -2
- package/dist/kubernetes/service-port-forward.js +4 -4
- package/dist/mysql/client.js +8 -3
- package/dist/mysql/index.d.ts +2 -0
- package/dist/mysql/index.js +23 -6
- package/dist/mysql/limited.d.ts +13 -0
- package/dist/mysql/limited.js +10 -0
- package/dist/mysql/scoped.d.ts +7 -0
- package/dist/mysql/scoped.js +6 -0
- package/dist/mysql/types.d.ts +2 -0
- package/dist/process/index.d.ts +1 -1
- package/dist/process/index.js +1 -1
- package/dist/redis/index.js +4 -4
- package/dist/s3/datasource.js +8 -3
- package/dist/s3/index.js +8 -3
- package/dist/transport/index.d.ts +1 -0
- package/dist/transport/index.js +9 -1
- package/dist/transport/pod-relay-script.d.ts +2 -0
- package/dist/transport/pod-relay-script.js +6 -0
- package/dist/transport/pod-relay.d.ts +31 -0
- package/dist/transport/pod-relay.js +10 -0
- package/package.json +5 -1
- package/dist/{chunk-1kpbvat9.js → chunk-2m0mh09h.js} +3 -3
- /package/dist/{chunk-xy4g56g4.js → chunk-h8fwx0k7.js} +0 -0
package/README.md
CHANGED
|
@@ -156,3 +156,93 @@ is not an arbitrary-SQL sandbox. This API requires MySQL 5.7.8+ `max_execution_t
|
|
|
156
156
|
fall back to Pod Python. Completion, truncation, timeout, and cancellation destroy the disposable
|
|
157
157
|
session without replay or changes to ordinary borrowed sessions. Disconnect does not prove immediate
|
|
158
158
|
server cancellation. The byte budget limits retained JSON rows, not individual wire packets or fields.
|
|
159
|
+
|
|
160
|
+
## Reusable bounded queries and read caches
|
|
161
|
+
|
|
162
|
+
For trusted, caller-owned SELECT statements, `queryLimited(database, target, statement, maxRows)`
|
|
163
|
+
appends an integer `LIMIT maxRows + 1` and returns `{ rows, truncated }`. SQL must not already have
|
|
164
|
+
LIMIT or a trailing terminator; the helper does not parse or rewrite arbitrary SQL. Business values
|
|
165
|
+
stay bound parameters. `limitedStatement` and `limitedRows` expose the same preparation/projection
|
|
166
|
+
for `Database.queryBatch`. `maxRows` must be a positive integer below 2147483647. These helpers
|
|
167
|
+
bound row count only: use `queryReadonly` when a disposable readonly session and byte/time budgets
|
|
168
|
+
are required.
|
|
169
|
+
|
|
170
|
+
`DataLoader` shares results, failures and in-flight work within one explicit read scope, similar to
|
|
171
|
+
Guava's LoadingCache but scoped to a single collection round. Consumers must not mutate shared results.
|
|
172
|
+
Use `withReadScope` to bind caller-approved database reads; do not use it for writes or polling.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { DataLoader } from '@compforge/harness-toolbox/data-loader';
|
|
176
|
+
import { withReadScope, queryLimited } from '@compforge/harness-toolbox/mysql';
|
|
177
|
+
|
|
178
|
+
const scope = new DataLoader({
|
|
179
|
+
maxEntries: 256, maxBytes: 4 * 1024 * 1024, signal: client.signal,
|
|
180
|
+
});
|
|
181
|
+
const reads = withReadScope(client.database, scope);
|
|
182
|
+
try {
|
|
183
|
+
const page = await queryLimited(reads, client.target, {
|
|
184
|
+
sql: 'SELECT id FROM items WHERE owner = ? ORDER BY id', values: [owner],
|
|
185
|
+
}, 100);
|
|
186
|
+
} finally {
|
|
187
|
+
await scope.close(); // Root still owns client.dispose().
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Keys include full database identity, SQL and typed parameters as an opaque digest. Nonserializable
|
|
192
|
+
parameters bypass caching. Batch reads reuse cached entries and send misses as one ordered native
|
|
193
|
+
batch, or sequential queries when batching is unavailable. Failures are shared without replay;
|
|
194
|
+
use a new scope for fresh observations. This is not a consistent database snapshot.
|
|
195
|
+
|
|
196
|
+
Entry capacity includes failures and pending reads; new keys bypass caching when full. Serialized
|
|
197
|
+
result bytes bound retained results, excluding keys/errors and active operation memory. Oversized
|
|
198
|
+
or nonserializable results are returned but not retained. Scope close signals cancellation and joins
|
|
199
|
+
operations, including uncached reads. Operations must honor the signal for prompt cancellation;
|
|
200
|
+
MySQL reads retain their underlying client's timeout/cancellation policy. Adapter close owns neither
|
|
201
|
+
the scope nor the database. A waiter's optional signal to `scope.read` cancels only that waiter.
|
|
202
|
+
|
|
203
|
+
## Shared Pod relay transport
|
|
204
|
+
|
|
205
|
+
`PodRelayTransport` opens TCP routes through an existing Running/Ready Pod. It discovers an
|
|
206
|
+
eligible container in the selected namespace (optionally restricted by a label selector); the
|
|
207
|
+
current implementation uses Python 3.8+ standard-library asyncio. Callers supply endpoints,
|
|
208
|
+
not a Pod name or interpreter. It creates no Pods and installs no packages. The selected Pod
|
|
209
|
+
must be able to resolve and reach each destination.
|
|
210
|
+
|
|
211
|
+
Create one transport in the root execution's ClientManager and lend it to all Service/protocol
|
|
212
|
+
clients. Its key describes cluster access and relay policy, never a Service or database identity.
|
|
213
|
+
Concurrent callers share Pod discovery, one relay process and one forward per host/port. Database
|
|
214
|
+
credentials and connections remain owned by their protocol clients.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import { ClientManager } from '@compforge/harness-toolbox';
|
|
218
|
+
import { PodRelayTransport } from '@compforge/harness-toolbox/transport';
|
|
219
|
+
|
|
220
|
+
const clients = new ClientManager();
|
|
221
|
+
try {
|
|
222
|
+
const relay = await clients.get({
|
|
223
|
+
clientKey: 'transport:customer-cluster:app',
|
|
224
|
+
createClient: (_clients, signal) => new PodRelayTransport({
|
|
225
|
+
kubeconfig: '/config/cluster', context: 'customer', namespace: 'app',
|
|
226
|
+
selector: 'diagnostic-relay=eligible',
|
|
227
|
+
startupTimeoutMs: 5000, connectTimeoutMs: 5000,
|
|
228
|
+
maxConnections: 16, maxTargets: 8, maxCandidatePods: 8, signal,
|
|
229
|
+
}),
|
|
230
|
+
});
|
|
231
|
+
// The same instance belongs in each protocol client's ConnectionSource.transports.
|
|
232
|
+
const address = await relay.connect({host: 'database.internal', port: 3306});
|
|
233
|
+
// Connect the native protocol client to address; do not dispose relay in a Service.
|
|
234
|
+
} finally {
|
|
235
|
+
await clients.dispose();
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The host must authorize `list pods`, `create pods/exec` and `create pods/portforward`, including
|
|
240
|
+
running a temporary process in the selected container. List/discovery and each readiness operation
|
|
241
|
+
use startupTimeoutMs; candidate Pod count, target count and aggregate live relay connections are
|
|
242
|
+
bounded separately. List pods is namespace scoped. Relay listeners bind only to Pod loopback and
|
|
243
|
+
local forwards only to host loopback. Control messages contain destinations, never DB credentials.
|
|
244
|
+
|
|
245
|
+
Disposal closes forwards and the exec stream; EOF closes remote listeners/connections. If an exec
|
|
246
|
+
connection is lost without EOF, a 30-second heartbeat lease bounds remote process lifetime.
|
|
247
|
+
A dead relay invalidates its routes and fails callers; no SQL or other protocol operation is replayed.
|
|
248
|
+
The root should finish or cancel protocol work before disposing the transport.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/mysql/limited.ts
|
|
2
|
+
function validateLimit(maxRows) {
|
|
3
|
+
if (!Number.isSafeInteger(maxRows) || maxRows < 1 || maxRows >= 2147483647) {
|
|
4
|
+
throw new Error("maxRows must be a positive integer < 2147483647");
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
function limitedStatement(statement, maxRows) {
|
|
8
|
+
validateLimit(maxRows);
|
|
9
|
+
return { sql: `${statement.sql} LIMIT ${maxRows + 1}`, values: statement.values };
|
|
10
|
+
}
|
|
11
|
+
function limitedRows(rows, maxRows) {
|
|
12
|
+
validateLimit(maxRows);
|
|
13
|
+
return { rows: rows.slice(0, maxRows), truncated: rows.length > maxRows };
|
|
14
|
+
}
|
|
15
|
+
async function queryLimited(database, target, statement, maxRows) {
|
|
16
|
+
const bounded = limitedStatement(statement, maxRows);
|
|
17
|
+
return limitedRows(await database.query(target, bounded.sql, bounded.values), maxRows);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { limitedStatement, limitedRows, queryLimited };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/mysql/scoped.ts
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { serialize } from "node:v8";
|
|
4
|
+
function queryKey(kind, target, statement) {
|
|
5
|
+
try {
|
|
6
|
+
return createHash("sha256").update(serialize([
|
|
7
|
+
kind,
|
|
8
|
+
target.host,
|
|
9
|
+
target.port,
|
|
10
|
+
target.database,
|
|
11
|
+
target.user,
|
|
12
|
+
target.password,
|
|
13
|
+
statement.sql,
|
|
14
|
+
statement.values
|
|
15
|
+
])).digest("hex");
|
|
16
|
+
} catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function withReadScope(database, scope) {
|
|
21
|
+
const query = (target, sql, values) => scope.read(queryKey("query", target, { sql, values }), () => database.query(target, sql, values));
|
|
22
|
+
return {
|
|
23
|
+
query,
|
|
24
|
+
queryOne: (target, sql, values) => scope.read(queryKey("one", target, { sql, values }), () => database.queryOne(target, sql, values)),
|
|
25
|
+
queryBatch: async (target, statements) => {
|
|
26
|
+
const misses = [];
|
|
27
|
+
let batch;
|
|
28
|
+
if (!statements.length)
|
|
29
|
+
return scope.read(undefined, async () => []);
|
|
30
|
+
return Promise.all(statements.map((statement) => scope.read(queryKey("query", target, statement), (signal) => {
|
|
31
|
+
const position = misses.length;
|
|
32
|
+
misses.push(statement);
|
|
33
|
+
batch ??= Promise.resolve().then(async () => {
|
|
34
|
+
signal.throwIfAborted();
|
|
35
|
+
if (database.queryBatch)
|
|
36
|
+
return database.queryBatch(target, misses);
|
|
37
|
+
const rows = [];
|
|
38
|
+
for (const item of misses) {
|
|
39
|
+
signal.throwIfAborted();
|
|
40
|
+
rows.push(await database.query(target, item.sql, item.values));
|
|
41
|
+
}
|
|
42
|
+
return rows;
|
|
43
|
+
});
|
|
44
|
+
return batch.then((rows) => rows[position]);
|
|
45
|
+
})));
|
|
46
|
+
},
|
|
47
|
+
close: async () => {}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { withReadScope };
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
queryReadonlySession,
|
|
3
|
-
validateQueryLimits
|
|
4
|
-
} from "./chunk-kbewz29a.js";
|
|
5
1
|
import {
|
|
6
2
|
queryMysqlViaPod,
|
|
7
3
|
queryMysqlViaPodBatch
|
|
8
4
|
} from "./chunk-m91t44jh.js";
|
|
5
|
+
import {
|
|
6
|
+
queryReadonlySession,
|
|
7
|
+
validateQueryLimits
|
|
8
|
+
} from "./chunk-kbewz29a.js";
|
|
9
9
|
import {
|
|
10
10
|
ConcurrencyPool
|
|
11
11
|
} from "./chunk-v3hxvnrc.js";
|
|
12
12
|
import {
|
|
13
13
|
isConnectionNetworkError
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-h8fwx0k7.js";
|
|
15
15
|
|
|
16
16
|
// src/mysql/client.ts
|
|
17
17
|
import { createConnection } from "mysql2/promise";
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import {
|
|
2
|
+
startPortForward
|
|
3
|
+
} from "./chunk-xh3zwhjh.js";
|
|
4
|
+
import {
|
|
5
|
+
KubectlExecutor
|
|
6
|
+
} from "./chunk-hmah9t8e.js";
|
|
7
|
+
import {
|
|
8
|
+
POD_RELAY_SCRIPT
|
|
9
|
+
} from "./chunk-ssj93brx.js";
|
|
10
|
+
|
|
11
|
+
// src/transport/pod-relay.ts
|
|
12
|
+
class PodRelayTransport {
|
|
13
|
+
options;
|
|
14
|
+
kind = "tcp";
|
|
15
|
+
name = "pod-relay";
|
|
16
|
+
#controller = new AbortController;
|
|
17
|
+
#signal;
|
|
18
|
+
#executor;
|
|
19
|
+
#startForward;
|
|
20
|
+
#targets = new Map;
|
|
21
|
+
#pending = new Map;
|
|
22
|
+
#forwards = [];
|
|
23
|
+
#startup;
|
|
24
|
+
#execution;
|
|
25
|
+
#input;
|
|
26
|
+
#heartbeat;
|
|
27
|
+
#failure;
|
|
28
|
+
#disposal;
|
|
29
|
+
#sequence = 0;
|
|
30
|
+
constructor(options, dependencies) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
for (const key of ["startupTimeoutMs", "connectTimeoutMs", "maxConnections", "maxTargets", "maxCandidatePods"]) {
|
|
33
|
+
if (!Number.isSafeInteger(options[key]) || options[key] <= 0)
|
|
34
|
+
throw new Error(`${key} must be a positive integer`);
|
|
35
|
+
}
|
|
36
|
+
this.#executor = dependencies?.executor ?? new KubectlExecutor(options);
|
|
37
|
+
this.#startForward = dependencies?.startForward ?? startPortForward;
|
|
38
|
+
this.#signal = options.signal ? AbortSignal.any([options.signal, this.#controller.signal]) : this.#controller.signal;
|
|
39
|
+
this.#signal.addEventListener("abort", this.#onAbort, { once: true });
|
|
40
|
+
}
|
|
41
|
+
async initialize() {
|
|
42
|
+
this.#check();
|
|
43
|
+
}
|
|
44
|
+
#check() {
|
|
45
|
+
this.#signal.throwIfAborted();
|
|
46
|
+
if (this.#failure)
|
|
47
|
+
throw this.#failure;
|
|
48
|
+
}
|
|
49
|
+
#onAbort = () => {
|
|
50
|
+
this.#fail(new Error("Pod relay aborted"));
|
|
51
|
+
};
|
|
52
|
+
#fail(error) {
|
|
53
|
+
this.#failure ??= error;
|
|
54
|
+
if (this.#heartbeat)
|
|
55
|
+
clearInterval(this.#heartbeat);
|
|
56
|
+
if (this.#input) {
|
|
57
|
+
try {
|
|
58
|
+
this.#input.close();
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
this.#input = undefined;
|
|
62
|
+
for (const pending of this.#pending.values())
|
|
63
|
+
pending.reject(this.#failure);
|
|
64
|
+
this.#pending.clear();
|
|
65
|
+
for (const forward of this.#forwards.splice(0))
|
|
66
|
+
forward.stop();
|
|
67
|
+
}
|
|
68
|
+
async#select() {
|
|
69
|
+
const command = ["get", "pods", "--field-selector=status.phase=Running", "-o", "json"];
|
|
70
|
+
if (this.options.selector)
|
|
71
|
+
command.push("-l", this.options.selector);
|
|
72
|
+
const listed = await this.#executor.run(command, { timeoutMs: this.options.startupTimeoutMs, signal: this.#signal });
|
|
73
|
+
if (!listed.ok)
|
|
74
|
+
throw new Error("Cannot discover an existing Pod for relay");
|
|
75
|
+
const pods = JSON.parse(listed.stdout).items.filter((pod) => pod.status?.phase === "Running" && !pod.metadata?.deletionTimestamp).sort((a, b) => (a.metadata?.name ?? "").localeCompare(b.metadata?.name ?? "")).slice(0, this.options.maxCandidatePods);
|
|
76
|
+
for (const pod of pods) {
|
|
77
|
+
if (!pod.metadata?.name)
|
|
78
|
+
continue;
|
|
79
|
+
for (const container of pod.status?.containerStatuses ?? []) {
|
|
80
|
+
if (!container.ready)
|
|
81
|
+
continue;
|
|
82
|
+
const target = { pod: pod.metadata.name, container: container.name };
|
|
83
|
+
for (const executable of ["python3", "python"]) {
|
|
84
|
+
this.#check();
|
|
85
|
+
const probe = await this.#executor.exec(target, [executable, "-c", "import asyncio,sys; assert sys.version_info >= (3, 8); print('relay-ready')"], { timeoutMs: this.options.startupTimeoutMs, signal: this.#signal });
|
|
86
|
+
if (probe.ok && probe.stdout.trim() === "relay-ready")
|
|
87
|
+
return { target, executable };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
throw new Error("No eligible existing Pod can run the TCP relay");
|
|
92
|
+
}
|
|
93
|
+
async#start() {
|
|
94
|
+
const pod = await this.#select();
|
|
95
|
+
this.#check();
|
|
96
|
+
const input = new ReadableStream({ start: (controller) => {
|
|
97
|
+
this.#input = controller;
|
|
98
|
+
} });
|
|
99
|
+
let ready;
|
|
100
|
+
let rejectReady;
|
|
101
|
+
const readiness = new Promise((resolve, reject) => {
|
|
102
|
+
ready = resolve;
|
|
103
|
+
rejectReady = reject;
|
|
104
|
+
});
|
|
105
|
+
let buffered = "";
|
|
106
|
+
const config = {
|
|
107
|
+
connectTimeoutMs: this.options.connectTimeoutMs,
|
|
108
|
+
maxConnections: this.options.maxConnections,
|
|
109
|
+
maxTargets: this.options.maxTargets,
|
|
110
|
+
leaseMs: 30000
|
|
111
|
+
};
|
|
112
|
+
this.#execution = this.#executor.exec(pod.target, [pod.executable, "-u", "-c", POD_RELAY_SCRIPT, JSON.stringify(config)], {
|
|
113
|
+
stdin: input,
|
|
114
|
+
signal: this.#signal,
|
|
115
|
+
timeoutMs: 2147483647,
|
|
116
|
+
collectStdout: false,
|
|
117
|
+
onStdout: (chunk) => {
|
|
118
|
+
buffered += chunk;
|
|
119
|
+
if (buffered.length > 65536) {
|
|
120
|
+
this.#fail(new Error("Pod relay control response exceeded limit"));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
while (buffered.includes(`
|
|
124
|
+
`)) {
|
|
125
|
+
const end = buffered.indexOf(`
|
|
126
|
+
`);
|
|
127
|
+
const line = buffered.slice(0, end);
|
|
128
|
+
buffered = buffered.slice(end + 1);
|
|
129
|
+
try {
|
|
130
|
+
const response = JSON.parse(line);
|
|
131
|
+
if (response.ready)
|
|
132
|
+
ready();
|
|
133
|
+
else if (response.id !== undefined) {
|
|
134
|
+
const pending = this.#pending.get(response.id);
|
|
135
|
+
this.#pending.delete(response.id);
|
|
136
|
+
if (Number.isInteger(response.port) && response.port > 0 && response.port <= 65535)
|
|
137
|
+
pending?.resolve(response.port);
|
|
138
|
+
else
|
|
139
|
+
pending?.reject(new Error("Pod relay listener unavailable"));
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
this.#fail(new Error("Invalid Pod relay control response"));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}).then(() => {
|
|
147
|
+
const error = new Error("Pod relay execution ended");
|
|
148
|
+
rejectReady(error);
|
|
149
|
+
this.#fail(error);
|
|
150
|
+
}, () => {
|
|
151
|
+
const error = new Error("Pod relay execution failed");
|
|
152
|
+
rejectReady(error);
|
|
153
|
+
this.#fail(error);
|
|
154
|
+
});
|
|
155
|
+
this.#heartbeat = setInterval(() => {
|
|
156
|
+
try {
|
|
157
|
+
this.#send({ heartbeat: true });
|
|
158
|
+
} catch {
|
|
159
|
+
this.#fail(new Error("Pod relay control channel closed"));
|
|
160
|
+
}
|
|
161
|
+
}, 5000);
|
|
162
|
+
const timer = setTimeout(() => rejectReady(new Error("Pod relay startup timed out")), this.options.startupTimeoutMs);
|
|
163
|
+
try {
|
|
164
|
+
await readiness;
|
|
165
|
+
this.#check();
|
|
166
|
+
return pod;
|
|
167
|
+
} finally {
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
#send(value) {
|
|
172
|
+
this.#check();
|
|
173
|
+
if (!this.#input)
|
|
174
|
+
throw new Error("Pod relay control channel unavailable");
|
|
175
|
+
this.#input.enqueue(new TextEncoder().encode(`${JSON.stringify(value)}
|
|
176
|
+
`));
|
|
177
|
+
}
|
|
178
|
+
connect(endpoint) {
|
|
179
|
+
this.#check();
|
|
180
|
+
if (!endpoint.host || !Number.isInteger(endpoint.port) || endpoint.port < 1 || endpoint.port > 65535) {
|
|
181
|
+
return Promise.reject(new Error("Invalid relay endpoint"));
|
|
182
|
+
}
|
|
183
|
+
const key = JSON.stringify([endpoint.host, endpoint.port]);
|
|
184
|
+
let pending = this.#targets.get(key);
|
|
185
|
+
if (!pending) {
|
|
186
|
+
if (this.#targets.size >= this.options.maxTargets)
|
|
187
|
+
return Promise.reject(new Error("Pod relay target limit reached"));
|
|
188
|
+
this.#startup ??= this.#start().catch((error) => {
|
|
189
|
+
this.#fail(error instanceof Error ? error : new Error("Pod relay startup failed"));
|
|
190
|
+
throw error;
|
|
191
|
+
});
|
|
192
|
+
pending = this.#connect(endpoint, this.#startup);
|
|
193
|
+
this.#targets.set(key, pending);
|
|
194
|
+
}
|
|
195
|
+
return pending.then((local) => ({ ...local, servername: endpoint.servername ?? endpoint.host }));
|
|
196
|
+
}
|
|
197
|
+
async#connect(endpoint, startup) {
|
|
198
|
+
const pod = await startup;
|
|
199
|
+
this.#check();
|
|
200
|
+
const id = ++this.#sequence;
|
|
201
|
+
const remotePort = await new Promise((resolve, reject) => {
|
|
202
|
+
const timer = setTimeout(() => {
|
|
203
|
+
this.#pending.delete(id);
|
|
204
|
+
reject(new Error("Pod relay listener timed out"));
|
|
205
|
+
}, this.options.startupTimeoutMs);
|
|
206
|
+
this.#pending.set(id, {
|
|
207
|
+
resolve: (value) => {
|
|
208
|
+
clearTimeout(timer);
|
|
209
|
+
resolve(value);
|
|
210
|
+
},
|
|
211
|
+
reject: (error) => {
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
reject(error);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
try {
|
|
217
|
+
this.#send({ id, host: endpoint.host, port: endpoint.port });
|
|
218
|
+
} catch (error) {
|
|
219
|
+
this.#pending.get(id)?.reject(error);
|
|
220
|
+
this.#pending.delete(id);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
this.#check();
|
|
224
|
+
const forwarded = await this.#startForward({
|
|
225
|
+
...this.options,
|
|
226
|
+
target: { kind: "pod", name: pod.target.pod },
|
|
227
|
+
remotePort,
|
|
228
|
+
timeoutMs: this.options.startupTimeoutMs
|
|
229
|
+
});
|
|
230
|
+
if (!forwarded.ok || !forwarded.value)
|
|
231
|
+
throw new Error("Pod relay port-forward unavailable");
|
|
232
|
+
try {
|
|
233
|
+
this.#check();
|
|
234
|
+
} catch (error) {
|
|
235
|
+
forwarded.value.stop();
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
this.#forwards.push(forwarded.value);
|
|
239
|
+
return { host: "127.0.0.1", port: forwarded.value.localPort };
|
|
240
|
+
}
|
|
241
|
+
dispose() {
|
|
242
|
+
return this.#disposal ??= (async () => {
|
|
243
|
+
this.#controller.abort(new Error("Pod relay disposed"));
|
|
244
|
+
this.#fail(new Error("Pod relay disposed"));
|
|
245
|
+
await Promise.allSettled(this.#targets.values());
|
|
246
|
+
await this.#execution;
|
|
247
|
+
this.#signal.removeEventListener("abort", this.#onAbort);
|
|
248
|
+
this.#targets.clear();
|
|
249
|
+
})();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export { PodRelayTransport };
|
|
@@ -8,7 +8,14 @@ function spawnProcess(argv, opts) {
|
|
|
8
8
|
stdio: "pipe",
|
|
9
9
|
env: opts?.env
|
|
10
10
|
});
|
|
11
|
-
|
|
11
|
+
if (opts?.stdin instanceof ReadableStream) {
|
|
12
|
+
const input = Readable.fromWeb(opts.stdin);
|
|
13
|
+
input.on("error", () => child.stdin.destroy());
|
|
14
|
+
child.stdin.on("error", () => input.destroy());
|
|
15
|
+
child.once("close", () => input.destroy());
|
|
16
|
+
input.pipe(child.stdin);
|
|
17
|
+
} else
|
|
18
|
+
child.stdin.end(opts?.stdin);
|
|
12
19
|
const exited = new Promise((resolve, reject) => {
|
|
13
20
|
child.once("error", reject);
|
|
14
21
|
child.once("close", (code) => resolve(code ?? 1));
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
PortForwardScope
|
|
3
|
-
} from "./chunk-hv2kx47y.js";
|
|
4
1
|
import {
|
|
5
2
|
findPodsForService,
|
|
6
3
|
findService,
|
|
@@ -9,6 +6,9 @@ import {
|
|
|
9
6
|
import {
|
|
10
7
|
findPod
|
|
11
8
|
} from "./chunk-6fnhzhdb.js";
|
|
9
|
+
import {
|
|
10
|
+
PortForwardScope
|
|
11
|
+
} from "./chunk-xh3zwhjh.js";
|
|
12
12
|
|
|
13
13
|
// src/kubernetes/service-port-forward.ts
|
|
14
14
|
class ServicePortForwarder {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/transport/pod-relay-script.ts
|
|
2
|
+
var POD_RELAY_SCRIPT = String.raw`
|
|
3
|
+
import asyncio, json, sys
|
|
4
|
+
|
|
5
|
+
async def main():
|
|
6
|
+
config = json.loads(sys.argv[1])
|
|
7
|
+
loop = asyncio.get_running_loop()
|
|
8
|
+
reader = asyncio.StreamReader(limit=4096)
|
|
9
|
+
await loop.connect_read_pipe(lambda: asyncio.StreamReaderProtocol(reader), sys.stdin.buffer)
|
|
10
|
+
servers = []
|
|
11
|
+
active = set()
|
|
12
|
+
|
|
13
|
+
async def pipe(source, destination):
|
|
14
|
+
while True:
|
|
15
|
+
chunk = await source.read(65536)
|
|
16
|
+
if not chunk:
|
|
17
|
+
if destination.can_write_eof():
|
|
18
|
+
destination.write_eof()
|
|
19
|
+
return
|
|
20
|
+
destination.write(chunk)
|
|
21
|
+
await destination.drain()
|
|
22
|
+
|
|
23
|
+
async def relay(source, destination, host, port):
|
|
24
|
+
task = asyncio.current_task()
|
|
25
|
+
if len(active) >= config["maxConnections"]:
|
|
26
|
+
destination.close()
|
|
27
|
+
await destination.wait_closed()
|
|
28
|
+
return
|
|
29
|
+
active.add(task)
|
|
30
|
+
upstream = None
|
|
31
|
+
pumps = []
|
|
32
|
+
try:
|
|
33
|
+
remote, upstream = await asyncio.wait_for(
|
|
34
|
+
asyncio.open_connection(host, port), config["connectTimeoutMs"] / 1000)
|
|
35
|
+
pumps = [asyncio.create_task(pipe(source, upstream)), asyncio.create_task(pipe(remote, destination))]
|
|
36
|
+
await asyncio.gather(*pumps)
|
|
37
|
+
except (OSError, asyncio.TimeoutError):
|
|
38
|
+
pass
|
|
39
|
+
finally:
|
|
40
|
+
for pump in pumps:
|
|
41
|
+
pump.cancel()
|
|
42
|
+
await asyncio.gather(*pumps, return_exceptions=True)
|
|
43
|
+
for writer in (upstream, destination):
|
|
44
|
+
if writer:
|
|
45
|
+
writer.close()
|
|
46
|
+
for writer in (upstream, destination):
|
|
47
|
+
if writer:
|
|
48
|
+
try:
|
|
49
|
+
await writer.wait_closed()
|
|
50
|
+
except OSError:
|
|
51
|
+
pass
|
|
52
|
+
active.discard(task)
|
|
53
|
+
|
|
54
|
+
print(json.dumps({"ready": True}), flush=True)
|
|
55
|
+
try:
|
|
56
|
+
while True:
|
|
57
|
+
# EOF cleans up normal shutdown; lease expiry bounds an orphan after lost exec transport.
|
|
58
|
+
line = await asyncio.wait_for(reader.readline(), config["leaseMs"] / 1000)
|
|
59
|
+
if not line:
|
|
60
|
+
break
|
|
61
|
+
request = json.loads(line)
|
|
62
|
+
if request.get("heartbeat"):
|
|
63
|
+
continue
|
|
64
|
+
identifier = request["id"]
|
|
65
|
+
try:
|
|
66
|
+
if len(servers) >= config["maxTargets"]:
|
|
67
|
+
raise ValueError("target limit")
|
|
68
|
+
host, port = request["host"], request["port"]
|
|
69
|
+
server = await asyncio.start_server(
|
|
70
|
+
lambda r, w, h=host, p=port: relay(r, w, h, p), "127.0.0.1", 0)
|
|
71
|
+
servers.append(server)
|
|
72
|
+
print(json.dumps({"id": identifier, "port": server.sockets[0].getsockname()[1]}), flush=True)
|
|
73
|
+
except (OSError, ValueError):
|
|
74
|
+
print(json.dumps({"id": identifier, "error": "relay listener unavailable"}), flush=True)
|
|
75
|
+
except asyncio.TimeoutError:
|
|
76
|
+
pass
|
|
77
|
+
finally:
|
|
78
|
+
for server in servers:
|
|
79
|
+
server.close()
|
|
80
|
+
await asyncio.gather(*(server.wait_closed() for server in servers))
|
|
81
|
+
tasks = list(active)
|
|
82
|
+
for task in tasks:
|
|
83
|
+
task.cancel()
|
|
84
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
85
|
+
|
|
86
|
+
asyncio.run(main())
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
export { POD_RELAY_SCRIPT };
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "./chunk-
|
|
2
|
+
resolveWorkload
|
|
3
|
+
} from "./chunk-y3dx8jh6.js";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
} from "./chunk-
|
|
5
|
+
ServicePortForwarder
|
|
6
|
+
} from "./chunk-qhpqse1y.js";
|
|
7
7
|
import {
|
|
8
8
|
resourceAccess
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import {
|
|
11
|
-
resolveWorkload
|
|
12
|
-
} from "./chunk-y3dx8jh6.js";
|
|
9
|
+
} from "./chunk-2m0mh09h.js";
|
|
13
10
|
import {
|
|
14
11
|
KubernetesError
|
|
15
12
|
} from "./chunk-ntfm5e85.js";
|
|
13
|
+
import {
|
|
14
|
+
KubectlExecutor
|
|
15
|
+
} from "./chunk-hmah9t8e.js";
|
|
16
16
|
|
|
17
17
|
// src/kubernetes/client.ts
|
|
18
18
|
class KubernetesClient {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildKubectlArgs
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-hmah9t8e.js";
|
|
4
4
|
import {
|
|
5
5
|
sleep,
|
|
6
6
|
spawnProcess
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-mn6dwsvh.js";
|
|
8
8
|
|
|
9
9
|
// src/kubernetes/port-forward.ts
|
|
10
10
|
async function startPortForward(opts) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface DataLoaderOptions {
|
|
2
|
+
maxEntries: number;
|
|
3
|
+
/** Serialized result bytes retained, excluding keys/errors and active operation memory. */
|
|
4
|
+
maxBytes: number;
|
|
5
|
+
signal: AbortSignal;
|
|
6
|
+
}
|
|
7
|
+
/** A scoped async loading cache, similar to Guava's LoadingCache.
|
|
8
|
+
* Shares results, failures and in-flight reads until close; consumers must not mutate results.
|
|
9
|
+
* Unlike a long-lived LoadingCache, this scope has no refresh/expiry or automatic batching.
|
|
10
|
+
* Keys and loading operations belong to adapters; the same key must have the same result type.
|
|
11
|
+
* Operations must honor their signal for prompt cancellation. No underlying client is owned.
|
|
12
|
+
*/
|
|
13
|
+
export declare class DataLoader {
|
|
14
|
+
#private;
|
|
15
|
+
private readonly options;
|
|
16
|
+
constructor(options: DataLoaderOptions);
|
|
17
|
+
/** Undefined keys bypass caching. A waiter's signal cancels only that wait, not shared work. */
|
|
18
|
+
read<T>(key: unknown, operation: (signal: AbortSignal) => Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
19
|
+
/** Abort cooperative operations and join all reads, including capacity-bypass reads. */
|
|
20
|
+
close(reason?: unknown): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// src/data-loader.ts
|
|
2
|
+
import { serialize } from "node:v8";
|
|
3
|
+
|
|
4
|
+
class DataLoader {
|
|
5
|
+
options;
|
|
6
|
+
#entries = new Map;
|
|
7
|
+
#pending = new Set;
|
|
8
|
+
#controller = new AbortController;
|
|
9
|
+
#bytes = 0;
|
|
10
|
+
#closing;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
for (const name of ["maxEntries", "maxBytes"]) {
|
|
14
|
+
if (!Number.isSafeInteger(options[name]) || options[name] < 1)
|
|
15
|
+
throw new Error(`${name} must be a positive safe integer`);
|
|
16
|
+
}
|
|
17
|
+
options.signal.throwIfAborted();
|
|
18
|
+
options.signal.addEventListener("abort", this.#abort, { once: true });
|
|
19
|
+
}
|
|
20
|
+
#abort = () => {
|
|
21
|
+
this.close(this.options.signal.reason);
|
|
22
|
+
};
|
|
23
|
+
async read(key, operation, signal) {
|
|
24
|
+
this.#controller.signal.throwIfAborted();
|
|
25
|
+
signal?.throwIfAborted();
|
|
26
|
+
let pending = key === undefined ? undefined : this.#entries.get(key);
|
|
27
|
+
if (!pending) {
|
|
28
|
+
const retain = key !== undefined && this.#entries.size < this.options.maxEntries;
|
|
29
|
+
pending = Promise.resolve().then(async () => {
|
|
30
|
+
this.#controller.signal.throwIfAborted();
|
|
31
|
+
const value = await operation(this.#controller.signal);
|
|
32
|
+
this.#controller.signal.throwIfAborted();
|
|
33
|
+
if (retain) {
|
|
34
|
+
let bytes;
|
|
35
|
+
try {
|
|
36
|
+
bytes = serialize(value).byteLength;
|
|
37
|
+
} catch {
|
|
38
|
+
bytes = Infinity;
|
|
39
|
+
}
|
|
40
|
+
if (this.#bytes + bytes <= this.options.maxBytes)
|
|
41
|
+
this.#bytes += bytes;
|
|
42
|
+
else
|
|
43
|
+
this.#entries.delete(key);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
});
|
|
47
|
+
if (retain)
|
|
48
|
+
this.#entries.set(key, pending);
|
|
49
|
+
this.#pending.add(pending);
|
|
50
|
+
const done = () => {
|
|
51
|
+
this.#pending.delete(pending);
|
|
52
|
+
};
|
|
53
|
+
pending.then(done, done);
|
|
54
|
+
}
|
|
55
|
+
if (!signal)
|
|
56
|
+
return await pending;
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const abort = () => {
|
|
59
|
+
cleanup();
|
|
60
|
+
reject(signal.reason);
|
|
61
|
+
};
|
|
62
|
+
const cleanup = () => {
|
|
63
|
+
signal.removeEventListener("abort", abort);
|
|
64
|
+
};
|
|
65
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
66
|
+
pending.then((value) => {
|
|
67
|
+
cleanup();
|
|
68
|
+
resolve(value);
|
|
69
|
+
}, (error) => {
|
|
70
|
+
cleanup();
|
|
71
|
+
reject(error);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
close(reason = new Error("DataLoader scope is closed")) {
|
|
76
|
+
if (!this.#closing) {
|
|
77
|
+
this.#closing = Promise.resolve().then(async () => {
|
|
78
|
+
await Promise.allSettled([...this.#pending]);
|
|
79
|
+
this.#entries.clear();
|
|
80
|
+
this.#bytes = 0;
|
|
81
|
+
});
|
|
82
|
+
this.options.signal.removeEventListener("abort", this.#abort);
|
|
83
|
+
this.#controller.abort(reason);
|
|
84
|
+
this.#entries.clear();
|
|
85
|
+
}
|
|
86
|
+
return this.#closing;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export {
|
|
90
|
+
DataLoader
|
|
91
|
+
};
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
KubernetesClient
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import"../chunk-
|
|
5
|
-
import"../chunk-
|
|
3
|
+
} from "../chunk-td11zjfb.js";
|
|
4
|
+
import"../chunk-y3dx8jh6.js";
|
|
5
|
+
import"../chunk-qhpqse1y.js";
|
|
6
6
|
import"../chunk-j46x7j7c.js";
|
|
7
7
|
import"../chunk-6fnhzhdb.js";
|
|
8
|
-
import"../chunk-
|
|
9
|
-
import"../chunk-ztenxgjv.js";
|
|
10
|
-
import"../chunk-1kpbvat9.js";
|
|
11
|
-
import"../chunk-y3dx8jh6.js";
|
|
12
|
-
import"../chunk-v3hxvnrc.js";
|
|
8
|
+
import"../chunk-2m0mh09h.js";
|
|
13
9
|
import"../chunk-ntfm5e85.js";
|
|
10
|
+
import"../chunk-v3hxvnrc.js";
|
|
11
|
+
import"../chunk-xh3zwhjh.js";
|
|
12
|
+
import"../chunk-hmah9t8e.js";
|
|
13
|
+
import"../chunk-mn6dwsvh.js";
|
|
14
14
|
export {
|
|
15
15
|
KubernetesClient
|
|
16
16
|
};
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
KubernetesClient
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import"../chunk-
|
|
5
|
-
import"../chunk-
|
|
3
|
+
} from "../chunk-td11zjfb.js";
|
|
4
|
+
import"../chunk-y3dx8jh6.js";
|
|
5
|
+
import"../chunk-qhpqse1y.js";
|
|
6
6
|
import"../chunk-j46x7j7c.js";
|
|
7
7
|
import"../chunk-6fnhzhdb.js";
|
|
8
|
-
import"../chunk-
|
|
9
|
-
import"../chunk-ztenxgjv.js";
|
|
10
|
-
import"../chunk-1kpbvat9.js";
|
|
11
|
-
import"../chunk-y3dx8jh6.js";
|
|
8
|
+
import"../chunk-2m0mh09h.js";
|
|
12
9
|
import {
|
|
13
10
|
clientKey
|
|
14
11
|
} from "../chunk-a9htb6cr.js";
|
|
15
|
-
import"../chunk-v3hxvnrc.js";
|
|
16
12
|
import"../chunk-ntfm5e85.js";
|
|
13
|
+
import"../chunk-v3hxvnrc.js";
|
|
14
|
+
import"../chunk-xh3zwhjh.js";
|
|
15
|
+
import"../chunk-hmah9t8e.js";
|
|
16
|
+
import"../chunk-mn6dwsvh.js";
|
|
17
17
|
|
|
18
18
|
// src/kubernetes/environment.ts
|
|
19
19
|
class KubernetesEnvironment {
|
|
@@ -9,7 +9,7 @@ export interface ExecTarget {
|
|
|
9
9
|
container?: string;
|
|
10
10
|
}
|
|
11
11
|
export interface RunOptions {
|
|
12
|
-
stdin?: string | Uint8Array
|
|
12
|
+
stdin?: string | Uint8Array | ReadableStream<Uint8Array>;
|
|
13
13
|
timeoutMs?: number;
|
|
14
14
|
signal?: AbortSignal;
|
|
15
15
|
onStdout?: (chunk: string) => void;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PortForwardScope,
|
|
3
3
|
startPortForward
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import"../chunk-
|
|
6
|
-
import"../chunk-
|
|
4
|
+
} from "../chunk-xh3zwhjh.js";
|
|
5
|
+
import"../chunk-hmah9t8e.js";
|
|
6
|
+
import"../chunk-mn6dwsvh.js";
|
|
7
7
|
export {
|
|
8
8
|
startPortForward,
|
|
9
9
|
PortForwardScope
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ServicePortForwarder
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import"../chunk-hv2kx47y.js";
|
|
3
|
+
} from "../chunk-qhpqse1y.js";
|
|
5
4
|
import"../chunk-j46x7j7c.js";
|
|
6
5
|
import"../chunk-6fnhzhdb.js";
|
|
7
|
-
import"../chunk-
|
|
8
|
-
import"../chunk-
|
|
6
|
+
import"../chunk-xh3zwhjh.js";
|
|
7
|
+
import"../chunk-hmah9t8e.js";
|
|
8
|
+
import"../chunk-mn6dwsvh.js";
|
|
9
9
|
export {
|
|
10
10
|
ServicePortForwarder
|
|
11
11
|
};
|
package/dist/mysql/client.js
CHANGED
|
@@ -2,11 +2,16 @@ import {
|
|
|
2
2
|
MysqlClient,
|
|
3
3
|
MysqlDatabase,
|
|
4
4
|
openMysql
|
|
5
|
-
} from "../chunk-
|
|
6
|
-
import"../chunk-kbewz29a.js";
|
|
5
|
+
} from "../chunk-56t4qyp7.js";
|
|
7
6
|
import"../chunk-m91t44jh.js";
|
|
7
|
+
import"../chunk-kbewz29a.js";
|
|
8
8
|
import"../chunk-v3hxvnrc.js";
|
|
9
|
-
import"../chunk-
|
|
9
|
+
import"../chunk-h8fwx0k7.js";
|
|
10
|
+
import"../chunk-5fgbhx2r.js";
|
|
11
|
+
import"../chunk-xh3zwhjh.js";
|
|
12
|
+
import"../chunk-hmah9t8e.js";
|
|
13
|
+
import"../chunk-mn6dwsvh.js";
|
|
14
|
+
import"../chunk-ssj93brx.js";
|
|
10
15
|
export {
|
|
11
16
|
openMysql,
|
|
12
17
|
MysqlDatabase,
|
package/dist/mysql/index.d.ts
CHANGED
package/dist/mysql/index.js
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
parseMysqlEnvTarget
|
|
3
|
-
} from "../chunk-qq77x9ek.js";
|
|
4
1
|
import"../chunk-pfar9cm3.js";
|
|
2
|
+
import {
|
|
3
|
+
withReadScope
|
|
4
|
+
} from "../chunk-4sqvppz7.js";
|
|
5
5
|
import {
|
|
6
6
|
MysqlClient,
|
|
7
7
|
MysqlDatabase,
|
|
8
8
|
openMysql
|
|
9
|
-
} from "../chunk-
|
|
10
|
-
import"../chunk-kbewz29a.js";
|
|
9
|
+
} from "../chunk-56t4qyp7.js";
|
|
11
10
|
import"../chunk-m91t44jh.js";
|
|
11
|
+
import"../chunk-kbewz29a.js";
|
|
12
|
+
import {
|
|
13
|
+
parseMysqlEnvTarget
|
|
14
|
+
} from "../chunk-qq77x9ek.js";
|
|
15
|
+
import {
|
|
16
|
+
limitedRows,
|
|
17
|
+
limitedStatement,
|
|
18
|
+
queryLimited
|
|
19
|
+
} from "../chunk-1gyshqd9.js";
|
|
12
20
|
import"../chunk-v3hxvnrc.js";
|
|
13
|
-
import"../chunk-
|
|
21
|
+
import"../chunk-h8fwx0k7.js";
|
|
22
|
+
import"../chunk-5fgbhx2r.js";
|
|
23
|
+
import"../chunk-xh3zwhjh.js";
|
|
24
|
+
import"../chunk-hmah9t8e.js";
|
|
25
|
+
import"../chunk-mn6dwsvh.js";
|
|
26
|
+
import"../chunk-ssj93brx.js";
|
|
14
27
|
export {
|
|
28
|
+
withReadScope,
|
|
29
|
+
queryLimited,
|
|
15
30
|
parseMysqlEnvTarget,
|
|
16
31
|
openMysql,
|
|
32
|
+
limitedStatement,
|
|
33
|
+
limitedRows,
|
|
17
34
|
MysqlDatabase,
|
|
18
35
|
MysqlClient
|
|
19
36
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Database, DatabaseRow, DatabaseTarget, SqlStatement } from "./types";
|
|
2
|
+
export interface LimitedRows {
|
|
3
|
+
rows: DatabaseRow[];
|
|
4
|
+
truncated: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** Append to caller-owned SQL without an existing LIMIT or trailing terminator; never rewrite SQL.
|
|
7
|
+
* An integer literal avoids driver/server differences in numeric LIMIT parameter binding.
|
|
8
|
+
*/
|
|
9
|
+
export declare function limitedStatement(statement: SqlStatement, maxRows: number): SqlStatement;
|
|
10
|
+
/** The extra row proves truncation; batch callers can use the same projection. */
|
|
11
|
+
export declare function limitedRows(rows: DatabaseRow[], maxRows: number): LimitedRows;
|
|
12
|
+
/** Row-count bound for trusted reads on the existing session, not a readonly transaction or byte cap. */
|
|
13
|
+
export declare function queryLimited(database: Pick<Database, "query">, target: DatabaseTarget, statement: SqlStatement, maxRows: number): Promise<LimitedRows>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DataLoader } from "../data-loader";
|
|
2
|
+
import type { Database } from "./types";
|
|
3
|
+
/** Bind caller-approved reads to a borrowed scope. Results are shared and must not be mutated.
|
|
4
|
+
* Close is a no-op: the root owns both scope and database lifetimes. Database operations retain
|
|
5
|
+
* their client's cancellation/timeout policy; they cannot be individually aborted by this adapter.
|
|
6
|
+
*/
|
|
7
|
+
export declare function withReadScope(database: Database, scope: DataLoader): Database & Required<Pick<Database, "queryBatch">>;
|
package/dist/mysql/types.d.ts
CHANGED
|
@@ -29,5 +29,7 @@ export interface DatabaseQueryResult {
|
|
|
29
29
|
export interface Database {
|
|
30
30
|
query(target: DatabaseTarget, sql: string, values: readonly unknown[]): Promise<DatabaseRow[]>;
|
|
31
31
|
queryOne(target: DatabaseTarget, sql: string, values: readonly unknown[]): Promise<DatabaseRow | undefined>;
|
|
32
|
+
/** Independent statements in submission order; fail-fast, without atomicity or replay. */
|
|
33
|
+
queryBatch?(target: DatabaseTarget, statements: readonly SqlStatement[]): Promise<DatabaseRow[][]>;
|
|
32
34
|
close(): Promise<void>;
|
|
33
35
|
}
|
package/dist/process/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface RuntimeProcess {
|
|
|
5
5
|
kill(): void;
|
|
6
6
|
}
|
|
7
7
|
export interface SpawnProcessOptions {
|
|
8
|
-
stdin?: string | Uint8Array
|
|
8
|
+
stdin?: string | Uint8Array | ReadableStream<Uint8Array>;
|
|
9
9
|
env?: NodeJS.ProcessEnv;
|
|
10
10
|
}
|
|
11
11
|
/** 使用 Node-compatible 子进程 API,不要求调用方使用 Bun runtime。 */
|
package/dist/process/index.js
CHANGED
package/dist/redis/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectRedisKeyStats,
|
|
3
|
+
redisGroupedSampleSizes
|
|
4
|
+
} from "../chunk-jr9y0ghr.js";
|
|
1
5
|
import {
|
|
2
6
|
discoverRedisTopology
|
|
3
7
|
} from "../chunk-c6z919qb.js";
|
|
@@ -8,10 +12,6 @@ import {
|
|
|
8
12
|
openRedis,
|
|
9
13
|
parseRedisInfo
|
|
10
14
|
} from "../chunk-ajhdd4hp.js";
|
|
11
|
-
import {
|
|
12
|
-
collectRedisKeyStats,
|
|
13
|
-
redisGroupedSampleSizes
|
|
14
|
-
} from "../chunk-jr9y0ghr.js";
|
|
15
15
|
export {
|
|
16
16
|
redisGroupedSampleSizes,
|
|
17
17
|
parseRedisInfo,
|
package/dist/s3/datasource.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
S3DataSource
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-35cjec9z.js";
|
|
4
4
|
import"../chunk-z5pm2emz.js";
|
|
5
|
+
import"../chunk-ntfm5e85.js";
|
|
5
6
|
import"../chunk-zp1s833x.js";
|
|
6
7
|
import"../chunk-v3hxvnrc.js";
|
|
7
8
|
import"../chunk-x4ntb898.js";
|
|
8
|
-
import"../chunk-
|
|
9
|
-
import"../chunk-
|
|
9
|
+
import"../chunk-h8fwx0k7.js";
|
|
10
|
+
import"../chunk-5fgbhx2r.js";
|
|
11
|
+
import"../chunk-xh3zwhjh.js";
|
|
12
|
+
import"../chunk-hmah9t8e.js";
|
|
13
|
+
import"../chunk-mn6dwsvh.js";
|
|
14
|
+
import"../chunk-ssj93brx.js";
|
|
10
15
|
export {
|
|
11
16
|
S3DataSource
|
|
12
17
|
};
|
package/dist/s3/index.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
S3DataSource
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-35cjec9z.js";
|
|
4
4
|
import"../chunk-z5pm2emz.js";
|
|
5
|
+
import"../chunk-ntfm5e85.js";
|
|
5
6
|
import {
|
|
6
7
|
S3Client
|
|
7
8
|
} from "../chunk-zp1s833x.js";
|
|
8
9
|
import"../chunk-v3hxvnrc.js";
|
|
9
10
|
import"../chunk-x4ntb898.js";
|
|
10
|
-
import"../chunk-
|
|
11
|
-
import"../chunk-
|
|
11
|
+
import"../chunk-h8fwx0k7.js";
|
|
12
|
+
import"../chunk-5fgbhx2r.js";
|
|
13
|
+
import"../chunk-xh3zwhjh.js";
|
|
14
|
+
import"../chunk-hmah9t8e.js";
|
|
15
|
+
import"../chunk-mn6dwsvh.js";
|
|
16
|
+
import"../chunk-ssj93brx.js";
|
|
12
17
|
export {
|
|
13
18
|
S3DataSource,
|
|
14
19
|
S3Client
|
|
@@ -38,3 +38,4 @@ export declare class PodPythonTransport {
|
|
|
38
38
|
export type Transport = TcpTransport | PodPythonTransport;
|
|
39
39
|
/** Only failure to establish a connection permits a route change; never replay protocol operations. */
|
|
40
40
|
export declare function isConnectionNetworkError(error: unknown): boolean;
|
|
41
|
+
export { PodRelayTransport, type PodRelayOptions } from "./pod-relay";
|
package/dist/transport/index.js
CHANGED
|
@@ -3,10 +3,18 @@ import {
|
|
|
3
3
|
PodPythonTransport,
|
|
4
4
|
PortForwardTransport,
|
|
5
5
|
isConnectionNetworkError
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-h8fwx0k7.js";
|
|
7
|
+
import {
|
|
8
|
+
PodRelayTransport
|
|
9
|
+
} from "../chunk-5fgbhx2r.js";
|
|
10
|
+
import"../chunk-xh3zwhjh.js";
|
|
11
|
+
import"../chunk-hmah9t8e.js";
|
|
12
|
+
import"../chunk-mn6dwsvh.js";
|
|
13
|
+
import"../chunk-ssj93brx.js";
|
|
7
14
|
export {
|
|
8
15
|
isConnectionNetworkError,
|
|
9
16
|
PortForwardTransport,
|
|
17
|
+
PodRelayTransport,
|
|
10
18
|
PodPythonTransport,
|
|
11
19
|
DirectTransport
|
|
12
20
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Client } from "../client";
|
|
2
|
+
import { type Executor, type KubectlOptions } from "../kubernetes/executor";
|
|
3
|
+
import { type StartPortForward } from "../kubernetes/port-forward";
|
|
4
|
+
import type { Endpoint, TcpTransport } from "./index";
|
|
5
|
+
export interface PodRelayOptions extends KubectlOptions {
|
|
6
|
+
namespace: string;
|
|
7
|
+
/** Restrict eligible existing Pods without coupling callers to the relay runtime. */
|
|
8
|
+
selector?: string;
|
|
9
|
+
startupTimeoutMs: number;
|
|
10
|
+
connectTimeoutMs: number;
|
|
11
|
+
maxConnections: number;
|
|
12
|
+
maxTargets: number;
|
|
13
|
+
maxCandidatePods: number;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
/** Execution-scoped TCP transport borrowed by all Service clients; only its root owner disposes it.
|
|
17
|
+
* Python is private. Requires list pods, pods/exec and pods/portforward, never pods/create.
|
|
18
|
+
*/
|
|
19
|
+
export declare class PodRelayTransport implements TcpTransport, Client {
|
|
20
|
+
#private;
|
|
21
|
+
private readonly options;
|
|
22
|
+
readonly kind = "tcp";
|
|
23
|
+
readonly name = "pod-relay";
|
|
24
|
+
constructor(options: PodRelayOptions, dependencies?: {
|
|
25
|
+
executor?: Executor;
|
|
26
|
+
startForward?: StartPortForward;
|
|
27
|
+
});
|
|
28
|
+
initialize(): Promise<void>;
|
|
29
|
+
connect(endpoint: Endpoint): Promise<Endpoint>;
|
|
30
|
+
dispose(): Promise<void>;
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@compforge/harness-toolbox",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Shared infrastructure clients, transports and bounded data capture",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -80,6 +80,10 @@
|
|
|
80
80
|
"./concurrency": {
|
|
81
81
|
"types": "./dist/concurrency.d.ts",
|
|
82
82
|
"import": "./dist/concurrency.js"
|
|
83
|
+
},
|
|
84
|
+
"./data-loader": {
|
|
85
|
+
"types": "./dist/data-loader.d.ts",
|
|
86
|
+
"import": "./dist/data-loader.js"
|
|
83
87
|
}
|
|
84
88
|
},
|
|
85
89
|
"files": [
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ConcurrencyPool
|
|
3
|
-
} from "./chunk-v3hxvnrc.js";
|
|
4
1
|
import {
|
|
5
2
|
KubernetesError,
|
|
6
3
|
ToolboxError,
|
|
7
4
|
kubernetesHttpError
|
|
8
5
|
} from "./chunk-ntfm5e85.js";
|
|
6
|
+
import {
|
|
7
|
+
ConcurrencyPool
|
|
8
|
+
} from "./chunk-v3hxvnrc.js";
|
|
9
9
|
|
|
10
10
|
// src/kubernetes/resources.ts
|
|
11
11
|
import { KubeConfig } from "@kubernetes/client-node";
|
|
File without changes
|