@compforge/harness-toolbox 0.4.1 → 0.4.3
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 +51 -27
- package/dist/{chunk-c6z919qb.js → chunk-5tbzf9wd.js} +1 -1
- package/dist/chunk-grpygk6f.js +157 -0
- package/dist/chunk-kkts16w9.js +131 -0
- package/dist/chunk-sqbew04a.js +65 -0
- package/dist/chunk-yp6tcw22.js +366 -0
- package/dist/file/client.d.ts +12 -7
- package/dist/file/client.js +4 -2
- package/dist/file/index.d.ts +1 -1
- package/dist/file/index.js +3 -3
- package/dist/file/local.d.ts +5 -5
- package/dist/file/local.js +2 -2
- package/dist/file/pod.d.ts +7 -6
- package/dist/file/pod.js +2 -2
- package/dist/redis/client.d.ts +27 -8
- package/dist/redis/client.js +9 -1
- package/dist/redis/index.js +11 -4
- package/dist/redis/topology.d.ts +7 -4
- package/dist/redis/topology.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-ajhdd4hp.js +0 -267
- package/dist/chunk-ba883sdp.js +0 -60
- package/dist/chunk-mxyvpy79.js +0 -62
- package/dist/chunk-mzdz6wwc.js +0 -91
|
@@ -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 };
|
package/dist/file/client.d.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
import type { FileClient, FileReadOptions } from "@compforge/harness-common";
|
|
1
|
+
import type { FileClient, FileReadOptions, FileReadResult } from "@compforge/harness-common";
|
|
2
2
|
export interface FileClientOptions {
|
|
3
3
|
concurrency: number;
|
|
4
4
|
}
|
|
5
|
+
export interface FileCaptureSink {
|
|
6
|
+
receive(index: number, bytes: Uint8Array): void;
|
|
7
|
+
complete(index: number, error?: Error): void;
|
|
8
|
+
}
|
|
5
9
|
export declare function fileClientOptions(options: FileClientOptions): Readonly<FileClientOptions>;
|
|
6
|
-
|
|
10
|
+
export declare function filePaths(paths: readonly string[]): readonly string[];
|
|
11
|
+
/** Group lifecycle and budgets; a per-file failure releases its partial bytes and preserves sibling results. */
|
|
7
12
|
export declare abstract class BoundedFileClient implements FileClient {
|
|
8
13
|
#private;
|
|
9
|
-
readonly
|
|
10
|
-
constructor(
|
|
14
|
+
readonly paths: readonly string[];
|
|
15
|
+
constructor(paths: readonly string[], options: FileClientOptions, signal: AbortSignal);
|
|
11
16
|
initialize(): Promise<void>;
|
|
12
17
|
protected initializeAccess(): Promise<void>;
|
|
13
|
-
protected abstract capture(signal: AbortSignal, limits:
|
|
14
|
-
/** @spec
|
|
15
|
-
read(options: FileReadOptions): Promise<
|
|
18
|
+
protected abstract capture(signal: AbortSignal, limits: FileReadOptions, sink: FileCaptureSink): Promise<void>;
|
|
19
|
+
/** @spec Enforce budgets while streaming; file failures discard only that file, never publish partial bytes. */
|
|
20
|
+
read(options: FileReadOptions): Promise<readonly FileReadResult[]>;
|
|
16
21
|
dispose(): Promise<void>;
|
|
17
22
|
}
|
package/dist/file/client.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BoundedFileClient,
|
|
3
|
-
fileClientOptions
|
|
4
|
-
|
|
3
|
+
fileClientOptions,
|
|
4
|
+
filePaths
|
|
5
|
+
} from "../chunk-kkts16w9.js";
|
|
5
6
|
import"../chunk-20kv4wc3.js";
|
|
6
7
|
import"../chunk-v3hxvnrc.js";
|
|
7
8
|
export {
|
|
9
|
+
filePaths,
|
|
8
10
|
fileClientOptions,
|
|
9
11
|
BoundedFileClient
|
|
10
12
|
};
|
package/dist/file/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { FileDataSource, type FileClient, type FileReadOptions } from "@compforge/harness-common";
|
|
1
|
+
export { FileDataSource, type FileClient, type FileReadOptions, type FileReadResult } from "@compforge/harness-common";
|
|
2
2
|
export { FileError } from "../errors";
|
|
3
3
|
export type { FileClientOptions } from "./client";
|
|
4
4
|
export { LocalFileDataSource, LocalFileClient } from "./local";
|
package/dist/file/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PodFileClient,
|
|
3
3
|
PodFileDataSource
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-grpygk6f.js";
|
|
5
5
|
import {
|
|
6
6
|
LocalFileClient,
|
|
7
7
|
LocalFileDataSource
|
|
8
|
-
} from "../chunk-
|
|
9
|
-
import"../chunk-
|
|
8
|
+
} from "../chunk-sqbew04a.js";
|
|
9
|
+
import"../chunk-kkts16w9.js";
|
|
10
10
|
import {
|
|
11
11
|
FileError
|
|
12
12
|
} from "../chunk-20kv4wc3.js";
|
package/dist/file/local.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { FileDataSource, type ClientManager, type FileReadOptions } from "@compforge/harness-common";
|
|
2
|
-
import { BoundedFileClient, type FileClientOptions } from "./client";
|
|
2
|
+
import { BoundedFileClient, type FileClientOptions, type FileCaptureSink } from "./client";
|
|
3
3
|
export declare class LocalFileClient extends BoundedFileClient {
|
|
4
|
-
protected capture(signal: AbortSignal, limits:
|
|
4
|
+
protected capture(signal: AbortSignal, limits: FileReadOptions, sink: FileCaptureSink): Promise<void>;
|
|
5
5
|
}
|
|
6
|
-
/** @spec Resolve
|
|
6
|
+
/** @spec Resolve paths once at declaration; each group read reopens them to observe replaced mounted files. */
|
|
7
7
|
export declare class LocalFileDataSource extends FileDataSource<LocalFileClient> {
|
|
8
8
|
#private;
|
|
9
|
-
readonly
|
|
9
|
+
readonly paths: readonly string[];
|
|
10
10
|
readonly clientKey: string;
|
|
11
|
-
constructor(
|
|
11
|
+
constructor(paths: readonly string[], options: FileClientOptions);
|
|
12
12
|
createClient(_clients: Pick<ClientManager, "get">, signal: AbortSignal): LocalFileClient;
|
|
13
13
|
}
|
package/dist/file/local.js
CHANGED
package/dist/file/pod.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FileDataSource, type ClientManager, type ClientProvider, type FileReadOptions } from "@compforge/harness-common";
|
|
2
2
|
import type { KubernetesClient } from "../kubernetes/client";
|
|
3
|
-
import { BoundedFileClient, type FileClientOptions } from "./client";
|
|
3
|
+
import { BoundedFileClient, type FileClientOptions, type FileCaptureSink } from "./client";
|
|
4
4
|
export interface PodFileTarget {
|
|
5
5
|
namespace: string;
|
|
6
6
|
pod: string;
|
|
@@ -11,21 +11,22 @@ export declare class PodFileClient extends BoundedFileClient {
|
|
|
11
11
|
#private;
|
|
12
12
|
readonly target: Readonly<PodFileTarget>;
|
|
13
13
|
private readonly acquire;
|
|
14
|
-
constructor(
|
|
14
|
+
constructor(paths: readonly string[], target: Readonly<PodFileTarget>, options: FileClientOptions, signal: AbortSignal, acquire: () => Promise<PodFileAccess>);
|
|
15
15
|
protected initializeAccess(): Promise<void>;
|
|
16
|
-
|
|
16
|
+
/** @spec The whole declared group uses exactly one exec; file failures are records, never implicit retries. */
|
|
17
|
+
protected capture(signal: AbortSignal, limits: FileReadOptions, sink: FileCaptureSink): Promise<void>;
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
19
|
-
* @spec One explicit Pod/container
|
|
20
|
+
* @spec One explicit Pod/container and one caller-declared acquisition group; discovery belongs to consumers.
|
|
20
21
|
* @rule Borrow Kubernetes access from the root manager; file clients never dispose that shared dependency.
|
|
21
22
|
* @see {@link component://../../../docs/toolbox.md}
|
|
22
23
|
*/
|
|
23
24
|
export declare class PodFileDataSource extends FileDataSource<PodFileClient> {
|
|
24
25
|
#private;
|
|
25
26
|
private readonly kubernetes;
|
|
26
|
-
readonly
|
|
27
|
+
readonly paths: readonly string[];
|
|
27
28
|
readonly target: Readonly<PodFileTarget>;
|
|
28
29
|
readonly clientKey: string;
|
|
29
|
-
constructor(kubernetes: ClientProvider<PodFileAccess>, target: PodFileTarget,
|
|
30
|
+
constructor(kubernetes: ClientProvider<PodFileAccess>, target: PodFileTarget, paths: readonly string[], options: FileClientOptions);
|
|
30
31
|
createClient(clients: Pick<ClientManager, "get">, signal: AbortSignal): PodFileClient;
|
|
31
32
|
}
|
package/dist/file/pod.js
CHANGED
package/dist/redis/client.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Client } from "../client";
|
|
2
2
|
import type { ConnectionSource, ClientLifecycle } from "../datasource";
|
|
3
|
-
import type
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
}
|
package/dist/redis/client.js
CHANGED
|
@@ -4,7 +4,15 @@ import {
|
|
|
4
4
|
RedisConnection,
|
|
5
5
|
openRedis,
|
|
6
6
|
parseRedisInfo
|
|
7
|
-
} from "../chunk-
|
|
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,
|
package/dist/redis/index.js
CHANGED
|
@@ -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-
|
|
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,
|
package/dist/redis/topology.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { RedisAccessApi, RedisCredentials, RedisEndpoint } from "./client";
|
|
2
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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;
|