@compforge/harness-toolbox 0.4.2 → 0.4.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 CHANGED
@@ -62,6 +62,26 @@ never close it themselves.
62
62
  Version 0.4.2 requires Common 0.5.4. Replace single path arguments with arrays, supply `maxTotalBytes`,
63
63
  and handle per-file results. Publish Common before Toolbox; this change has no single-file legacy adapter.
64
64
 
65
+ ## Redis sources
66
+
67
+ `RedisClient` owns resolved configuration and reusable node connections. Use `clusterType` to
68
+ select `single`, `cluster` or `sentinel`; `endpoints` contains that mode's seed addresses. Sentinel
69
+ configuration additionally provides `sentinelMasterName` and optional Sentinel credentials,
70
+ separate from data-node credentials. Cluster connections require database zero.
71
+
72
+ Supply ordered TCP transports through `ConnectionSource`. Only mapping or handshake network
73
+ failures advance to the next transport; authentication and TLS verification failures stop immediately.
74
+ `connectTimeoutMs` bounds each route (mapping and handshake); it defaults to the command
75
+ `timeoutMs`. `acquireTimeoutMs` bounds all candidates together and defaults to the per-route budget
76
+ times the number of routes. A spent total budget does not start another route.
77
+ `onRoute` reports the selected route and the preceding failure code without credentials.
78
+
79
+ `client.topology()` refreshes topology using the same connections and route policy as
80
+ `client.access.connection(endpoint, database)`. A root signal cancels connection preparation and
81
+ active commands. Commands and pipelines are sent once, with no offline queue or automatic replay;
82
+ a subsequent acquisition replaces an unhealthy connection. The caller owns `dispose()` and transport
83
+ cleanup, and diagnostics own sampling policy and collected observations.
84
+
65
85
  ## Kubernetes resources
66
86
 
67
87
  Borrow a root `KubernetesClient` through `KubernetesEnvironment` and `ClientManager`. Use
@@ -356,3 +376,11 @@ reason. The caller owns shared transports such as `PodRelayTransport` and their
356
376
 
357
377
  Routed HTTPS preserves the original TLS server name. Bun disables HTTP keep-alive because its reused
358
378
  socket recovery can replay a failed request below the driver; Node retains connection pooling.
379
+
380
+ ## Kubernetes compute evidence
381
+
382
+ `kubernetes/compute` exports `collectClusterCompute` and `collectWorkloadCompute`. They borrow
383
+ ResourceAccess, use explicit deadlines/item/page budgets, and retain read coverage independently.
384
+ Use `client.clusterResources()` for Nodes and all-namespace Pods; normal client methods retain their
385
+ configured namespace. A forbidden cluster read records `permission_denied`; it does not block a
386
+ separate namespace-scoped Workload read. See [compute collection](../../../docs/toolbox.md#kubernetes-compute-evidence).
@@ -26,7 +26,7 @@ function sentinelRows(value) {
26
26
  });
27
27
  }
28
28
  async function discoverSentinel(access, config) {
29
- const sentinels = config.sentinelHosts.length ? config.sentinelHosts : config.endpoints;
29
+ const sentinels = config.endpoints;
30
30
  let lastError;
31
31
  for (const sentinel of sentinels) {
32
32
  try {
@@ -0,0 +1,145 @@
1
+ import {
2
+ KubernetesError
3
+ } from "./chunk-20kv4wc3.js";
4
+
5
+ // src/kubernetes/compute-quantity.ts
6
+ function object(value) {
7
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
8
+ }
9
+ function invalid(message) {
10
+ throw new KubernetesError(message, { kind: "invalid_response" });
11
+ }
12
+ function requiredString(value, field) {
13
+ if (typeof value !== "string" || !value)
14
+ return invalid(`Kubernetes response lacks ${field}`);
15
+ return value;
16
+ }
17
+ function parseComputeQuantity(value) {
18
+ if (typeof value !== "string")
19
+ return invalid("Kubernetes quantity must be a string");
20
+ const match = /^([+-]?(?:\d+(?:\.\d*)?|\.\d+))([eE][+-]?\d+|[numkMGTPE]|[KMGTPE]i)?$/.exec(value);
21
+ if (!match)
22
+ return invalid("Invalid Kubernetes quantity");
23
+ const suffix = match[2] ?? "";
24
+ const factors = {
25
+ "": 1,
26
+ n: 0.000000001,
27
+ u: 0.000001,
28
+ m: 0.001,
29
+ k: 1000,
30
+ M: 1e6,
31
+ G: 1e9,
32
+ T: 1000000000000,
33
+ P: 1000000000000000,
34
+ E: 1000000000000000000,
35
+ Ki: 2 ** 10,
36
+ Mi: 2 ** 20,
37
+ Gi: 2 ** 30,
38
+ Ti: 2 ** 40,
39
+ Pi: 2 ** 50,
40
+ Ei: 2 ** 60
41
+ };
42
+ const result = Number(match[1]) * (/^[eE][+-]?\d+$/.test(suffix) ? 10 ** Number(suffix.slice(1)) : factors[suffix]);
43
+ if (!Number.isFinite(result) || result < 0 || result > Number.MAX_SAFE_INTEGER) {
44
+ return invalid("Kubernetes quantity is outside the supported numeric range");
45
+ }
46
+ return result;
47
+ }
48
+ var zero = () => ({ cpu: 0, memory: 0 });
49
+ function add(left, right) {
50
+ return { cpu: left.cpu + right.cpu, memory: left.memory + right.memory };
51
+ }
52
+ function max(left, right) {
53
+ return { cpu: Math.max(left.cpu, right.cpu), memory: Math.max(left.memory, right.memory) };
54
+ }
55
+ function quantities(value, required = false) {
56
+ const input = object(value);
57
+ return {
58
+ cpu: input.cpu === undefined && !required ? 0 : parseComputeQuantity(input.cpu),
59
+ memory: input.memory === undefined && !required ? 0 : parseComputeQuantity(input.memory)
60
+ };
61
+ }
62
+ function sum(values) {
63
+ return values.reduce(add, zero());
64
+ }
65
+ function containers(value) {
66
+ if (!Array.isArray(value))
67
+ return invalid("Pod response lacks containers");
68
+ return value.map((item) => {
69
+ const container = object(item);
70
+ requiredString(container.name, "container.name");
71
+ return container;
72
+ });
73
+ }
74
+ function podCompute(value) {
75
+ const meta = object(value.metadata), spec = object(value.spec), status = object(value.status);
76
+ const app = containers(spec.containers);
77
+ if (!app.length)
78
+ return invalid("Pod must contain an application container");
79
+ const init = containers(spec.initContainers ?? []);
80
+ const effective = (field) => {
81
+ let steady = sum(app.map((c) => quantities(object(c.resources)[field])));
82
+ let sidecars = zero(), peak = zero();
83
+ for (const container of init) {
84
+ const current = quantities(object(container.resources)[field]);
85
+ if (container.restartPolicy === "Always") {
86
+ sidecars = add(sidecars, current);
87
+ steady = add(steady, current);
88
+ peak = max(peak, sidecars);
89
+ } else
90
+ peak = max(peak, add(sidecars, current));
91
+ }
92
+ const result = max(steady, peak);
93
+ const podLevel = object(object(spec.resources)[field]);
94
+ const overhead = quantities(spec.overhead);
95
+ for (const key of ["cpu", "memory"]) {
96
+ if (podLevel[key] !== undefined)
97
+ result[key] = parseComputeQuantity(podLevel[key]);
98
+ if (field === "requests" || result[key] > 0)
99
+ result[key] += overhead[key];
100
+ }
101
+ return result;
102
+ };
103
+ const without = zero();
104
+ for (const container of [...app, ...init]) {
105
+ const limits = object(object(container.resources).limits);
106
+ for (const key of ["cpu", "memory"])
107
+ if (limits[key] === undefined)
108
+ without[key]++;
109
+ }
110
+ const conditions = Array.isArray(status.conditions) ? status.conditions.map(object) : [];
111
+ const states = [
112
+ ...Array.isArray(status.containerStatuses) ? status.containerStatuses : [],
113
+ ...Array.isArray(status.initContainerStatuses) ? status.initContainerStatuses : []
114
+ ].map(object);
115
+ return {
116
+ namespace: requiredString(meta.namespace, "metadata.namespace"),
117
+ name: requiredString(meta.name, "metadata.name"),
118
+ uid: requiredString(meta.uid, "metadata.uid"),
119
+ createdAt: typeof meta.creationTimestamp === "string" ? meta.creationTimestamp : undefined,
120
+ node: typeof spec.nodeName === "string" && spec.nodeName ? spec.nodeName : undefined,
121
+ phase: requiredString(status.phase, "status.phase"),
122
+ ready: conditions.some((c) => c.type === "Ready" && c.status === "True"),
123
+ restarts: states.reduce((n, c) => n + (typeof c.restartCount === "number" ? c.restartCount : 0), 0),
124
+ containers: [...app, ...init.filter((c) => c.restartPolicy === "Always")].map((c) => String(c.name)),
125
+ requests: effective("requests"),
126
+ configuredLimits: effective("limits"),
127
+ containersWithoutLimits: without
128
+ };
129
+ }
130
+ function summarizePods(pods) {
131
+ const active = pods.filter((p) => p.phase !== "Succeeded" && p.phase !== "Failed");
132
+ const scheduled = active.filter((p) => p.node);
133
+ return {
134
+ pods: pods.length,
135
+ activePods: active.length,
136
+ scheduledPods: scheduled.length,
137
+ readyPods: active.filter((p) => p.ready).length,
138
+ requests: sum(active.map((p) => p.requests)),
139
+ scheduledRequests: sum(scheduled.map((p) => p.requests)),
140
+ configuredLimits: sum(active.map((p) => p.configuredLimits)),
141
+ containersWithoutLimits: sum(active.map((p) => p.containersWithoutLimits))
142
+ };
143
+ }
144
+
145
+ export { object, requiredString, parseComputeQuantity, zero, add, quantities, sum, podCompute, summarizePods };
@@ -1,15 +1,15 @@
1
- import {
2
- KubernetesResourceTransport
3
- } from "./chunk-4ms037k2.js";
4
- import {
5
- resolveWorkload
6
- } from "./chunk-0s27bgga.js";
7
1
  import {
8
2
  ServicePortForwarder
9
3
  } from "./chunk-2171tajs.js";
4
+ import {
5
+ KubernetesResourceTransport
6
+ } from "./chunk-4ms037k2.js";
10
7
  import {
11
8
  KubernetesExecTransport
12
9
  } from "./chunk-shsabpc1.js";
10
+ import {
11
+ resolveWorkload
12
+ } from "./chunk-0s27bgga.js";
13
13
  import {
14
14
  KubernetesError
15
15
  } from "./chunk-20kv4wc3.js";
@@ -83,6 +83,12 @@ class KubernetesClient {
83
83
  list(query, options) {
84
84
  return this.#track(() => this.#resourceAccess().list({ ...query, namespace: query.namespace ?? this.kube.namespace }, this.#options(options)));
85
85
  }
86
+ clusterResources() {
87
+ return {
88
+ get: (ref, options) => this.#track(() => this.#resourceAccess().get(ref, this.#options(options))),
89
+ list: (query, options) => this.#track(() => this.#resourceAccess().list(query, this.#options(options)))
90
+ };
91
+ }
86
92
  inNamespace(namespace) {
87
93
  if (!namespace.trim())
88
94
  throw new KubernetesError("Namespace is required", { kind: "invalid_argument" });
@@ -0,0 +1,366 @@
1
+ import {
2
+ discoverRedisTopology
3
+ } from "./chunk-5tbzf9wd.js";
4
+ import {
5
+ ToolboxError
6
+ } from "./chunk-20kv4wc3.js";
7
+ import {
8
+ isConnectionNetworkError
9
+ } from "./chunk-h8fwx0k7.js";
10
+
11
+ // src/redis/client.ts
12
+ import { createClient, SocketClosedUnexpectedlyError, ConnectionTimeoutError } from "@redis/client";
13
+ function stringValue(value) {
14
+ if (Buffer.isBuffer(value))
15
+ return value.toString("utf8");
16
+ return String(value ?? "");
17
+ }
18
+ function scalar(value) {
19
+ if (/^-?(?:\d+|\d+\.\d+)$/.test(value))
20
+ return Number(value);
21
+ return value;
22
+ }
23
+ function parseRedisInfo(raw) {
24
+ const result = {};
25
+ for (const line of raw.split(/\r?\n/)) {
26
+ if (!line || line.startsWith("#"))
27
+ continue;
28
+ const separator = line.indexOf(":");
29
+ if (separator < 1)
30
+ continue;
31
+ const name = line.slice(0, separator);
32
+ const value = line.slice(separator + 1);
33
+ if (/^db\d+$/.test(name)) {
34
+ result[name] = Object.fromEntries(value.split(",").map((item) => {
35
+ const index = item.indexOf("=");
36
+ return [item.slice(0, index), scalar(item.slice(index + 1))];
37
+ }));
38
+ } else {
39
+ result[name] = scalar(value);
40
+ }
41
+ }
42
+ return result;
43
+ }
44
+ async function abortable(pending, signal, cancel) {
45
+ if (signal.aborted) {
46
+ cancel?.();
47
+ pending.catch(() => {});
48
+ signal.throwIfAborted();
49
+ }
50
+ let abort;
51
+ const cancelled = new Promise((_, reject) => {
52
+ abort = () => {
53
+ cancel?.();
54
+ reject(signal.reason);
55
+ };
56
+ signal.addEventListener("abort", abort, { once: true });
57
+ });
58
+ try {
59
+ return await Promise.race([pending, cancelled]);
60
+ } finally {
61
+ signal.removeEventListener("abort", abort);
62
+ }
63
+ }
64
+
65
+ class RedisConnection {
66
+ endpoint;
67
+ client;
68
+ timeoutMs;
69
+ lastError;
70
+ signal;
71
+ #abort;
72
+ constructor(endpoint, client, timeoutMs, lastError, signal) {
73
+ this.endpoint = endpoint;
74
+ this.client = client;
75
+ this.timeoutMs = timeoutMs;
76
+ this.lastError = lastError;
77
+ this.signal = signal;
78
+ this.#abort = () => this.close();
79
+ signal?.addEventListener("abort", this.#abort, { once: true });
80
+ }
81
+ static async connect(endpoint, mapped, config, clientFactory = createClient) {
82
+ config.signal?.throwIfAborted();
83
+ config.connectSignal?.throwIfAborted();
84
+ const baseSocket = {
85
+ host: mapped.host,
86
+ port: mapped.port,
87
+ connectTimeout: config.connectTimeoutMs ?? config.timeoutMs,
88
+ reconnectStrategy: false
89
+ };
90
+ const socket = config.useSsl ? { ...baseSocket, tls: true, servername: mapped.servername ?? endpoint.host } : { ...baseSocket, tls: false };
91
+ const client = clientFactory({
92
+ socket,
93
+ database: config.database,
94
+ username: config.username,
95
+ password: config.password,
96
+ disableOfflineQueue: true,
97
+ commandOptions: { timeout: config.timeoutMs }
98
+ });
99
+ let lastError;
100
+ client.on("error", (err) => {
101
+ lastError = err instanceof Error ? err : new Error(String(err));
102
+ });
103
+ const timeout = new AbortController;
104
+ const timer = setTimeout(() => timeout.abort(new ToolboxError("Redis handshake timed out", { kind: "timeout", code: "ETIMEDOUT" })), config.connectTimeoutMs ?? config.timeoutMs);
105
+ const signal = AbortSignal.any([
106
+ timeout.signal,
107
+ ...config.signal ? [config.signal] : [],
108
+ ...config.connectSignal ? [config.connectSignal] : []
109
+ ]);
110
+ const close = () => {
111
+ if (client.isOpen)
112
+ client.destroy();
113
+ };
114
+ try {
115
+ await abortable(client.connect(), signal, close);
116
+ signal.throwIfAborted();
117
+ return new RedisConnection(endpoint, client, config.timeoutMs, () => lastError, config.signal);
118
+ } catch (error) {
119
+ close();
120
+ throw error;
121
+ } finally {
122
+ clearTimeout(timer);
123
+ }
124
+ }
125
+ get isReady() {
126
+ return !this.signal?.aborted && this.client.isReady;
127
+ }
128
+ async#execute(operationName, operation) {
129
+ this.signal?.throwIfAborted();
130
+ if (!this.client.isOpen) {
131
+ const reason = this.lastError()?.message;
132
+ throw new Error(`Redis connection is closed${reason ? `: ${reason}` : ""}`);
133
+ }
134
+ const timeout = new AbortController;
135
+ const timer = setTimeout(() => timeout.abort(new ToolboxError(`Redis ${operationName} timed out after ${this.timeoutMs}ms (${this.endpoint.host}:${this.endpoint.port})`, { kind: "timeout", code: "ETIMEDOUT" })), this.timeoutMs);
136
+ const signal = this.signal ? AbortSignal.any([this.signal, timeout.signal]) : timeout.signal;
137
+ try {
138
+ return await abortable(Promise.resolve().then(() => {
139
+ signal.throwIfAborted();
140
+ return operation();
141
+ }), signal, () => this.close());
142
+ } finally {
143
+ clearTimeout(timer);
144
+ }
145
+ }
146
+ ping() {
147
+ return this.#execute("PING", () => this.client.ping());
148
+ }
149
+ async info(section) {
150
+ const raw = await this.#execute(section ? `INFO ${section}` : "INFO", () => section ? this.client.sendCommand(["INFO", section]) : this.client.sendCommand(["INFO"]));
151
+ return parseRedisInfo(stringValue(raw));
152
+ }
153
+ async dbSize() {
154
+ return Number(await this.#execute("DBSIZE", () => this.client.sendCommand(["DBSIZE"])));
155
+ }
156
+ command(args) {
157
+ return this.#execute(args[0]?.toUpperCase() ?? "command", () => this.client.sendCommand(args));
158
+ }
159
+ async pipeline(commands) {
160
+ const commandNames = new Set(commands.map((command) => command[0]?.toUpperCase()).filter(Boolean));
161
+ const operationName = commandNames.size === 1 ? `${[...commandNames][0]} pipeline` : "pipeline";
162
+ return await this.#execute(operationName, () => {
163
+ const pipeline = this.client.multi();
164
+ for (const command of commands)
165
+ pipeline.addCommand(command);
166
+ return pipeline.execAsPipeline();
167
+ });
168
+ }
169
+ async scan(cursor, count) {
170
+ const reply = await this.command(["SCAN", cursor, "COUNT", String(count)]);
171
+ if (!Array.isArray(reply) || !Array.isArray(reply[1]))
172
+ throw new Error("Redis SCAN 返回格式无效");
173
+ return { cursor: stringValue(reply[0]), keys: reply[1].map(stringValue) };
174
+ }
175
+ close() {
176
+ if (this.#abort)
177
+ this.signal?.removeEventListener("abort", this.#abort);
178
+ if (this.client.isOpen)
179
+ this.client.destroy();
180
+ }
181
+ }
182
+
183
+ class RedisAccess {
184
+ baseConfig;
185
+ connectionFactory;
186
+ lifecycle;
187
+ #connections = new Map;
188
+ #controller = new AbortController;
189
+ #signal;
190
+ #transports;
191
+ #closing;
192
+ constructor(transports, baseConfig, connectionFactory = RedisConnection.connect, lifecycle = {}) {
193
+ this.baseConfig = baseConfig;
194
+ this.connectionFactory = connectionFactory;
195
+ this.lifecycle = lifecycle;
196
+ this.#transports = Array.isArray(transports) ? transports : [transports];
197
+ if (!this.#transports.length)
198
+ throw new Error("Redis requires a TCP transport");
199
+ for (const value of [
200
+ baseConfig.timeoutMs,
201
+ baseConfig.connectTimeoutMs ?? baseConfig.timeoutMs,
202
+ baseConfig.acquireTimeoutMs ?? (baseConfig.connectTimeoutMs ?? baseConfig.timeoutMs) * this.#transports.length
203
+ ]) {
204
+ if (!Number.isFinite(value) || value <= 0)
205
+ throw new Error("Redis timeouts must be positive milliseconds");
206
+ }
207
+ this.#signal = lifecycle.signal ? AbortSignal.any([lifecycle.signal, this.#controller.signal]) : this.#controller.signal;
208
+ }
209
+ async connection(endpoint, database, credentials = this.baseConfig) {
210
+ this.#signal.throwIfAborted();
211
+ const key = [endpoint.host, endpoint.port, database, credentials.username ?? "", credentials.password ?? ""].join("\x00");
212
+ const existing = this.#connections.get(key);
213
+ if (existing) {
214
+ const connection = await existing;
215
+ this.#signal.throwIfAborted();
216
+ if (connection.isReady)
217
+ return connection;
218
+ if (this.#connections.get(key) === existing) {
219
+ this.#connections.delete(key);
220
+ connection.close();
221
+ }
222
+ return this.connection(endpoint, database, credentials);
223
+ }
224
+ const pending = this.#open(endpoint, database, credentials);
225
+ this.#connections.set(key, pending);
226
+ pending.catch(() => {
227
+ if (this.#connections.get(key) === pending)
228
+ this.#connections.delete(key);
229
+ });
230
+ return pending;
231
+ }
232
+ async#open(endpoint, database, credentials) {
233
+ const timeout = new AbortController;
234
+ const attemptBudget = this.baseConfig.connectTimeoutMs ?? this.baseConfig.timeoutMs;
235
+ const budget = this.baseConfig.acquireTimeoutMs ?? attemptBudget * this.#transports.length;
236
+ const deadline = performance.now() + budget;
237
+ const timer = setTimeout(() => timeout.abort(new ToolboxError(`Redis connection timed out after ${budget}ms (${endpoint.host}:${endpoint.port})`, { kind: "timeout", code: "ETIMEDOUT" })), budget);
238
+ const signal = AbortSignal.any([this.#signal, timeout.signal]);
239
+ let reason;
240
+ try {
241
+ for (let index = 0;index < this.#transports.length; index++) {
242
+ signal.throwIfAborted();
243
+ const transport = this.#transports[index];
244
+ const attempt = new AbortController;
245
+ const remaining = Math.min(attemptBudget, Math.max(1, Math.ceil(deadline - performance.now())));
246
+ const attemptTimer = setTimeout(() => attempt.abort(new ToolboxError(`Redis connection attempt timed out (${transport.name})`, { kind: "timeout", code: "ETIMEDOUT" })), remaining);
247
+ const attemptSignal = AbortSignal.any([signal, attempt.signal]);
248
+ try {
249
+ const mapped = await abortable(transport.connect(endpoint), attemptSignal);
250
+ attemptSignal.throwIfAborted();
251
+ const pending = this.connectionFactory(endpoint, mapped, {
252
+ ...this.baseConfig,
253
+ ...credentials,
254
+ database,
255
+ connectTimeoutMs: remaining,
256
+ signal: this.#signal,
257
+ connectSignal: attemptSignal
258
+ });
259
+ const prepared = pending.then((connection2) => {
260
+ if (attemptSignal.aborted) {
261
+ connection2.close();
262
+ attemptSignal.throwIfAborted();
263
+ }
264
+ return connection2;
265
+ });
266
+ const connection = await abortable(prepared, attemptSignal);
267
+ this.lifecycle.onRoute?.({ transport: mapped.host === endpoint.host && mapped.port === endpoint.port ? "direct" : transport.name, reason });
268
+ return connection;
269
+ } catch (failure) {
270
+ signal.throwIfAborted();
271
+ const error = attemptSignal.aborted ? attemptSignal.reason : failure;
272
+ const network = isConnectionNetworkError(error) || error instanceof SocketClosedUnexpectedlyError || error instanceof ConnectionTimeoutError;
273
+ if (!network || index === this.#transports.length - 1)
274
+ throw error;
275
+ reason = error instanceof SocketClosedUnexpectedlyError ? "connection_lost" : error instanceof ConnectionTimeoutError ? "ETIMEDOUT" : String(error.code);
276
+ } finally {
277
+ clearTimeout(attemptTimer);
278
+ }
279
+ }
280
+ throw new Error("Redis requires a TCP transport");
281
+ } finally {
282
+ clearTimeout(timer);
283
+ }
284
+ }
285
+ close() {
286
+ return this.#closing ??= (async () => {
287
+ this.#controller.abort(new Error("Redis access is closed"));
288
+ const settled = await Promise.allSettled(this.#connections.values());
289
+ this.#connections.clear();
290
+ const errors = [];
291
+ for (const result of settled)
292
+ if (result.status === "fulfilled") {
293
+ try {
294
+ result.value.close();
295
+ } catch (error) {
296
+ errors.push(error);
297
+ }
298
+ }
299
+ if (errors.length)
300
+ throw new AggregateError(errors, "Redis connection cleanup failed");
301
+ })();
302
+ }
303
+ }
304
+
305
+ class RedisClient {
306
+ source;
307
+ lifecycle;
308
+ #controller = new AbortController;
309
+ signal;
310
+ #initialization;
311
+ #disposal;
312
+ #access;
313
+ #target;
314
+ constructor(source, lifecycle = {}) {
315
+ this.source = source;
316
+ this.lifecycle = lifecycle;
317
+ this.signal = lifecycle.signal ? AbortSignal.any([lifecycle.signal, this.#controller.signal]) : this.#controller.signal;
318
+ }
319
+ initialize() {
320
+ this.signal.throwIfAborted();
321
+ return this.#initialization ??= (async () => {
322
+ this.#target = await this.source.resolve();
323
+ this.signal.throwIfAborted();
324
+ if (!this.source.transports.length || this.source.transports.some((transport) => transport.kind !== "tcp")) {
325
+ throw new Error("Redis requires TCP transports");
326
+ }
327
+ if (this.#target.clusterType === "cluster" && this.#target.database !== 0)
328
+ throw new Error("Redis Cluster requires database 0");
329
+ this.#access = new RedisAccess(this.source.transports, this.#target, undefined, { ...this.lifecycle, signal: this.signal });
330
+ })();
331
+ }
332
+ get access() {
333
+ this.signal.throwIfAborted();
334
+ if (!this.#access)
335
+ throw new Error("Redis client is not initialized");
336
+ return this.#access;
337
+ }
338
+ topology() {
339
+ return discoverRedisTopology(this.access, this.target);
340
+ }
341
+ get target() {
342
+ if (!this.#target)
343
+ throw new Error("Redis client is not initialized");
344
+ return this.#target;
345
+ }
346
+ dispose() {
347
+ return this.#disposal ??= (async () => {
348
+ this.#controller.abort(new Error("Redis client disposed"));
349
+ await this.#initialization?.catch(() => {});
350
+ await this.#access?.close();
351
+ })();
352
+ }
353
+ }
354
+ async function openRedis(source, lifecycle = {}) {
355
+ const client = new RedisClient(source, lifecycle);
356
+ try {
357
+ await client.initialize();
358
+ } catch (error) {
359
+ await client.dispose();
360
+ throw error;
361
+ }
362
+ lifecycle.onDispose?.(() => client.dispose());
363
+ return client;
364
+ }
365
+
366
+ export { parseRedisInfo, RedisConnection, RedisAccess, RedisClient, openRedis };
@@ -23,6 +23,10 @@ export declare class KubernetesClient implements Client {
23
23
  */
24
24
  get(ref: ResourceRef, options?: ResourceRequestOptions): Promise<import("./resources").Resource>;
25
25
  list(query: ResourceListQuery, options?: ResourceRequestOptions): Promise<import("./resources").ResourceList>;
26
+ /** Explicit root scope for Nodes and all-namespace lists; no default namespace is inserted.
27
+ * @spec This view borrows root transport, cancellation and cleanup just like namespace views.
28
+ */
29
+ clusterResources(): ResourceAccess;
26
30
  inNamespace(namespace: string): NamespaceResources;
27
31
  exec(namespace: string, target: ExecTarget, command: string[], options?: RunOptions): Promise<import("./executor").ExecResult>;
28
32
  forward(namespace: string, target: KubernetesEndpoint): Promise<import("./service-port-forward").ForwardedEndpoint>;
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  KubernetesClient
3
- } from "../chunk-8kts470p.js";
4
- import"../chunk-4ms037k2.js";
5
- import"../chunk-0s27bgga.js";
3
+ } from "../chunk-c2n8zvyn.js";
6
4
  import"../chunk-2171tajs.js";
7
5
  import"../chunk-j46x7j7c.js";
8
6
  import"../chunk-6fnhzhdb.js";
7
+ import"../chunk-4ms037k2.js";
9
8
  import"../chunk-ftkrjqkc.js";
10
9
  import"../chunk-shsabpc1.js";
10
+ import"../chunk-0s27bgga.js";
11
11
  import"../chunk-a9htb6cr.js";
12
12
  import"../chunk-20kv4wc3.js";
13
13
  import"../chunk-v3hxvnrc.js";