@wnlx/o2-client 1.0.0
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 +41 -0
- package/dist/index.cjs +620 -0
- package/dist/index.d.cts +98 -0
- package/dist/index.d.mts +98 -0
- package/dist/index.mjs +595 -0
- package/package.json +32 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import { createConnection, isIP } from "node:net";
|
|
2
|
+
import { connect } from "node:tls";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
//#region src/helpers/hmac.ts
|
|
5
|
+
function calculateResponse(challenge, clientSecret) {
|
|
6
|
+
return crypto.createHmac("sha256", clientSecret).update(challenge).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
const MAX_BODY_SIZE = 1048576;
|
|
9
|
+
function buildHeader(opcode, streamId, paramLen, flagsLen, bodyLen) {
|
|
10
|
+
validateHeader({
|
|
11
|
+
opcode,
|
|
12
|
+
streamId,
|
|
13
|
+
paramLen,
|
|
14
|
+
flagsLen,
|
|
15
|
+
bodyLen
|
|
16
|
+
});
|
|
17
|
+
const header = /* @__PURE__ */ new Uint8Array(20);
|
|
18
|
+
const view = new DataView(header.buffer);
|
|
19
|
+
view.setUint32(0, opcode, false);
|
|
20
|
+
view.setUint32(4, streamId, false);
|
|
21
|
+
view.setUint32(8, paramLen, false);
|
|
22
|
+
view.setUint32(12, flagsLen, false);
|
|
23
|
+
view.setUint32(16, bodyLen, false);
|
|
24
|
+
return header;
|
|
25
|
+
}
|
|
26
|
+
function parseHeader(header) {
|
|
27
|
+
if (header.byteLength < 20) throw new Error("Incomplete O2 header");
|
|
28
|
+
const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
|
|
29
|
+
const parsed = {
|
|
30
|
+
opcode: view.getUint32(0, false),
|
|
31
|
+
streamId: view.getUint32(4, false),
|
|
32
|
+
paramLen: view.getUint32(8, false),
|
|
33
|
+
flagsLen: view.getUint32(12, false),
|
|
34
|
+
bodyLen: view.getUint32(16, false)
|
|
35
|
+
};
|
|
36
|
+
validateHeader(parsed);
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
function validateHeader(header) {
|
|
40
|
+
const { opcode, streamId, paramLen, flagsLen, bodyLen } = header;
|
|
41
|
+
for (const value of [
|
|
42
|
+
opcode,
|
|
43
|
+
streamId,
|
|
44
|
+
paramLen,
|
|
45
|
+
flagsLen,
|
|
46
|
+
bodyLen
|
|
47
|
+
]) if (!Number.isInteger(value) || value < 0 || value > 4294967295) throw new Error("Invalid O2 header value");
|
|
48
|
+
if (paramLen > 65536 || flagsLen > 65536 || bodyLen > 1048576) throw new Error("O2 frame exceeds size limits");
|
|
49
|
+
}
|
|
50
|
+
function parseMessage(message, header) {
|
|
51
|
+
const { opcode, streamId, paramLen, flagsLen, bodyLen } = header;
|
|
52
|
+
const expectedLength = 20 + paramLen + flagsLen + bodyLen;
|
|
53
|
+
if (message.byteLength < expectedLength) throw new Error(`Incomplete O2 frame: expected ${expectedLength} bytes, got ${message.byteLength}`);
|
|
54
|
+
let offset = 20;
|
|
55
|
+
const param = message.subarray(offset, offset + paramLen);
|
|
56
|
+
offset += paramLen;
|
|
57
|
+
const flags = message.subarray(offset, offset + flagsLen);
|
|
58
|
+
offset += flagsLen;
|
|
59
|
+
return {
|
|
60
|
+
opcode,
|
|
61
|
+
streamId,
|
|
62
|
+
param,
|
|
63
|
+
flags,
|
|
64
|
+
body: message.subarray(offset, offset + bodyLen)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function parseFlags(flags) {
|
|
68
|
+
const flagPairs = flags.split(",");
|
|
69
|
+
const flagObject = Object.create(null);
|
|
70
|
+
for (const pair of flagPairs) {
|
|
71
|
+
const [key, value] = pair.split("=");
|
|
72
|
+
if (key && value) flagObject[key.trim()] = value.trim();
|
|
73
|
+
}
|
|
74
|
+
return flagObject;
|
|
75
|
+
}
|
|
76
|
+
const encoder = new TextEncoder();
|
|
77
|
+
const decoder = new TextDecoder();
|
|
78
|
+
var FrameReader = class {
|
|
79
|
+
buffer = /* @__PURE__ */ new Uint8Array(20);
|
|
80
|
+
offset = 0;
|
|
81
|
+
header = null;
|
|
82
|
+
get incomplete() {
|
|
83
|
+
return this.offset !== 0;
|
|
84
|
+
}
|
|
85
|
+
*push(data) {
|
|
86
|
+
let position = 0;
|
|
87
|
+
while (position < data.length) {
|
|
88
|
+
const count = Math.min(this.buffer.length - this.offset, data.length - position);
|
|
89
|
+
this.buffer.set(data.subarray(position, position + count), this.offset);
|
|
90
|
+
this.offset += count;
|
|
91
|
+
position += count;
|
|
92
|
+
if (this.offset !== this.buffer.length) continue;
|
|
93
|
+
if (!this.header) {
|
|
94
|
+
this.header = parseHeader(this.buffer);
|
|
95
|
+
const length = 20 + this.header.paramLen + this.header.flagsLen + this.header.bodyLen;
|
|
96
|
+
if (length > 20) {
|
|
97
|
+
const frame = new Uint8Array(length);
|
|
98
|
+
frame.set(this.buffer);
|
|
99
|
+
this.buffer = frame;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const parsed = parseMessage(this.buffer, this.header);
|
|
104
|
+
this.buffer = /* @__PURE__ */ new Uint8Array(20);
|
|
105
|
+
this.offset = 0;
|
|
106
|
+
this.header = null;
|
|
107
|
+
yield parsed;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
var SocketWriter = class {
|
|
112
|
+
socket;
|
|
113
|
+
queue = [];
|
|
114
|
+
pendingBytes = 0;
|
|
115
|
+
failure = null;
|
|
116
|
+
constructor(socket) {
|
|
117
|
+
this.socket = socket;
|
|
118
|
+
}
|
|
119
|
+
write(bytes) {
|
|
120
|
+
if (this.failure) return Promise.reject(this.failure);
|
|
121
|
+
if (this.pendingBytes + bytes.length > 4194304) return Promise.reject(/* @__PURE__ */ new Error("Outbound queue exceeds limit"));
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
this.queue.push({
|
|
124
|
+
bytes,
|
|
125
|
+
offset: 0,
|
|
126
|
+
resolve,
|
|
127
|
+
reject
|
|
128
|
+
});
|
|
129
|
+
this.pendingBytes += bytes.length;
|
|
130
|
+
if (this.queue.length === 1) this.drain();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
frame(opcode, streamId, param = "", flags = /* @__PURE__ */ new Uint8Array(), body = /* @__PURE__ */ new Uint8Array()) {
|
|
134
|
+
const params = encoder.encode(param);
|
|
135
|
+
const header = buildHeader(opcode, streamId, params.length, flags.length, body.length);
|
|
136
|
+
const bytes = new Uint8Array(header.length + params.length + flags.length + body.length);
|
|
137
|
+
let offset = 0;
|
|
138
|
+
bytes.set(header, offset);
|
|
139
|
+
offset += header.length;
|
|
140
|
+
bytes.set(params, offset);
|
|
141
|
+
offset += params.length;
|
|
142
|
+
bytes.set(flags, offset);
|
|
143
|
+
offset += flags.length;
|
|
144
|
+
bytes.set(body, offset);
|
|
145
|
+
return this.write(bytes);
|
|
146
|
+
}
|
|
147
|
+
drain() {
|
|
148
|
+
try {
|
|
149
|
+
while (this.queue.length) {
|
|
150
|
+
const pending = this.queue[0];
|
|
151
|
+
const written = this.socket.write(pending.bytes.subarray(pending.offset));
|
|
152
|
+
if (written < 0) throw new Error("Socket closed during write");
|
|
153
|
+
pending.offset += written;
|
|
154
|
+
this.pendingBytes -= written;
|
|
155
|
+
if (pending.offset < pending.bytes.length) return;
|
|
156
|
+
this.queue.shift();
|
|
157
|
+
pending.resolve();
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
this.close(error instanceof Error ? error : new Error(String(error)));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
close(error = /* @__PURE__ */ new Error("Connection closed")) {
|
|
164
|
+
this.failure ??= error;
|
|
165
|
+
for (const pending of this.queue.splice(0)) pending.reject(this.failure);
|
|
166
|
+
this.pendingBytes = 0;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region src/sdk/client.ts
|
|
171
|
+
var O2Error = class extends Error {
|
|
172
|
+
streamId;
|
|
173
|
+
constructor(message, streamId) {
|
|
174
|
+
super(message);
|
|
175
|
+
this.streamId = streamId;
|
|
176
|
+
this.name = "O2Error";
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
function deferred() {
|
|
180
|
+
let resolve;
|
|
181
|
+
let reject;
|
|
182
|
+
return {
|
|
183
|
+
promise: new Promise((res, rej) => {
|
|
184
|
+
resolve = res;
|
|
185
|
+
reject = rej;
|
|
186
|
+
}),
|
|
187
|
+
resolve,
|
|
188
|
+
reject
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
var AsyncByteQueue = class {
|
|
192
|
+
highWaterMark;
|
|
193
|
+
chunks = [];
|
|
194
|
+
readers = [];
|
|
195
|
+
drainWaiters = [];
|
|
196
|
+
buffered = 0;
|
|
197
|
+
ended = false;
|
|
198
|
+
failure = null;
|
|
199
|
+
constructor(highWaterMark) {
|
|
200
|
+
this.highWaterMark = highWaterMark;
|
|
201
|
+
}
|
|
202
|
+
async push(chunk) {
|
|
203
|
+
if (this.ended || this.failure) return;
|
|
204
|
+
const reader = this.readers.shift();
|
|
205
|
+
if (reader) {
|
|
206
|
+
reader.resolve({
|
|
207
|
+
value: chunk,
|
|
208
|
+
done: false
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
this.chunks.push(chunk);
|
|
213
|
+
this.buffered += chunk.byteLength;
|
|
214
|
+
if (this.buffered > this.highWaterMark) await new Promise((resolve) => this.drainWaiters.push(resolve));
|
|
215
|
+
}
|
|
216
|
+
close() {
|
|
217
|
+
if (this.ended || this.failure) return;
|
|
218
|
+
this.ended = true;
|
|
219
|
+
for (const reader of this.readers.splice(0)) reader.resolve({
|
|
220
|
+
value: void 0,
|
|
221
|
+
done: true
|
|
222
|
+
});
|
|
223
|
+
this.releaseDrainWaiters();
|
|
224
|
+
}
|
|
225
|
+
fail(error) {
|
|
226
|
+
if (this.failure || this.ended) return;
|
|
227
|
+
this.failure = error;
|
|
228
|
+
for (const reader of this.readers.splice(0)) reader.reject(error);
|
|
229
|
+
this.releaseDrainWaiters();
|
|
230
|
+
}
|
|
231
|
+
async next() {
|
|
232
|
+
if (this.failure) throw this.failure;
|
|
233
|
+
const chunk = this.chunks.shift();
|
|
234
|
+
if (chunk) {
|
|
235
|
+
this.buffered -= chunk.byteLength;
|
|
236
|
+
if (this.buffered <= this.highWaterMark / 2) this.releaseDrainWaiters();
|
|
237
|
+
return {
|
|
238
|
+
value: chunk,
|
|
239
|
+
done: false
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (this.ended) return {
|
|
243
|
+
value: void 0,
|
|
244
|
+
done: true
|
|
245
|
+
};
|
|
246
|
+
const waiter = deferred();
|
|
247
|
+
this.readers.push(waiter);
|
|
248
|
+
return waiter.promise;
|
|
249
|
+
}
|
|
250
|
+
[Symbol.asyncIterator]() {
|
|
251
|
+
return this;
|
|
252
|
+
}
|
|
253
|
+
releaseDrainWaiters() {
|
|
254
|
+
for (const resolve of this.drainWaiters.splice(0)) resolve();
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
var O2Client = class {
|
|
258
|
+
options;
|
|
259
|
+
socket = null;
|
|
260
|
+
writer = null;
|
|
261
|
+
reader = new FrameReader();
|
|
262
|
+
incomingWork = Promise.resolve();
|
|
263
|
+
pendingBytes = 0;
|
|
264
|
+
writeBlocked = false;
|
|
265
|
+
nextStreamId = 1;
|
|
266
|
+
connected = false;
|
|
267
|
+
closing = false;
|
|
268
|
+
challengeWaiter = null;
|
|
269
|
+
controlWaiter = null;
|
|
270
|
+
requests = /* @__PURE__ */ new Map();
|
|
271
|
+
transfers = /* @__PURE__ */ new Map();
|
|
272
|
+
host;
|
|
273
|
+
port;
|
|
274
|
+
transferBufferBytes;
|
|
275
|
+
constructor(options) {
|
|
276
|
+
this.options = options;
|
|
277
|
+
this.host = options.host ?? "127.0.0.1";
|
|
278
|
+
this.port = options.port ?? 52202;
|
|
279
|
+
this.transferBufferBytes = options.transferBufferBytes ?? 4194304;
|
|
280
|
+
}
|
|
281
|
+
async connect() {
|
|
282
|
+
if (this.connected) return;
|
|
283
|
+
if (this.socket) throw new Error("O2 connection is already opening");
|
|
284
|
+
const tlsOptions = this.options.tls;
|
|
285
|
+
const socket = tlsOptions ? connect({
|
|
286
|
+
host: this.host,
|
|
287
|
+
port: this.port,
|
|
288
|
+
...typeof tlsOptions === "object" ? tlsOptions : {},
|
|
289
|
+
servername: typeof tlsOptions === "object" && tlsOptions.servername !== void 0 ? tlsOptions.servername : isIP(this.host) ? void 0 : this.host
|
|
290
|
+
}) : createConnection({
|
|
291
|
+
host: this.host,
|
|
292
|
+
port: this.port
|
|
293
|
+
});
|
|
294
|
+
this.socket = socket;
|
|
295
|
+
this.writer = new SocketWriter({ write: (data) => {
|
|
296
|
+
if (socket.destroyed) return -1;
|
|
297
|
+
if (this.writeBlocked) return 0;
|
|
298
|
+
this.writeBlocked = !socket.write(data);
|
|
299
|
+
return data.length;
|
|
300
|
+
} });
|
|
301
|
+
socket.on("drain", () => {
|
|
302
|
+
this.writeBlocked = false;
|
|
303
|
+
this.writer?.drain();
|
|
304
|
+
});
|
|
305
|
+
socket.on("data", (incoming) => {
|
|
306
|
+
socket.pause();
|
|
307
|
+
const data = new Uint8Array(incoming);
|
|
308
|
+
this.pendingBytes += data.byteLength;
|
|
309
|
+
if (this.pendingBytes > 4194304) {
|
|
310
|
+
socket.destroy(/* @__PURE__ */ new Error("Incoming O2 queue exceeds limit"));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
this.incomingWork = this.incomingWork.then(async () => {
|
|
314
|
+
for (const frame of this.reader.push(data)) await this.handleFrame(frame);
|
|
315
|
+
}).catch((error) => socket.destroy(error)).finally(() => {
|
|
316
|
+
this.pendingBytes -= data.byteLength;
|
|
317
|
+
if (!socket.destroyed && this.pendingBytes === 0) socket.resume();
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
socket.on("error", (error) => this.failAll(error));
|
|
321
|
+
socket.on("close", () => {
|
|
322
|
+
this.connected = false;
|
|
323
|
+
this.writer?.close();
|
|
324
|
+
this.failAll(/* @__PURE__ */ new Error("O2 connection closed"));
|
|
325
|
+
this.socket = null;
|
|
326
|
+
this.writer = null;
|
|
327
|
+
});
|
|
328
|
+
await new Promise((resolve, reject) => {
|
|
329
|
+
const readyEvent = tlsOptions ? "secureConnect" : "connect";
|
|
330
|
+
const onReady = () => {
|
|
331
|
+
socket.off("error", onError);
|
|
332
|
+
resolve();
|
|
333
|
+
};
|
|
334
|
+
const onError = (error) => {
|
|
335
|
+
socket.off(readyEvent, onReady);
|
|
336
|
+
reject(error);
|
|
337
|
+
};
|
|
338
|
+
socket.once(readyEvent, onReady);
|
|
339
|
+
socket.once("error", onError);
|
|
340
|
+
});
|
|
341
|
+
this.challengeWaiter = deferred();
|
|
342
|
+
await this.writer.frame(0, 0);
|
|
343
|
+
const challenge = await this.challengeWaiter.promise;
|
|
344
|
+
this.controlWaiter = deferred();
|
|
345
|
+
await this.writer.frame(1, 0, `${this.options.keyId}:${calculateResponse(challenge, this.options.secret)}`);
|
|
346
|
+
await this.controlWaiter.promise;
|
|
347
|
+
this.connected = true;
|
|
348
|
+
}
|
|
349
|
+
async close() {
|
|
350
|
+
if (!this.socket) return;
|
|
351
|
+
this.closing = true;
|
|
352
|
+
try {
|
|
353
|
+
if (this.writer && !this.socket.destroyed) {
|
|
354
|
+
this.controlWaiter = deferred();
|
|
355
|
+
await this.writer.frame(255, 0);
|
|
356
|
+
await Promise.race([this.controlWaiter.promise, new Promise((resolve) => setTimeout(() => resolve("timeout"), 1e3))]);
|
|
357
|
+
}
|
|
358
|
+
} finally {
|
|
359
|
+
this.socket?.end();
|
|
360
|
+
this.closing = false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async listNamespaces() {
|
|
364
|
+
const message = await this.request(2);
|
|
365
|
+
if (!message) return [];
|
|
366
|
+
return message.split(",").map((entry) => {
|
|
367
|
+
const separator = entry.lastIndexOf(":");
|
|
368
|
+
return {
|
|
369
|
+
name: entry.slice(0, separator),
|
|
370
|
+
length: Number(entry.slice(separator + 1))
|
|
371
|
+
};
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
async listObjects(namespace) {
|
|
375
|
+
const message = await this.request(2, namespace);
|
|
376
|
+
if (!message) return [];
|
|
377
|
+
return message.split(",").map((entry) => {
|
|
378
|
+
const separator = entry.lastIndexOf(":");
|
|
379
|
+
return {
|
|
380
|
+
key: entry.slice(0, separator),
|
|
381
|
+
size: Number(entry.slice(separator + 1))
|
|
382
|
+
};
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
async createNamespace(namespace) {
|
|
386
|
+
await this.request(3, namespace);
|
|
387
|
+
}
|
|
388
|
+
async deleteNamespace(namespace) {
|
|
389
|
+
await this.request(4, namespace);
|
|
390
|
+
}
|
|
391
|
+
async metadata(namespace, key) {
|
|
392
|
+
const message = await this.request(11, `${namespace}:${key}`);
|
|
393
|
+
const separator = message.lastIndexOf(":");
|
|
394
|
+
if (separator <= 0) throw new O2Error("Invalid METADATA response");
|
|
395
|
+
return {
|
|
396
|
+
sha256: message.slice(0, separator),
|
|
397
|
+
size: Number(message.slice(separator + 1))
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
async deleteObject(namespace, key) {
|
|
401
|
+
return this.request(9, `${namespace}:${key}`);
|
|
402
|
+
}
|
|
403
|
+
async putObject(namespace, key, data, options = {}) {
|
|
404
|
+
async function* chunks() {
|
|
405
|
+
const chunkSize = options.chunkSize ?? 262144;
|
|
406
|
+
for (let offset = 0; offset < data.length; offset += chunkSize) yield data.subarray(offset, Math.min(offset + chunkSize, data.length));
|
|
407
|
+
}
|
|
408
|
+
await this.putObjectStream(namespace, key, chunks());
|
|
409
|
+
}
|
|
410
|
+
async putObjectStream(namespace, key, source) {
|
|
411
|
+
this.ensureConnected();
|
|
412
|
+
const streamId = this.allocateStreamId();
|
|
413
|
+
const identifier = `${namespace}:${key}`;
|
|
414
|
+
try {
|
|
415
|
+
await this.requestOnStream(streamId, 5, identifier);
|
|
416
|
+
for await (const chunk of source) {
|
|
417
|
+
if (!(chunk instanceof Uint8Array)) throw new Error("PUT source must yield Uint8Array chunks");
|
|
418
|
+
if (!chunk.length) continue;
|
|
419
|
+
if (chunk.length > 1048576) throw new Error(`PUT chunk exceeds ${MAX_BODY_SIZE} bytes`);
|
|
420
|
+
await this.requestOnStream(streamId, 6, identifier, /* @__PURE__ */ new Uint8Array(), Uint8Array.from(chunk));
|
|
421
|
+
}
|
|
422
|
+
await this.requestOnStream(streamId, 7, identifier);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
if (this.writer && this.socket && !this.socket.destroyed) await this.writer.frame(8, streamId, identifier).catch(() => {});
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async signObject(namespace, key, expirySeconds) {
|
|
429
|
+
if (!Number.isSafeInteger(expirySeconds) || expirySeconds <= 0) throw new Error("expirySeconds must be a positive integer");
|
|
430
|
+
return this.request(12, `${namespace}:${key}`, encoder.encode(`EXPIRY=${expirySeconds}`));
|
|
431
|
+
}
|
|
432
|
+
async retrieve(namespace, key, options = {}) {
|
|
433
|
+
const chunkSize = options.chunkSize ?? 262144;
|
|
434
|
+
if (!Number.isSafeInteger(chunkSize) || chunkSize < 1 || chunkSize > 1048576) throw new Error(`chunkSize must be between 1 and ${MAX_BODY_SIZE}`);
|
|
435
|
+
if (options.offset !== void 0 && (!Number.isSafeInteger(options.offset) || options.offset < 0)) throw new Error("offset must be a non-negative integer");
|
|
436
|
+
if (options.length !== void 0 && (!Number.isSafeInteger(options.length) || options.length <= 0)) throw new Error("length must be a positive integer");
|
|
437
|
+
if (options.length !== void 0 && options.offset === void 0) throw new Error("length requires offset");
|
|
438
|
+
this.ensureConnected();
|
|
439
|
+
const streamId = this.allocateStreamId();
|
|
440
|
+
const begin = deferred();
|
|
441
|
+
const state = {
|
|
442
|
+
begin,
|
|
443
|
+
queue: new AsyncByteQueue(this.transferBufferBytes),
|
|
444
|
+
identifier: `${namespace}:${key}`,
|
|
445
|
+
namespace,
|
|
446
|
+
key,
|
|
447
|
+
finished: false
|
|
448
|
+
};
|
|
449
|
+
this.transfers.set(streamId, state);
|
|
450
|
+
const flags = [`CHUNK_SIZE=${chunkSize}`];
|
|
451
|
+
if (options.offset !== void 0) flags.push(`OFFSET=${options.offset}`);
|
|
452
|
+
if (options.length !== void 0) flags.push(`LENGTH=${options.length}`);
|
|
453
|
+
try {
|
|
454
|
+
await this.writer.frame(10, streamId, state.identifier, encoder.encode(flags.join(",")));
|
|
455
|
+
} catch (error) {
|
|
456
|
+
this.transfers.delete(streamId);
|
|
457
|
+
throw error;
|
|
458
|
+
}
|
|
459
|
+
return begin.promise;
|
|
460
|
+
}
|
|
461
|
+
async request(opcode, param = "", flags = /* @__PURE__ */ new Uint8Array()) {
|
|
462
|
+
this.ensureConnected();
|
|
463
|
+
const streamId = this.allocateStreamId();
|
|
464
|
+
const waiter = deferred();
|
|
465
|
+
this.requests.set(streamId, waiter);
|
|
466
|
+
try {
|
|
467
|
+
await this.writer.frame(opcode, streamId, param, flags);
|
|
468
|
+
return await waiter.promise;
|
|
469
|
+
} finally {
|
|
470
|
+
this.requests.delete(streamId);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async handleFrame(frame) {
|
|
474
|
+
const message = decoder.decode(frame.param);
|
|
475
|
+
if (frame.streamId === 0) {
|
|
476
|
+
if (frame.opcode === 2) {
|
|
477
|
+
this.challengeWaiter?.resolve(message);
|
|
478
|
+
this.challengeWaiter = null;
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (frame.opcode === 1) {
|
|
482
|
+
this.controlWaiter?.resolve(message);
|
|
483
|
+
this.controlWaiter = null;
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (frame.opcode === 0) {
|
|
487
|
+
const error = new O2Error(message, 0);
|
|
488
|
+
this.controlWaiter?.reject(error);
|
|
489
|
+
this.challengeWaiter?.reject(error);
|
|
490
|
+
this.controlWaiter = null;
|
|
491
|
+
this.challengeWaiter = null;
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
throw new O2Error(`Unexpected control response ${frame.opcode}`, 0);
|
|
495
|
+
}
|
|
496
|
+
const transfer = this.transfers.get(frame.streamId);
|
|
497
|
+
if (transfer) {
|
|
498
|
+
if (frame.opcode === 0) {
|
|
499
|
+
const error = new O2Error(message, frame.streamId);
|
|
500
|
+
transfer.begin.reject(error);
|
|
501
|
+
transfer.queue.fail(error);
|
|
502
|
+
this.transfers.delete(frame.streamId);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (frame.opcode === 3) {
|
|
506
|
+
if (message !== transfer.identifier || frame.body.length) throw new O2Error("Invalid RET_BEGIN", frame.streamId);
|
|
507
|
+
const metadata = parseFlags(decoder.decode(frame.flags));
|
|
508
|
+
const size = Number(metadata.SIZE);
|
|
509
|
+
if (!Number.isSafeInteger(size) || size < 0) throw new O2Error("RET_BEGIN missing valid SIZE", frame.streamId);
|
|
510
|
+
const sha256 = metadata.SHA256;
|
|
511
|
+
const retrieval = {
|
|
512
|
+
streamId: frame.streamId,
|
|
513
|
+
namespace: transfer.namespace,
|
|
514
|
+
key: transfer.key,
|
|
515
|
+
size,
|
|
516
|
+
sha256,
|
|
517
|
+
[Symbol.asyncIterator]: () => transfer.queue,
|
|
518
|
+
cancel: async () => this.cancelTransfer(frame.streamId)
|
|
519
|
+
};
|
|
520
|
+
transfer.begin.resolve(retrieval);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (frame.opcode === 4) {
|
|
524
|
+
if (frame.param.length || frame.flags.length || !frame.body.length) throw new O2Error("Invalid RET_CHUNK", frame.streamId);
|
|
525
|
+
await transfer.queue.push(frame.body.slice());
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (frame.opcode === 5) {
|
|
529
|
+
if (message !== transfer.identifier || frame.flags.length || frame.body.length) throw new O2Error("Invalid RET_END", frame.streamId);
|
|
530
|
+
transfer.finished = true;
|
|
531
|
+
transfer.queue.close();
|
|
532
|
+
this.transfers.delete(frame.streamId);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
throw new O2Error(`Unexpected transfer response ${frame.opcode}`, frame.streamId);
|
|
536
|
+
}
|
|
537
|
+
const request = this.requests.get(frame.streamId);
|
|
538
|
+
if (!request) return;
|
|
539
|
+
if (frame.flags.length || frame.body.length) {
|
|
540
|
+
request.reject(new O2Error("Invalid response payload", frame.streamId));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (frame.opcode === 1) request.resolve(message);
|
|
544
|
+
else if (frame.opcode === 0) request.reject(new O2Error(message, frame.streamId));
|
|
545
|
+
else request.reject(new O2Error(`Unexpected response ${frame.opcode}`, frame.streamId));
|
|
546
|
+
}
|
|
547
|
+
async requestOnStream(streamId, opcode, param = "", flags = /* @__PURE__ */ new Uint8Array(), body = /* @__PURE__ */ new Uint8Array()) {
|
|
548
|
+
this.ensureConnected();
|
|
549
|
+
if (this.requests.has(streamId)) throw new Error(`Stream ${streamId} already has a pending request`);
|
|
550
|
+
const waiter = deferred();
|
|
551
|
+
this.requests.set(streamId, waiter);
|
|
552
|
+
try {
|
|
553
|
+
await this.writer.frame(opcode, streamId, param, flags, body);
|
|
554
|
+
return await waiter.promise;
|
|
555
|
+
} finally {
|
|
556
|
+
this.requests.delete(streamId);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async cancelTransfer(streamId) {
|
|
560
|
+
const transfer = this.transfers.get(streamId);
|
|
561
|
+
if (!transfer) return;
|
|
562
|
+
this.transfers.delete(streamId);
|
|
563
|
+
const error = new O2Error("Transfer cancelled", streamId);
|
|
564
|
+
transfer.begin.reject(error);
|
|
565
|
+
transfer.queue.fail(error);
|
|
566
|
+
if (this.writer && this.socket && !this.socket.destroyed) await this.writer.frame(13, streamId).catch(() => {});
|
|
567
|
+
}
|
|
568
|
+
allocateStreamId() {
|
|
569
|
+
for (let attempts = 0; attempts < 4294967295; attempts++) {
|
|
570
|
+
const id = this.nextStreamId++;
|
|
571
|
+
if (this.nextStreamId > 4294967295) this.nextStreamId = 1;
|
|
572
|
+
if (!this.requests.has(id) && !this.transfers.has(id)) return id;
|
|
573
|
+
}
|
|
574
|
+
throw new Error("No O2 stream IDs available");
|
|
575
|
+
}
|
|
576
|
+
ensureConnected() {
|
|
577
|
+
if (!this.connected || !this.writer || !this.socket || this.socket.destroyed) throw new Error("O2 client is not connected");
|
|
578
|
+
}
|
|
579
|
+
failAll(error) {
|
|
580
|
+
if (this.closing) return;
|
|
581
|
+
this.challengeWaiter?.reject(error);
|
|
582
|
+
this.controlWaiter?.reject(error);
|
|
583
|
+
this.challengeWaiter = null;
|
|
584
|
+
this.controlWaiter = null;
|
|
585
|
+
for (const waiter of this.requests.values()) waiter.reject(error);
|
|
586
|
+
this.requests.clear();
|
|
587
|
+
for (const transfer of this.transfers.values()) {
|
|
588
|
+
transfer.begin.reject(error);
|
|
589
|
+
transfer.queue.fail(error);
|
|
590
|
+
}
|
|
591
|
+
this.transfers.clear();
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
//#endregion
|
|
595
|
+
export { O2Client, O2Error };
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wnlx/o2-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "./dist/index.cjs",
|
|
5
|
+
"types": "./dist/index.d.mts",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"private": false,
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"elysia": "^1.4.30",
|
|
10
|
+
"jose": "^6.2.12",
|
|
11
|
+
"mime-types": "^3.0.2"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.mts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"require": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"build": "tsdown src/sdk/index.ts --format esm,cjs --dts --clean",
|
|
26
|
+
"prepublishOnly": "bun run typecheck && bun run build"
|
|
27
|
+
},
|
|
28
|
+
"type": "module",
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"tsdown": "^0.23.0"
|
|
31
|
+
}
|
|
32
|
+
}
|