@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
package/dist/redis/topology.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@compforge/harness-toolbox",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Shared infrastructure clients, transports and bounded data capture",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"README.md"
|
|
100
100
|
],
|
|
101
101
|
"dependencies": {
|
|
102
|
-
"@compforge/harness-common": "0.5.
|
|
102
|
+
"@compforge/harness-common": "0.5.4",
|
|
103
103
|
"@aws-sdk/client-s3": "3.1133.0",
|
|
104
104
|
"@kubernetes/client-node": "1.4.0",
|
|
105
105
|
"@opensearch-project/opensearch": "3.6.0",
|
package/dist/chunk-ajhdd4hp.js
DELETED
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
// src/redis/client.ts
|
|
2
|
-
import { createClient } from "@redis/client";
|
|
3
|
-
function stringValue(value) {
|
|
4
|
-
if (Buffer.isBuffer(value))
|
|
5
|
-
return value.toString("utf8");
|
|
6
|
-
return String(value ?? "");
|
|
7
|
-
}
|
|
8
|
-
function scalar(value) {
|
|
9
|
-
if (/^-?(?:\d+|\d+\.\d+)$/.test(value))
|
|
10
|
-
return Number(value);
|
|
11
|
-
return value;
|
|
12
|
-
}
|
|
13
|
-
function parseRedisInfo(raw) {
|
|
14
|
-
const result = {};
|
|
15
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
16
|
-
if (!line || line.startsWith("#"))
|
|
17
|
-
continue;
|
|
18
|
-
const separator = line.indexOf(":");
|
|
19
|
-
if (separator < 1)
|
|
20
|
-
continue;
|
|
21
|
-
const name = line.slice(0, separator);
|
|
22
|
-
const value = line.slice(separator + 1);
|
|
23
|
-
if (/^db\d+$/.test(name)) {
|
|
24
|
-
result[name] = Object.fromEntries(value.split(",").map((item) => {
|
|
25
|
-
const index = item.indexOf("=");
|
|
26
|
-
return [item.slice(0, index), scalar(item.slice(index + 1))];
|
|
27
|
-
}));
|
|
28
|
-
} else {
|
|
29
|
-
result[name] = scalar(value);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return result;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
class RedisConnection {
|
|
36
|
-
endpoint;
|
|
37
|
-
client;
|
|
38
|
-
timeoutMs;
|
|
39
|
-
lastError;
|
|
40
|
-
constructor(endpoint, client, timeoutMs, lastError) {
|
|
41
|
-
this.endpoint = endpoint;
|
|
42
|
-
this.client = client;
|
|
43
|
-
this.timeoutMs = timeoutMs;
|
|
44
|
-
this.lastError = lastError;
|
|
45
|
-
}
|
|
46
|
-
static async connect(endpoint, mapped, config, clientFactory = createClient) {
|
|
47
|
-
const baseSocket = {
|
|
48
|
-
host: mapped.host,
|
|
49
|
-
port: mapped.port,
|
|
50
|
-
connectTimeout: config.timeoutMs,
|
|
51
|
-
reconnectStrategy: (retries) => retries >= 2 ? false : 100 * 2 ** retries
|
|
52
|
-
};
|
|
53
|
-
const socket = config.useSsl ? {
|
|
54
|
-
...baseSocket,
|
|
55
|
-
tls: true,
|
|
56
|
-
servername: mapped.servername ?? endpoint.host
|
|
57
|
-
} : {
|
|
58
|
-
...baseSocket,
|
|
59
|
-
tls: false
|
|
60
|
-
};
|
|
61
|
-
const client = clientFactory({
|
|
62
|
-
socket,
|
|
63
|
-
database: config.database,
|
|
64
|
-
username: config.username,
|
|
65
|
-
password: config.password,
|
|
66
|
-
commandOptions: { timeout: config.timeoutMs }
|
|
67
|
-
});
|
|
68
|
-
let lastError;
|
|
69
|
-
client.on("error", (err) => {
|
|
70
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
71
|
-
});
|
|
72
|
-
try {
|
|
73
|
-
await client.connect();
|
|
74
|
-
} catch (error) {
|
|
75
|
-
if (client.isOpen)
|
|
76
|
-
client.destroy();
|
|
77
|
-
throw error;
|
|
78
|
-
}
|
|
79
|
-
return new RedisConnection(endpoint, client, config.timeoutMs, () => lastError);
|
|
80
|
-
}
|
|
81
|
-
get isReady() {
|
|
82
|
-
return this.client.isReady;
|
|
83
|
-
}
|
|
84
|
-
async#execute(operationName, operation) {
|
|
85
|
-
try {
|
|
86
|
-
return await this.#executeOnce(operationName, operation);
|
|
87
|
-
} catch (err) {
|
|
88
|
-
if (this.client.isOpen && !this.client.isReady)
|
|
89
|
-
return this.#executeOnce(operationName, operation);
|
|
90
|
-
throw err;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
async#executeOnce(operationName, operation) {
|
|
94
|
-
if (!this.client.isOpen) {
|
|
95
|
-
const reason = this.lastError()?.message;
|
|
96
|
-
throw new Error(`Redis connection is closed${reason ? `: ${reason}` : ""}`);
|
|
97
|
-
}
|
|
98
|
-
let timer;
|
|
99
|
-
const deadline = new Promise((_, reject) => {
|
|
100
|
-
timer = setTimeout(() => {
|
|
101
|
-
reject(new Error(`Redis ${operationName} timed out after ${this.timeoutMs}ms (${this.endpoint.host}:${this.endpoint.port})`));
|
|
102
|
-
this.close();
|
|
103
|
-
}, this.timeoutMs);
|
|
104
|
-
});
|
|
105
|
-
try {
|
|
106
|
-
return await Promise.race([Promise.resolve().then(operation), deadline]);
|
|
107
|
-
} finally {
|
|
108
|
-
if (timer)
|
|
109
|
-
clearTimeout(timer);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
ping() {
|
|
113
|
-
return this.#execute("PING", () => this.client.ping());
|
|
114
|
-
}
|
|
115
|
-
async info(section) {
|
|
116
|
-
const raw = await this.#execute(section ? `INFO ${section}` : "INFO", () => section ? this.client.sendCommand(["INFO", section]) : this.client.sendCommand(["INFO"]));
|
|
117
|
-
return parseRedisInfo(stringValue(raw));
|
|
118
|
-
}
|
|
119
|
-
async dbSize() {
|
|
120
|
-
return Number(await this.#execute("DBSIZE", () => this.client.sendCommand(["DBSIZE"])));
|
|
121
|
-
}
|
|
122
|
-
command(args) {
|
|
123
|
-
return this.#execute(args[0]?.toUpperCase() ?? "command", () => this.client.sendCommand(args));
|
|
124
|
-
}
|
|
125
|
-
async pipeline(commands) {
|
|
126
|
-
const commandNames = new Set(commands.map((command) => command[0]?.toUpperCase()).filter(Boolean));
|
|
127
|
-
const operationName = commandNames.size === 1 ? `${[...commandNames][0]} pipeline` : "pipeline";
|
|
128
|
-
return await this.#execute(operationName, () => {
|
|
129
|
-
const pipeline = this.client.multi();
|
|
130
|
-
for (const command of commands)
|
|
131
|
-
pipeline.addCommand(command);
|
|
132
|
-
return pipeline.execAsPipeline();
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
async scan(cursor, count) {
|
|
136
|
-
const reply = await this.command(["SCAN", cursor, "COUNT", String(count)]);
|
|
137
|
-
if (!Array.isArray(reply) || !Array.isArray(reply[1]))
|
|
138
|
-
throw new Error("Redis SCAN 返回格式无效");
|
|
139
|
-
return { cursor: stringValue(reply[0]), keys: reply[1].map(stringValue) };
|
|
140
|
-
}
|
|
141
|
-
close() {
|
|
142
|
-
if (this.client.isOpen)
|
|
143
|
-
this.client.destroy();
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
class RedisAccess {
|
|
148
|
-
transport;
|
|
149
|
-
baseConfig;
|
|
150
|
-
connectionFactory;
|
|
151
|
-
#connections = new Map;
|
|
152
|
-
#closing;
|
|
153
|
-
constructor(transport, baseConfig, connectionFactory = RedisConnection.connect) {
|
|
154
|
-
this.transport = transport;
|
|
155
|
-
this.baseConfig = baseConfig;
|
|
156
|
-
this.connectionFactory = connectionFactory;
|
|
157
|
-
}
|
|
158
|
-
async connection(endpoint, database, credentials = this.baseConfig) {
|
|
159
|
-
if (this.#closing)
|
|
160
|
-
throw new Error("Redis access is closed");
|
|
161
|
-
const key = [
|
|
162
|
-
endpoint.host,
|
|
163
|
-
endpoint.port,
|
|
164
|
-
database,
|
|
165
|
-
credentials.username ?? "",
|
|
166
|
-
credentials.password ?? ""
|
|
167
|
-
].join("\x00");
|
|
168
|
-
let pending = this.#connections.get(key);
|
|
169
|
-
if (pending) {
|
|
170
|
-
const connection = await pending;
|
|
171
|
-
if (this.#closing)
|
|
172
|
-
throw new Error("Redis access is closed");
|
|
173
|
-
if (connection.isReady)
|
|
174
|
-
return connection;
|
|
175
|
-
if (this.#connections.get(key) === pending) {
|
|
176
|
-
this.#connections.delete(key);
|
|
177
|
-
connection.close();
|
|
178
|
-
} else {
|
|
179
|
-
return this.connection(endpoint, database, credentials);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
pending = this.transport.connect(endpoint).then((mapped) => this.connectionFactory(endpoint, mapped, {
|
|
183
|
-
...this.baseConfig,
|
|
184
|
-
...credentials,
|
|
185
|
-
database
|
|
186
|
-
}));
|
|
187
|
-
this.#connections.set(key, pending);
|
|
188
|
-
pending.catch(() => {
|
|
189
|
-
if (this.#connections.get(key) === pending)
|
|
190
|
-
this.#connections.delete(key);
|
|
191
|
-
});
|
|
192
|
-
return await pending;
|
|
193
|
-
}
|
|
194
|
-
close() {
|
|
195
|
-
return this.#closing ??= (async () => {
|
|
196
|
-
const settled = await Promise.allSettled(this.#connections.values());
|
|
197
|
-
this.#connections.clear();
|
|
198
|
-
const errors = [];
|
|
199
|
-
for (const result of settled)
|
|
200
|
-
if (result.status === "fulfilled") {
|
|
201
|
-
try {
|
|
202
|
-
result.value.close();
|
|
203
|
-
} catch (error) {
|
|
204
|
-
errors.push(error);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
if (errors.length)
|
|
208
|
-
throw new AggregateError(errors, "Redis connection cleanup failed");
|
|
209
|
-
})();
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
class RedisClient {
|
|
214
|
-
source;
|
|
215
|
-
#controller = new AbortController;
|
|
216
|
-
signal;
|
|
217
|
-
#initialization;
|
|
218
|
-
#disposal;
|
|
219
|
-
#access;
|
|
220
|
-
#target;
|
|
221
|
-
constructor(source, lifecycle = {}) {
|
|
222
|
-
this.source = source;
|
|
223
|
-
this.signal = lifecycle.signal ? AbortSignal.any([lifecycle.signal, this.#controller.signal]) : this.#controller.signal;
|
|
224
|
-
}
|
|
225
|
-
initialize() {
|
|
226
|
-
this.signal.throwIfAborted();
|
|
227
|
-
return this.#initialization ??= (async () => {
|
|
228
|
-
this.#target = await this.source.resolve();
|
|
229
|
-
this.signal.throwIfAborted();
|
|
230
|
-
const transport = this.source.transports[0];
|
|
231
|
-
if (!transport || transport.kind !== "tcp")
|
|
232
|
-
throw new Error("Redis requires a TCP transport");
|
|
233
|
-
this.#access = new RedisAccess(transport, this.#target);
|
|
234
|
-
})();
|
|
235
|
-
}
|
|
236
|
-
get access() {
|
|
237
|
-
this.signal.throwIfAborted();
|
|
238
|
-
if (!this.#access)
|
|
239
|
-
throw new Error("Redis client is not initialized");
|
|
240
|
-
return this.#access;
|
|
241
|
-
}
|
|
242
|
-
get target() {
|
|
243
|
-
if (!this.#target)
|
|
244
|
-
throw new Error("Redis client is not initialized");
|
|
245
|
-
return this.#target;
|
|
246
|
-
}
|
|
247
|
-
dispose() {
|
|
248
|
-
return this.#disposal ??= (async () => {
|
|
249
|
-
this.#controller.abort(new Error("Redis client disposed"));
|
|
250
|
-
await this.#initialization?.catch(() => {});
|
|
251
|
-
await this.#access?.close();
|
|
252
|
-
})();
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
async function openRedis(source, lifecycle = {}) {
|
|
256
|
-
const client = new RedisClient(source, lifecycle);
|
|
257
|
-
try {
|
|
258
|
-
await client.initialize();
|
|
259
|
-
} catch (error) {
|
|
260
|
-
await client.dispose();
|
|
261
|
-
throw error;
|
|
262
|
-
}
|
|
263
|
-
lifecycle.onDispose?.(() => client.dispose());
|
|
264
|
-
return client;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export { parseRedisInfo, RedisConnection, RedisAccess, RedisClient, openRedis };
|
package/dist/chunk-ba883sdp.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BoundedFileClient,
|
|
3
|
-
fileClientOptions
|
|
4
|
-
} from "./chunk-mzdz6wwc.js";
|
|
5
|
-
import {
|
|
6
|
-
FileError
|
|
7
|
-
} from "./chunk-20kv4wc3.js";
|
|
8
|
-
|
|
9
|
-
// src/file/local.ts
|
|
10
|
-
import { constants } from "node:fs";
|
|
11
|
-
import { open } from "node:fs/promises";
|
|
12
|
-
import { resolve } from "node:path";
|
|
13
|
-
import { clientKey, FileDataSource } from "@compforge/harness-common";
|
|
14
|
-
class LocalFileClient extends BoundedFileClient {
|
|
15
|
-
async capture(signal, limits, receive) {
|
|
16
|
-
try {
|
|
17
|
-
const file = await open(this.path, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
18
|
-
try {
|
|
19
|
-
signal.throwIfAborted();
|
|
20
|
-
if (!(await file.stat()).isFile()) {
|
|
21
|
-
throw new FileError(`Not a regular file: ${this.path}`, { kind: "unsupported_operation" });
|
|
22
|
-
}
|
|
23
|
-
const stream = file.createReadStream({ autoClose: false, signal, highWaterMark: Math.min(limits.maxBytes + 1, 64 * 1024) });
|
|
24
|
-
for await (const bytes of stream)
|
|
25
|
-
receive(bytes);
|
|
26
|
-
} finally {
|
|
27
|
-
await file.close();
|
|
28
|
-
}
|
|
29
|
-
} catch (cause) {
|
|
30
|
-
signal.throwIfAborted();
|
|
31
|
-
if (cause instanceof FileError)
|
|
32
|
-
throw cause;
|
|
33
|
-
const code = cause?.code;
|
|
34
|
-
throw new FileError(`Cannot read local file: ${this.path}`, {
|
|
35
|
-
kind: code === "ENOENT" || code === "ENOTDIR" ? "resource_not_found" : code === "EACCES" || code === "EPERM" ? "permission_denied" : "operation_failed",
|
|
36
|
-
code,
|
|
37
|
-
cause
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
class LocalFileDataSource extends FileDataSource {
|
|
44
|
-
path;
|
|
45
|
-
clientKey;
|
|
46
|
-
#options;
|
|
47
|
-
constructor(path, options) {
|
|
48
|
-
super();
|
|
49
|
-
if (!path || path.includes("\x00"))
|
|
50
|
-
throw new FileError("File path must be nonempty and contain no NUL", { kind: "invalid_argument" });
|
|
51
|
-
this.path = resolve(path);
|
|
52
|
-
this.#options = fileClientOptions(options);
|
|
53
|
-
this.clientKey = clientKey("local-file", { path: this.path, options: this.#options });
|
|
54
|
-
}
|
|
55
|
-
createClient(_clients, signal) {
|
|
56
|
-
return new LocalFileClient(this.path, this.#options, signal);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export { LocalFileClient, LocalFileDataSource };
|
package/dist/chunk-mxyvpy79.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BoundedFileClient,
|
|
3
|
-
fileClientOptions
|
|
4
|
-
} from "./chunk-mzdz6wwc.js";
|
|
5
|
-
import {
|
|
6
|
-
FileError
|
|
7
|
-
} from "./chunk-20kv4wc3.js";
|
|
8
|
-
|
|
9
|
-
// src/file/pod.ts
|
|
10
|
-
import { posix } from "node:path";
|
|
11
|
-
import { clientKey, FileDataSource } from "@compforge/harness-common";
|
|
12
|
-
class PodFileClient extends BoundedFileClient {
|
|
13
|
-
target;
|
|
14
|
-
acquire;
|
|
15
|
-
#kubernetes;
|
|
16
|
-
constructor(path, target, options, signal, acquire) {
|
|
17
|
-
super(path, options, signal);
|
|
18
|
-
this.target = target;
|
|
19
|
-
this.acquire = acquire;
|
|
20
|
-
}
|
|
21
|
-
async initializeAccess() {
|
|
22
|
-
this.#kubernetes = await this.acquire();
|
|
23
|
-
}
|
|
24
|
-
async capture(signal, limits, receive) {
|
|
25
|
-
const result = await this.#kubernetes.exec(this.target.namespace, this.target, ["head", "-c", String(limits.maxBytes + 1), this.path], { signal, timeoutMs: limits.timeoutMs, collectStdout: false, onStdoutBytes: receive });
|
|
26
|
-
signal.throwIfAborted();
|
|
27
|
-
if (!result.ok)
|
|
28
|
-
throw new FileError(`Cannot read Pod file: ${this.target.namespace}/${this.target.pod}/${this.target.container} ${this.path}`, { kind: result.timedOut ? "timeout" : "operation_failed", code: result.exitCode ?? undefined });
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
class PodFileDataSource extends FileDataSource {
|
|
33
|
-
kubernetes;
|
|
34
|
-
path;
|
|
35
|
-
target;
|
|
36
|
-
clientKey;
|
|
37
|
-
#options;
|
|
38
|
-
constructor(kubernetes, target, path, options) {
|
|
39
|
-
super();
|
|
40
|
-
this.kubernetes = kubernetes;
|
|
41
|
-
if (!posix.isAbsolute(path) || path.includes("\x00")) {
|
|
42
|
-
throw new FileError("Pod file path must be absolute and contain no NUL", { kind: "invalid_argument" });
|
|
43
|
-
}
|
|
44
|
-
if ([target.namespace, target.pod, target.container].some((value) => !value.trim())) {
|
|
45
|
-
throw new FileError("Pod file namespace, Pod and Container are required", { kind: "invalid_argument" });
|
|
46
|
-
}
|
|
47
|
-
this.path = path;
|
|
48
|
-
this.target = Object.freeze({ namespace: target.namespace, pod: target.pod, container: target.container });
|
|
49
|
-
this.#options = fileClientOptions(options);
|
|
50
|
-
this.clientKey = clientKey("pod-file", {
|
|
51
|
-
access: kubernetes.clientKey,
|
|
52
|
-
target: this.target,
|
|
53
|
-
path: this.path,
|
|
54
|
-
options: this.#options
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
createClient(clients, signal) {
|
|
58
|
-
return new PodFileClient(this.path, this.target, this.#options, signal, () => clients.get(this.kubernetes));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export { PodFileClient, PodFileDataSource };
|
package/dist/chunk-mzdz6wwc.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
FileError,
|
|
3
|
-
ToolboxError
|
|
4
|
-
} from "./chunk-20kv4wc3.js";
|
|
5
|
-
import {
|
|
6
|
-
ConcurrencyPool
|
|
7
|
-
} from "./chunk-v3hxvnrc.js";
|
|
8
|
-
|
|
9
|
-
// src/file/client.ts
|
|
10
|
-
function fileClientOptions(options) {
|
|
11
|
-
if (!Number.isSafeInteger(options.concurrency) || options.concurrency < 1) {
|
|
12
|
-
throw new FileError("File concurrency must be a positive integer", { kind: "invalid_argument" });
|
|
13
|
-
}
|
|
14
|
-
return Object.freeze({ concurrency: options.concurrency });
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
class BoundedFileClient {
|
|
18
|
-
path;
|
|
19
|
-
#controller = new AbortController;
|
|
20
|
-
#signal;
|
|
21
|
-
#pool;
|
|
22
|
-
#operations = new Set;
|
|
23
|
-
#ready = false;
|
|
24
|
-
#disposal;
|
|
25
|
-
constructor(path, options, signal) {
|
|
26
|
-
this.path = path;
|
|
27
|
-
this.#pool = new ConcurrencyPool(fileClientOptions(options).concurrency);
|
|
28
|
-
this.#signal = AbortSignal.any([signal, this.#controller.signal]);
|
|
29
|
-
}
|
|
30
|
-
async initialize() {
|
|
31
|
-
this.#signal.throwIfAborted();
|
|
32
|
-
await this.initializeAccess();
|
|
33
|
-
this.#signal.throwIfAborted();
|
|
34
|
-
this.#ready = true;
|
|
35
|
-
}
|
|
36
|
-
async initializeAccess() {}
|
|
37
|
-
async read(options) {
|
|
38
|
-
this.#signal.throwIfAborted();
|
|
39
|
-
if (!this.#ready)
|
|
40
|
-
throw new FileError(`File client is not initialized: ${this.path}`, { kind: "operation_failed" });
|
|
41
|
-
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0 || options.maxBytes === Number.MAX_SAFE_INTEGER || !Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > 2147483647) {
|
|
42
|
-
throw new FileError(`Invalid file read limits: ${this.path}`, { kind: "invalid_argument" });
|
|
43
|
-
}
|
|
44
|
-
const { maxBytes, timeoutMs } = options;
|
|
45
|
-
const controller = new AbortController;
|
|
46
|
-
const signal = AbortSignal.any([this.#signal, controller.signal, ...options.signal ? [options.signal] : []]);
|
|
47
|
-
const timer = setTimeout(() => controller.abort(new FileError(`File read timed out: ${this.path}`, { kind: "timeout" })), timeoutMs);
|
|
48
|
-
const operation = this.#pool.run(async () => {
|
|
49
|
-
const chunks = [];
|
|
50
|
-
let size = 0;
|
|
51
|
-
await this.capture(signal, { maxBytes, timeoutMs }, (bytes) => {
|
|
52
|
-
if (signal.aborted)
|
|
53
|
-
return;
|
|
54
|
-
if (bytes.byteLength > maxBytes - size) {
|
|
55
|
-
controller.abort(new FileError(`File exceeds ${maxBytes} bytes: ${this.path}`, { kind: "limit_exceeded" }));
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
chunks.push(Uint8Array.from(bytes));
|
|
59
|
-
size += bytes.byteLength;
|
|
60
|
-
});
|
|
61
|
-
signal.throwIfAborted();
|
|
62
|
-
const result = new Uint8Array(size);
|
|
63
|
-
let offset = 0;
|
|
64
|
-
for (const chunk of chunks) {
|
|
65
|
-
result.set(chunk, offset);
|
|
66
|
-
offset += chunk.byteLength;
|
|
67
|
-
}
|
|
68
|
-
return result;
|
|
69
|
-
}, signal);
|
|
70
|
-
this.#operations.add(operation);
|
|
71
|
-
try {
|
|
72
|
-
return await operation;
|
|
73
|
-
} catch (cause) {
|
|
74
|
-
signal.throwIfAborted();
|
|
75
|
-
if (cause instanceof ToolboxError)
|
|
76
|
-
throw cause;
|
|
77
|
-
throw new FileError(`Cannot read file: ${this.path}`, { kind: "operation_failed", cause });
|
|
78
|
-
} finally {
|
|
79
|
-
clearTimeout(timer);
|
|
80
|
-
this.#operations.delete(operation);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
dispose() {
|
|
84
|
-
return this.#disposal ??= (async () => {
|
|
85
|
-
this.#controller.abort(new Error(`File client disposed: ${this.path}`));
|
|
86
|
-
await Promise.allSettled(this.#operations);
|
|
87
|
-
})();
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export { fileClientOptions, BoundedFileClient };
|