@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.
@@ -0,0 +1,115 @@
1
+ import type { ErrorKind } from "../errors";
2
+ /** CPU cores and bytes; never percentages or unitless Kubernetes quantity strings. */
3
+ export interface ComputeQuantity {
4
+ cpu: number;
5
+ memory: number;
6
+ }
7
+ export interface ComputeIssue {
8
+ source: string;
9
+ kind: ErrorKind;
10
+ message: string;
11
+ code?: string | number;
12
+ }
13
+ export interface ComputeRead<T> {
14
+ status: "complete" | "partial" | "unavailable";
15
+ items: T[];
16
+ issues: ComputeIssue[];
17
+ }
18
+ export interface ComputeUsage extends ComputeQuantity {
19
+ timestamp: string;
20
+ window: string;
21
+ }
22
+ export interface NodeCompute {
23
+ name: string;
24
+ uid: string;
25
+ ready: boolean;
26
+ unschedulable: boolean;
27
+ capacity: ComputeQuantity;
28
+ allocatable: ComputeQuantity;
29
+ }
30
+ export interface PodCompute {
31
+ namespace: string;
32
+ name: string;
33
+ uid: string;
34
+ createdAt?: string;
35
+ node?: string;
36
+ phase: string;
37
+ ready: boolean;
38
+ restarts: number;
39
+ /** Steady-state application and native sidecar container names, for metrics coverage. */
40
+ containers: string[];
41
+ /** Spec-based effective requests, including init/sidecar peak, Pod-level overrides and overhead. */
42
+ requests: ComputeQuantity;
43
+ /** Sum/peak of declared limits, NOT an upper bound when containers have no limit. */
44
+ configuredLimits: ComputeQuantity;
45
+ /** Container counts, not cores/bytes; a Pod-level budget can still apply. */
46
+ containersWithoutLimits: {
47
+ cpu: number;
48
+ memory: number;
49
+ };
50
+ }
51
+ export interface NodeComputeUsage extends ComputeUsage {
52
+ name: string;
53
+ }
54
+ export interface PodComputeUsage extends ComputeUsage {
55
+ namespace: string;
56
+ name: string;
57
+ uid: string;
58
+ }
59
+ export interface PodComputeSummary {
60
+ pods: number;
61
+ activePods: number;
62
+ scheduledPods: number;
63
+ readyPods: number;
64
+ /** Active Pods only; unscheduled requests are demand, not allocated node capacity. */
65
+ requests: ComputeQuantity;
66
+ scheduledRequests: ComputeQuantity;
67
+ configuredLimits: ComputeQuantity;
68
+ /** Container counts, not cores/bytes; a Pod-level budget can still apply. */
69
+ containersWithoutLimits: {
70
+ cpu: number;
71
+ memory: number;
72
+ };
73
+ }
74
+ export interface ClusterCompute {
75
+ environment: string;
76
+ observedAt: string;
77
+ scope: "cluster";
78
+ nodes: ComputeRead<NodeCompute>;
79
+ pods: ComputeRead<PodCompute>;
80
+ usage: ComputeRead<NodeComputeUsage>;
81
+ /** Totals describe acquired evidence only; consult each read's coverage before claiming cluster totals. */
82
+ summary: {
83
+ capacity?: ComputeQuantity;
84
+ allocatable?: ComputeQuantity;
85
+ pods?: PodComputeSummary;
86
+ usage?: ComputeQuantity;
87
+ };
88
+ }
89
+ export interface WorkloadCompute {
90
+ environment: string;
91
+ observedAt: string;
92
+ /** Scope is exactly the supplied instance set, not a claim of complete Service discovery. */
93
+ scope: "instances";
94
+ workloads: readonly {
95
+ namespace: string;
96
+ workload: string;
97
+ pod: string;
98
+ uid: string;
99
+ }[];
100
+ pods: ComputeRead<PodCompute>;
101
+ usage: ComputeRead<PodComputeUsage>;
102
+ summary: {
103
+ pods?: PodComputeSummary;
104
+ usage?: ComputeQuantity;
105
+ };
106
+ }
107
+ export interface ComputeOptions {
108
+ environment: string;
109
+ signal?: AbortSignal;
110
+ /** Whole collection budget, including all pages and metrics reads. */
111
+ timeoutMs: number;
112
+ maxPods: number;
113
+ maxNodes: number;
114
+ pageSize: number;
115
+ }
File without changes
@@ -0,0 +1,18 @@
1
+ import type { ComputeQuantity, PodCompute, PodComputeSummary } from "./compute-model";
2
+ import type { Resource } from "./resources";
3
+ type ObjectValue = Record<string, unknown>;
4
+ export declare function object(value: unknown): ObjectValue;
5
+ export declare function requiredString(value: unknown, field: string): string;
6
+ /**
7
+ * @why client-node 1.4 quantityToScalar uses BigInt for binary suffixes and rejects valid fractional Gi values.
8
+ * @spec Quantities normalize to cores/bytes; invalid or unrepresentable values never become zero usage.
9
+ */
10
+ export declare function parseComputeQuantity(value: unknown): number;
11
+ export declare const zero: () => ComputeQuantity;
12
+ export declare function add(left: ComputeQuantity, right: ComputeQuantity): ComputeQuantity;
13
+ export declare function quantities(value: unknown, required?: boolean): ComputeQuantity;
14
+ export declare function sum(values: readonly ComputeQuantity[]): ComputeQuantity;
15
+ /** Spec projection only: do not claim current cgroup allocation or scheduler admission headroom. */
16
+ export declare function podCompute(value: Resource): PodCompute;
17
+ export declare function summarizePods(pods: readonly PodCompute[]): PodComputeSummary;
18
+ export {};
@@ -0,0 +1,23 @@
1
+ import {
2
+ add,
3
+ object,
4
+ parseComputeQuantity,
5
+ podCompute,
6
+ quantities,
7
+ requiredString,
8
+ sum,
9
+ summarizePods,
10
+ zero
11
+ } from "../chunk-5zgz1z41.js";
12
+ import"../chunk-20kv4wc3.js";
13
+ export {
14
+ zero,
15
+ summarizePods,
16
+ sum,
17
+ requiredString,
18
+ quantities,
19
+ podCompute,
20
+ parseComputeQuantity,
21
+ object,
22
+ add
23
+ };
@@ -0,0 +1,17 @@
1
+ import type { WorkloadInstance } from "@compforge/harness-common";
2
+ import type { ResourceAccess } from "./resources";
3
+ import type { ClusterCompute, ComputeOptions, WorkloadCompute } from "./compute-model";
4
+ export type * from "./compute-model";
5
+ export { parseComputeQuantity } from "./compute-quantity";
6
+ /**
7
+ * @spec Cluster reads are independent: forbidden Nodes/metrics are coverage gaps, not cluster-health failures.
8
+ * @rule access must be an explicit cluster-scoped view, e.g. KubernetesClient.clusterResources().
9
+ * No namespace fallback, client creation or cleanup occurs here.
10
+ */
11
+ export declare function collectClusterCompute(access: ResourceAccess, options: ComputeOptions): Promise<ClusterCompute>;
12
+ /**
13
+ * @spec Whole-Pod accounting deduplicates physical UID across Workload aliases and container defaults.
14
+ * @spec Only caller-supplied instances are read; cluster permissions are not needed.
15
+ * @rule The caller retains discovery coverage. This result never asserts that input covers all Service instances.
16
+ */
17
+ export declare function collectWorkloadCompute(access: ResourceAccess, instances: readonly WorkloadInstance[], options: ComputeOptions): Promise<WorkloadCompute>;
@@ -0,0 +1,211 @@
1
+ import {
2
+ object,
3
+ parseComputeQuantity,
4
+ podCompute,
5
+ quantities,
6
+ requiredString,
7
+ sum,
8
+ summarizePods
9
+ } from "../chunk-5zgz1z41.js";
10
+ import {
11
+ KubernetesError
12
+ } from "../chunk-20kv4wc3.js";
13
+
14
+ // src/kubernetes/compute.ts
15
+ function requestBudget(options) {
16
+ if (!options.environment.trim() || [options.timeoutMs, options.maxPods, options.maxNodes, options.pageSize].some((value) => !Number.isSafeInteger(value) || value <= 0)) {
17
+ throw new KubernetesError("Compute requires environment and positive timeout/item/page budgets", { kind: "invalid_argument" });
18
+ }
19
+ const deadline = performance.now() + options.timeoutMs;
20
+ return () => {
21
+ options.signal?.throwIfAborted();
22
+ const remaining = Math.ceil(deadline - performance.now());
23
+ if (remaining <= 0)
24
+ throw new KubernetesError("Compute collection budget exhausted", { kind: "timeout" });
25
+ return { signal: options.signal, timeoutMs: remaining };
26
+ };
27
+ }
28
+ function issue(source, error) {
29
+ const kind = error instanceof KubernetesError ? error.kind : "operation_failed";
30
+ return {
31
+ source,
32
+ kind,
33
+ code: error instanceof KubernetesError ? error.code : undefined,
34
+ message: kind === "permission_denied" ? "Permission denied" : `Kubernetes compute read failed: ${kind}`
35
+ };
36
+ }
37
+ function finish(items, issues, observed) {
38
+ return { items, issues, status: !issues.length ? "complete" : observed ? "partial" : "unavailable" };
39
+ }
40
+ function gap(source, kind, message) {
41
+ return { source, kind, message };
42
+ }
43
+ async function readPages(access, query, maxItems, options, request, parse, identity) {
44
+ const source = `${query.apiVersion}/${query.resource}`;
45
+ const items = [], issues = [];
46
+ const tokens = new Set, identities = new Set;
47
+ let continuation, observed = false, read = 0;
48
+ try {
49
+ do {
50
+ const page = await access.list({ ...query, limit: Math.min(options.pageSize, maxItems - read), continue: continuation }, request());
51
+ if (!Array.isArray(page.items))
52
+ throw new KubernetesError("List lacks items", { kind: "invalid_response" });
53
+ observed = true;
54
+ for (const item of page.items.slice(0, maxItems - read)) {
55
+ try {
56
+ const parsed = parse(item), key = identity(parsed);
57
+ if (!identities.has(key)) {
58
+ items.push(parsed);
59
+ identities.add(key);
60
+ }
61
+ } catch (error) {
62
+ issues.push(issue(source, error));
63
+ }
64
+ }
65
+ read += page.items.length;
66
+ continuation = page.metadata?.continue || undefined;
67
+ if (read > maxItems || read >= maxItems && continuation) {
68
+ issues.push(gap(source, "limit_exceeded", `Collection limited to ${maxItems} items`));
69
+ break;
70
+ }
71
+ if (continuation && tokens.has(continuation)) {
72
+ issues.push(gap(source, "invalid_response", "Pagination did not advance"));
73
+ break;
74
+ }
75
+ if (continuation)
76
+ tokens.add(continuation);
77
+ } while (continuation);
78
+ } catch (error) {
79
+ options.signal?.throwIfAborted();
80
+ issues.push(issue(source, error));
81
+ }
82
+ return finish(items, issues, observed);
83
+ }
84
+ function nodeCompute(value) {
85
+ const meta = object(value.metadata), status = object(value.status), spec = object(value.spec);
86
+ const conditions = Array.isArray(status.conditions) ? status.conditions.map(object) : [];
87
+ return {
88
+ name: requiredString(meta.name, "node name"),
89
+ uid: requiredString(meta.uid, "node uid"),
90
+ ready: conditions.some((c) => c.type === "Ready" && c.status === "True"),
91
+ unschedulable: spec.unschedulable === true,
92
+ capacity: quantities(status.capacity, true),
93
+ allocatable: quantities(status.allocatable, true)
94
+ };
95
+ }
96
+ function usage(value) {
97
+ const timestamp = requiredString(value.timestamp, "metrics timestamp"), window = requiredString(value.window, "metrics window");
98
+ if (!Number.isFinite(Date.parse(timestamp)))
99
+ throw new KubernetesError("Invalid metrics timestamp", { kind: "invalid_response" });
100
+ return { ...quantities(value.usage, true), timestamp, window };
101
+ }
102
+ async function collectClusterCompute(access, options) {
103
+ const request = requestBudget(options);
104
+ const nodes = await readPages(access, { apiVersion: "v1", resource: "nodes" }, options.maxNodes, options, request, nodeCompute, (n) => n.uid);
105
+ const pods = await readPages(access, { apiVersion: "v1", resource: "pods" }, options.maxPods, options, request, podCompute, (p) => `${p.namespace}/${p.uid}`);
106
+ const metrics = await readPages(access, { apiVersion: "metrics.k8s.io/v1beta1", resource: "nodes" }, options.maxNodes, options, request, (item) => ({ ...usage(item), name: requiredString(item.metadata?.name, "metrics node name") }), (n) => n.name);
107
+ if (nodes.status !== "unavailable" && metrics.status !== "unavailable") {
108
+ const measured = new Set(metrics.items.map((item) => item.name));
109
+ for (const node of nodes.items)
110
+ if (!measured.has(node.name)) {
111
+ metrics.issues.push(gap(`nodes/${node.name}`, "resource_not_found", "Node has no metrics sample"));
112
+ }
113
+ if (metrics.issues.length)
114
+ metrics.status = "partial";
115
+ }
116
+ options.signal?.throwIfAborted();
117
+ return {
118
+ environment: options.environment,
119
+ observedAt: new Date().toISOString(),
120
+ scope: "cluster",
121
+ nodes,
122
+ pods,
123
+ usage: metrics,
124
+ summary: {
125
+ capacity: nodes.status !== "complete" && !nodes.items.length ? undefined : sum(nodes.items.map((n) => n.capacity)),
126
+ allocatable: nodes.status !== "complete" && !nodes.items.length ? undefined : sum(nodes.items.map((n) => n.allocatable)),
127
+ pods: pods.status !== "complete" && !pods.items.length ? undefined : summarizePods(pods.items),
128
+ usage: metrics.status === "unavailable" || !metrics.items.length && (metrics.status !== "complete" || nodes.status !== "complete" || nodes.items.length > 0) ? undefined : sum(metrics.items)
129
+ }
130
+ };
131
+ }
132
+ async function collectWorkloadCompute(access, instances, options) {
133
+ const request = requestBudget(options);
134
+ const selected = new Map;
135
+ for (const instance of instances) {
136
+ if (instance.environment !== options.environment) {
137
+ throw new KubernetesError("Compute instance belongs to a different environment", { kind: "invalid_argument" });
138
+ }
139
+ selected.set(JSON.stringify([instance.namespace, instance.pod, instance.uid]), instance);
140
+ }
141
+ const pods = [], metrics = [];
142
+ const podIssues = [], metricIssues = [];
143
+ if (selected.size > options.maxPods)
144
+ podIssues.push(gap("pods", "limit_exceeded", `Collection limited to ${options.maxPods} Pods`));
145
+ for (const instance of [...selected.values()].slice(0, options.maxPods)) {
146
+ const source = `pods/${instance.namespace}/${instance.pod}`;
147
+ let pod;
148
+ try {
149
+ pod = podCompute(await access.get({ apiVersion: "v1", resource: "pods", namespace: instance.namespace, name: instance.pod }, request()));
150
+ if (pod.uid !== instance.uid || pod.name !== instance.pod || pod.namespace !== instance.namespace) {
151
+ throw new KubernetesError("Pod identity changed after discovery", { kind: "invalid_response" });
152
+ }
153
+ pods.push(pod);
154
+ } catch (error) {
155
+ options.signal?.throwIfAborted();
156
+ podIssues.push(issue(source, error));
157
+ metricIssues.push(gap(source, "operation_failed", "Metrics not collected because Pod identity could not be verified"));
158
+ continue;
159
+ }
160
+ if (pod.phase === "Succeeded" || pod.phase === "Failed")
161
+ continue;
162
+ try {
163
+ const raw = await access.get({ apiVersion: "metrics.k8s.io/v1beta1", resource: "pods", namespace: pod.namespace, name: pod.name }, request());
164
+ if (raw.metadata?.name !== pod.name || raw.metadata?.namespace !== pod.namespace || !Array.isArray(raw.containers)) {
165
+ throw new KubernetesError("Pod metrics lacks identity or containers", { kind: "invalid_response" });
166
+ }
167
+ const names = raw.containers.map((c) => requiredString(object(c).name, "metrics container name"));
168
+ if (new Set(names).size !== names.length || pod.containers.some((name) => !names.includes(name))) {
169
+ throw new KubernetesError("Pod metrics does not cover its containers", { kind: "invalid_response" });
170
+ }
171
+ const values = raw.containers.map((c) => quantities(object(c).usage, true));
172
+ if (!values.length)
173
+ throw new KubernetesError("Empty Pod metrics", { kind: "invalid_response" });
174
+ const measured = {
175
+ ...usage({ ...raw, usage: { cpu: String(sum(values).cpu), memory: String(sum(values).memory) } }),
176
+ namespace: pod.namespace,
177
+ name: pod.name,
178
+ uid: pod.uid
179
+ };
180
+ if (raw.metadata.uid && raw.metadata.uid !== pod.uid || pod.createdAt && Date.parse(measured.timestamp) < Date.parse(pod.createdAt)) {
181
+ throw new KubernetesError("Pod metrics precedes current Pod incarnation", { kind: "invalid_response" });
182
+ }
183
+ metrics.push(measured);
184
+ } catch (error) {
185
+ options.signal?.throwIfAborted();
186
+ metricIssues.push(issue(`metrics/${source}`, error));
187
+ }
188
+ }
189
+ if (selected.size > options.maxPods)
190
+ metricIssues.push(gap("metrics/pods", "limit_exceeded", "Metrics limited to collected Pods"));
191
+ options.signal?.throwIfAborted();
192
+ const podRead = finish(pods, podIssues, pods.length > 0 || !selected.size);
193
+ const metricRead = finish(metrics, metricIssues, metrics.length > 0 || !podIssues.length && pods.every((p) => p.phase === "Succeeded" || p.phase === "Failed"));
194
+ return {
195
+ environment: options.environment,
196
+ observedAt: new Date().toISOString(),
197
+ scope: "instances",
198
+ workloads: instances.map(({ namespace, workload, pod, uid }) => ({ namespace, workload, pod, uid })),
199
+ pods: podRead,
200
+ usage: metricRead,
201
+ summary: {
202
+ pods: podRead.status === "unavailable" ? undefined : summarizePods(pods),
203
+ usage: metricRead.status === "unavailable" ? undefined : sum(metrics)
204
+ }
205
+ };
206
+ }
207
+ export {
208
+ parseComputeQuantity,
209
+ collectWorkloadCompute,
210
+ collectClusterCompute
211
+ };
@@ -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 {
12
12
  clientKey
13
13
  } from "../chunk-a9htb6cr.js";
@@ -1,6 +1,7 @@
1
1
  import type { Client } from "../client";
2
2
  import type { ConnectionSource, ClientLifecycle } from "../datasource";
3
- import type { TcpTransport } from "../transport";
3
+ import { type TcpTransport } from "../transport";
4
+ import { type RedisTopologyConfig, type RedisTopology } from "./topology";
4
5
  import { createClient, type RedisClientType } from "@redis/client";
5
6
  export interface RedisEndpoint {
6
7
  host: string;
@@ -17,6 +18,10 @@ export interface RedisConnectionConfig extends RedisCredentials {
17
18
  database: number;
18
19
  useSsl: boolean;
19
20
  timeoutMs: number;
21
+ /** Per-route mapping and handshake budget; defaults to timeoutMs. */
22
+ connectTimeoutMs?: number;
23
+ /** Total acquisition budget; defaults to connectTimeoutMs times the number of routes. */
24
+ acquireTimeoutMs?: number;
20
25
  }
21
26
  export type RedisEndpointMapper = (endpoint: RedisEndpoint) => Promise<RedisMappedEndpoint>;
22
27
  type NodeRedisClient = RedisClientType;
@@ -25,7 +30,10 @@ export interface RedisManagedConnection extends RedisConnectionApi {
25
30
  readonly isReady: boolean;
26
31
  close(): void;
27
32
  }
28
- export type RedisConnectionFactory = (endpoint: RedisEndpoint, mapped: RedisMappedEndpoint, config: RedisConnectionConfig) => Promise<RedisManagedConnection>;
33
+ export type RedisConnectionFactory = (endpoint: RedisEndpoint, mapped: RedisMappedEndpoint, config: RedisConnectionConfig & {
34
+ signal?: AbortSignal;
35
+ connectSignal?: AbortSignal;
36
+ }) => Promise<RedisManagedConnection>;
29
37
  /** Redis INFO wire 文本解析;协议适配留在 infra,领域层只接收结构化字段。 */
30
38
  export declare function parseRedisInfo(raw: string): Record<string, unknown>;
31
39
  export interface RedisConnectionApi {
@@ -44,14 +52,19 @@ export interface RedisAccessApi {
44
52
  connection(endpoint: RedisEndpoint, database: number, credentials?: RedisCredentials): Promise<RedisConnectionApi>;
45
53
  close(): Promise<void>;
46
54
  }
55
+ /** @spec A command is sent once; a later operation may acquire a new connection after failure. */
47
56
  export declare class RedisConnection implements RedisManagedConnection {
48
57
  #private;
49
58
  readonly endpoint: RedisEndpoint;
50
59
  private readonly client;
51
60
  private readonly timeoutMs;
52
61
  private readonly lastError;
62
+ private readonly signal?;
53
63
  private constructor();
54
- static connect(endpoint: RedisEndpoint, mapped: RedisMappedEndpoint, config: RedisConnectionConfig, clientFactory?: NodeRedisClientFactory): Promise<RedisConnection>;
64
+ static connect(endpoint: RedisEndpoint, mapped: RedisMappedEndpoint, config: RedisConnectionConfig & {
65
+ signal?: AbortSignal;
66
+ connectSignal?: AbortSignal;
67
+ }, clientFactory?: NodeRedisClientFactory): Promise<RedisConnection>;
55
68
  get isReady(): boolean;
56
69
  ping(): Promise<string>;
57
70
  info(section?: string): Promise<Record<string, unknown>>;
@@ -64,25 +77,31 @@ export declare class RedisConnection implements RedisManagedConnection {
64
77
  }>;
65
78
  close(): void;
66
79
  }
80
+ /**
81
+ * @spec Only connection establishment can advance to another transport; protocol operations are never replayed.
82
+ * @why Every topology node needs the same route policy, credentials, deadline and root cancellation.
83
+ */
67
84
  export declare class RedisAccess implements RedisAccessApi {
68
85
  #private;
69
- private readonly transport;
70
86
  private readonly baseConfig;
71
87
  private readonly connectionFactory;
72
- constructor(transport: TcpTransport, baseConfig: Omit<RedisConnectionConfig, "database">, connectionFactory?: RedisConnectionFactory);
88
+ private readonly lifecycle;
89
+ constructor(transports: TcpTransport | readonly TcpTransport[], baseConfig: Omit<RedisConnectionConfig, "database">, connectionFactory?: RedisConnectionFactory, lifecycle?: ClientLifecycle);
73
90
  connection(endpoint: RedisEndpoint, database: number, credentials?: RedisCredentials): Promise<RedisManagedConnection>;
74
91
  close(): Promise<void>;
75
92
  }
76
- export interface RedisDataSourceTarget extends RedisConnectionConfig {
77
- endpoints: RedisEndpoint[];
78
- }
93
+ /** Connection configuration includes topology; diagnostics do not need a second target model. */
94
+ export type RedisDataSourceTarget = RedisConnectionConfig & RedisTopologyConfig;
79
95
  export declare class RedisClient implements Client {
80
96
  #private;
81
97
  private readonly source;
98
+ private readonly lifecycle;
82
99
  readonly signal: AbortSignal;
83
100
  constructor(source: ConnectionSource<RedisDataSourceTarget>, lifecycle?: ClientLifecycle);
84
101
  initialize(): Promise<void>;
85
102
  get access(): RedisAccess;
103
+ /** Refresh topology for this observation, while retaining reusable node connections. */
104
+ topology(): Promise<RedisTopology>;
86
105
  get target(): RedisDataSourceTarget;
87
106
  dispose(): Promise<void>;
88
107
  }
@@ -4,7 +4,15 @@ import {
4
4
  RedisConnection,
5
5
  openRedis,
6
6
  parseRedisInfo
7
- } from "../chunk-ajhdd4hp.js";
7
+ } from "../chunk-yp6tcw22.js";
8
+ import"../chunk-5tbzf9wd.js";
9
+ import"../chunk-20kv4wc3.js";
10
+ import"../chunk-h8fwx0k7.js";
11
+ import"../chunk-wv4jwhvj.js";
12
+ import"../chunk-631vce5z.js";
13
+ import"../chunk-hmah9t8e.js";
14
+ import"../chunk-mn6dwsvh.js";
15
+ import"../chunk-ssj93brx.js";
8
16
  export {
9
17
  parseRedisInfo,
10
18
  openRedis,
@@ -2,16 +2,23 @@ import {
2
2
  collectRedisKeyStats,
3
3
  redisGroupedSampleSizes
4
4
  } from "../chunk-jr9y0ghr.js";
5
- import {
6
- discoverRedisTopology
7
- } from "../chunk-c6z919qb.js";
8
5
  import {
9
6
  RedisAccess,
10
7
  RedisClient,
11
8
  RedisConnection,
12
9
  openRedis,
13
10
  parseRedisInfo
14
- } from "../chunk-ajhdd4hp.js";
11
+ } from "../chunk-yp6tcw22.js";
12
+ import {
13
+ discoverRedisTopology
14
+ } from "../chunk-5tbzf9wd.js";
15
+ import"../chunk-20kv4wc3.js";
16
+ import"../chunk-h8fwx0k7.js";
17
+ import"../chunk-wv4jwhvj.js";
18
+ import"../chunk-631vce5z.js";
19
+ import"../chunk-hmah9t8e.js";
20
+ import"../chunk-mn6dwsvh.js";
21
+ import"../chunk-ssj93brx.js";
15
22
  export {
16
23
  redisGroupedSampleSizes,
17
24
  parseRedisInfo,
@@ -1,13 +1,16 @@
1
1
  import type { RedisAccessApi, RedisCredentials, RedisEndpoint } from "./client";
2
- export interface RedisTopologyConfig extends RedisCredentials {
2
+ /** Endpoints are standalone/cluster seeds or Sentinel seeds according to clusterType. */
3
+ export type RedisTopologyConfig = RedisCredentials & {
3
4
  endpoints: RedisEndpoint[];
4
5
  database: number;
5
- clusterType: "single" | "sentinel" | "cluster";
6
- sentinelHosts: RedisEndpoint[];
6
+ } & ({
7
+ clusterType: "single" | "cluster";
8
+ } | {
9
+ clusterType: "sentinel";
7
10
  sentinelMasterName: string;
8
11
  sentinelUsername?: string;
9
12
  sentinelPassword?: string;
10
- }
13
+ });
11
14
  export interface RedisSlotRange {
12
15
  start: number;
13
16
  end: number;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  discoverRedisTopology
3
- } from "../chunk-c6z919qb.js";
3
+ } from "../chunk-5tbzf9wd.js";
4
4
  export {
5
5
  discoverRedisTopology
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compforge/harness-toolbox",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Shared infrastructure clients, transports and bounded data capture",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -116,6 +116,9 @@
116
116
  "@types/node": "^22",
117
117
  "@types/ws": "^8.18.1"
118
118
  },
119
+ "overrides": {
120
+ "@compforge/harness-common": "file:../common"
121
+ },
119
122
  "publishConfig": {
120
123
  "access": "public"
121
124
  },