@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 ADDED
@@ -0,0 +1,41 @@
1
+ # origin-o2
2
+
3
+ A Bun TCP object-storage prototype with a binary protocol and interactive client.
4
+
5
+ ```sh
6
+ bun install
7
+ HOSTNAME=127.0.0.1 PORT=50202 bun src/index.ts
8
+ ```
9
+
10
+ In another terminal:
11
+
12
+ ```sh
13
+ HOSTNAME=127.0.0.1 PORT=50202 bun src/client.ts
14
+ ```
15
+
16
+ The server uses `BASEPATH=./data` by default and stores objects under `BASEPATH/storage/<namespace>/<key>`. The client saves completed downloads under `./downloads/<namespace>/<key>`, relative to its working directory. `HOSTNAME` defaults to `localhost` only when the environment variable is unset; set it explicitly if your shell already supplies it.
17
+
18
+ The client requests an authentication challenge automatically. Example commands:
19
+
20
+ ```text
21
+ !AUTH admin:admin123
22
+ 0x02
23
+ 0x02 assets
24
+ 0x08 assets:/toy-story-2-airport-car-chase-dub.mp4
25
+ 0x08 assets:/story.txt CHUNK_SIZE=65536
26
+ 0x03 empty-namespace
27
+ 0x04 empty-namespace
28
+ 0x0f
29
+ ```
30
+
31
+ The raw command format is `<opcode> <param/0> <flags/0> <body/0>`; omitted fields default to `0` (empty). Supported commands have no request body. The prototype credentials are `admin:admin123` and `user:user123`; this transport currently uses plain TCP.
32
+
33
+ Downloads stream through bounded buffers to unique `.part` files. A completed transfer atomically replaces its destination; failed or interrupted transfers remove their temporary files and preserve an existing destination. Input remains available during transfers. Commands on one connection execute in order, so another retrieval or listing waits for the active retrieval. Ctrl+C cancels the connection; EOF queues a graceful goodbye after submitted commands.
34
+
35
+ Namespace deletion succeeds only for empty namespaces. Upload opcodes are reserved but not implemented. See [O2_DOC.md](O2_DOC.md) for the implemented protocol; [O2_CONCEPT.md](O2_CONCEPT.md) is an earlier design, not the current wire format.
36
+
37
+ Type-check with:
38
+
39
+ ```sh
40
+ bunx tsc --noEmit
41
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,620 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let node_net = require("node:net");
25
+ let node_tls = require("node:tls");
26
+ let crypto = require("crypto");
27
+ crypto = __toESM(crypto, 1);
28
+ //#region src/helpers/hmac.ts
29
+ function calculateResponse(challenge, clientSecret) {
30
+ return crypto.default.createHmac("sha256", clientSecret).update(challenge).digest("hex");
31
+ }
32
+ const MAX_BODY_SIZE = 1048576;
33
+ function buildHeader(opcode, streamId, paramLen, flagsLen, bodyLen) {
34
+ validateHeader({
35
+ opcode,
36
+ streamId,
37
+ paramLen,
38
+ flagsLen,
39
+ bodyLen
40
+ });
41
+ const header = /* @__PURE__ */ new Uint8Array(20);
42
+ const view = new DataView(header.buffer);
43
+ view.setUint32(0, opcode, false);
44
+ view.setUint32(4, streamId, false);
45
+ view.setUint32(8, paramLen, false);
46
+ view.setUint32(12, flagsLen, false);
47
+ view.setUint32(16, bodyLen, false);
48
+ return header;
49
+ }
50
+ function parseHeader(header) {
51
+ if (header.byteLength < 20) throw new Error("Incomplete O2 header");
52
+ const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
53
+ const parsed = {
54
+ opcode: view.getUint32(0, false),
55
+ streamId: view.getUint32(4, false),
56
+ paramLen: view.getUint32(8, false),
57
+ flagsLen: view.getUint32(12, false),
58
+ bodyLen: view.getUint32(16, false)
59
+ };
60
+ validateHeader(parsed);
61
+ return parsed;
62
+ }
63
+ function validateHeader(header) {
64
+ const { opcode, streamId, paramLen, flagsLen, bodyLen } = header;
65
+ for (const value of [
66
+ opcode,
67
+ streamId,
68
+ paramLen,
69
+ flagsLen,
70
+ bodyLen
71
+ ]) if (!Number.isInteger(value) || value < 0 || value > 4294967295) throw new Error("Invalid O2 header value");
72
+ if (paramLen > 65536 || flagsLen > 65536 || bodyLen > 1048576) throw new Error("O2 frame exceeds size limits");
73
+ }
74
+ function parseMessage(message, header) {
75
+ const { opcode, streamId, paramLen, flagsLen, bodyLen } = header;
76
+ const expectedLength = 20 + paramLen + flagsLen + bodyLen;
77
+ if (message.byteLength < expectedLength) throw new Error(`Incomplete O2 frame: expected ${expectedLength} bytes, got ${message.byteLength}`);
78
+ let offset = 20;
79
+ const param = message.subarray(offset, offset + paramLen);
80
+ offset += paramLen;
81
+ const flags = message.subarray(offset, offset + flagsLen);
82
+ offset += flagsLen;
83
+ return {
84
+ opcode,
85
+ streamId,
86
+ param,
87
+ flags,
88
+ body: message.subarray(offset, offset + bodyLen)
89
+ };
90
+ }
91
+ function parseFlags(flags) {
92
+ const flagPairs = flags.split(",");
93
+ const flagObject = Object.create(null);
94
+ for (const pair of flagPairs) {
95
+ const [key, value] = pair.split("=");
96
+ if (key && value) flagObject[key.trim()] = value.trim();
97
+ }
98
+ return flagObject;
99
+ }
100
+ const encoder = new TextEncoder();
101
+ const decoder = new TextDecoder();
102
+ var FrameReader = class {
103
+ buffer = /* @__PURE__ */ new Uint8Array(20);
104
+ offset = 0;
105
+ header = null;
106
+ get incomplete() {
107
+ return this.offset !== 0;
108
+ }
109
+ *push(data) {
110
+ let position = 0;
111
+ while (position < data.length) {
112
+ const count = Math.min(this.buffer.length - this.offset, data.length - position);
113
+ this.buffer.set(data.subarray(position, position + count), this.offset);
114
+ this.offset += count;
115
+ position += count;
116
+ if (this.offset !== this.buffer.length) continue;
117
+ if (!this.header) {
118
+ this.header = parseHeader(this.buffer);
119
+ const length = 20 + this.header.paramLen + this.header.flagsLen + this.header.bodyLen;
120
+ if (length > 20) {
121
+ const frame = new Uint8Array(length);
122
+ frame.set(this.buffer);
123
+ this.buffer = frame;
124
+ continue;
125
+ }
126
+ }
127
+ const parsed = parseMessage(this.buffer, this.header);
128
+ this.buffer = /* @__PURE__ */ new Uint8Array(20);
129
+ this.offset = 0;
130
+ this.header = null;
131
+ yield parsed;
132
+ }
133
+ }
134
+ };
135
+ var SocketWriter = class {
136
+ socket;
137
+ queue = [];
138
+ pendingBytes = 0;
139
+ failure = null;
140
+ constructor(socket) {
141
+ this.socket = socket;
142
+ }
143
+ write(bytes) {
144
+ if (this.failure) return Promise.reject(this.failure);
145
+ if (this.pendingBytes + bytes.length > 4194304) return Promise.reject(/* @__PURE__ */ new Error("Outbound queue exceeds limit"));
146
+ return new Promise((resolve, reject) => {
147
+ this.queue.push({
148
+ bytes,
149
+ offset: 0,
150
+ resolve,
151
+ reject
152
+ });
153
+ this.pendingBytes += bytes.length;
154
+ if (this.queue.length === 1) this.drain();
155
+ });
156
+ }
157
+ frame(opcode, streamId, param = "", flags = /* @__PURE__ */ new Uint8Array(), body = /* @__PURE__ */ new Uint8Array()) {
158
+ const params = encoder.encode(param);
159
+ const header = buildHeader(opcode, streamId, params.length, flags.length, body.length);
160
+ const bytes = new Uint8Array(header.length + params.length + flags.length + body.length);
161
+ let offset = 0;
162
+ bytes.set(header, offset);
163
+ offset += header.length;
164
+ bytes.set(params, offset);
165
+ offset += params.length;
166
+ bytes.set(flags, offset);
167
+ offset += flags.length;
168
+ bytes.set(body, offset);
169
+ return this.write(bytes);
170
+ }
171
+ drain() {
172
+ try {
173
+ while (this.queue.length) {
174
+ const pending = this.queue[0];
175
+ const written = this.socket.write(pending.bytes.subarray(pending.offset));
176
+ if (written < 0) throw new Error("Socket closed during write");
177
+ pending.offset += written;
178
+ this.pendingBytes -= written;
179
+ if (pending.offset < pending.bytes.length) return;
180
+ this.queue.shift();
181
+ pending.resolve();
182
+ }
183
+ } catch (error) {
184
+ this.close(error instanceof Error ? error : new Error(String(error)));
185
+ }
186
+ }
187
+ close(error = /* @__PURE__ */ new Error("Connection closed")) {
188
+ this.failure ??= error;
189
+ for (const pending of this.queue.splice(0)) pending.reject(this.failure);
190
+ this.pendingBytes = 0;
191
+ }
192
+ };
193
+ //#endregion
194
+ //#region src/sdk/client.ts
195
+ var O2Error = class extends Error {
196
+ streamId;
197
+ constructor(message, streamId) {
198
+ super(message);
199
+ this.streamId = streamId;
200
+ this.name = "O2Error";
201
+ }
202
+ };
203
+ function deferred() {
204
+ let resolve;
205
+ let reject;
206
+ return {
207
+ promise: new Promise((res, rej) => {
208
+ resolve = res;
209
+ reject = rej;
210
+ }),
211
+ resolve,
212
+ reject
213
+ };
214
+ }
215
+ var AsyncByteQueue = class {
216
+ highWaterMark;
217
+ chunks = [];
218
+ readers = [];
219
+ drainWaiters = [];
220
+ buffered = 0;
221
+ ended = false;
222
+ failure = null;
223
+ constructor(highWaterMark) {
224
+ this.highWaterMark = highWaterMark;
225
+ }
226
+ async push(chunk) {
227
+ if (this.ended || this.failure) return;
228
+ const reader = this.readers.shift();
229
+ if (reader) {
230
+ reader.resolve({
231
+ value: chunk,
232
+ done: false
233
+ });
234
+ return;
235
+ }
236
+ this.chunks.push(chunk);
237
+ this.buffered += chunk.byteLength;
238
+ if (this.buffered > this.highWaterMark) await new Promise((resolve) => this.drainWaiters.push(resolve));
239
+ }
240
+ close() {
241
+ if (this.ended || this.failure) return;
242
+ this.ended = true;
243
+ for (const reader of this.readers.splice(0)) reader.resolve({
244
+ value: void 0,
245
+ done: true
246
+ });
247
+ this.releaseDrainWaiters();
248
+ }
249
+ fail(error) {
250
+ if (this.failure || this.ended) return;
251
+ this.failure = error;
252
+ for (const reader of this.readers.splice(0)) reader.reject(error);
253
+ this.releaseDrainWaiters();
254
+ }
255
+ async next() {
256
+ if (this.failure) throw this.failure;
257
+ const chunk = this.chunks.shift();
258
+ if (chunk) {
259
+ this.buffered -= chunk.byteLength;
260
+ if (this.buffered <= this.highWaterMark / 2) this.releaseDrainWaiters();
261
+ return {
262
+ value: chunk,
263
+ done: false
264
+ };
265
+ }
266
+ if (this.ended) return {
267
+ value: void 0,
268
+ done: true
269
+ };
270
+ const waiter = deferred();
271
+ this.readers.push(waiter);
272
+ return waiter.promise;
273
+ }
274
+ [Symbol.asyncIterator]() {
275
+ return this;
276
+ }
277
+ releaseDrainWaiters() {
278
+ for (const resolve of this.drainWaiters.splice(0)) resolve();
279
+ }
280
+ };
281
+ var O2Client = class {
282
+ options;
283
+ socket = null;
284
+ writer = null;
285
+ reader = new FrameReader();
286
+ incomingWork = Promise.resolve();
287
+ pendingBytes = 0;
288
+ writeBlocked = false;
289
+ nextStreamId = 1;
290
+ connected = false;
291
+ closing = false;
292
+ challengeWaiter = null;
293
+ controlWaiter = null;
294
+ requests = /* @__PURE__ */ new Map();
295
+ transfers = /* @__PURE__ */ new Map();
296
+ host;
297
+ port;
298
+ transferBufferBytes;
299
+ constructor(options) {
300
+ this.options = options;
301
+ this.host = options.host ?? "127.0.0.1";
302
+ this.port = options.port ?? 52202;
303
+ this.transferBufferBytes = options.transferBufferBytes ?? 4194304;
304
+ }
305
+ async connect() {
306
+ if (this.connected) return;
307
+ if (this.socket) throw new Error("O2 connection is already opening");
308
+ const tlsOptions = this.options.tls;
309
+ const socket = tlsOptions ? (0, node_tls.connect)({
310
+ host: this.host,
311
+ port: this.port,
312
+ ...typeof tlsOptions === "object" ? tlsOptions : {},
313
+ servername: typeof tlsOptions === "object" && tlsOptions.servername !== void 0 ? tlsOptions.servername : (0, node_net.isIP)(this.host) ? void 0 : this.host
314
+ }) : (0, node_net.createConnection)({
315
+ host: this.host,
316
+ port: this.port
317
+ });
318
+ this.socket = socket;
319
+ this.writer = new SocketWriter({ write: (data) => {
320
+ if (socket.destroyed) return -1;
321
+ if (this.writeBlocked) return 0;
322
+ this.writeBlocked = !socket.write(data);
323
+ return data.length;
324
+ } });
325
+ socket.on("drain", () => {
326
+ this.writeBlocked = false;
327
+ this.writer?.drain();
328
+ });
329
+ socket.on("data", (incoming) => {
330
+ socket.pause();
331
+ const data = new Uint8Array(incoming);
332
+ this.pendingBytes += data.byteLength;
333
+ if (this.pendingBytes > 4194304) {
334
+ socket.destroy(/* @__PURE__ */ new Error("Incoming O2 queue exceeds limit"));
335
+ return;
336
+ }
337
+ this.incomingWork = this.incomingWork.then(async () => {
338
+ for (const frame of this.reader.push(data)) await this.handleFrame(frame);
339
+ }).catch((error) => socket.destroy(error)).finally(() => {
340
+ this.pendingBytes -= data.byteLength;
341
+ if (!socket.destroyed && this.pendingBytes === 0) socket.resume();
342
+ });
343
+ });
344
+ socket.on("error", (error) => this.failAll(error));
345
+ socket.on("close", () => {
346
+ this.connected = false;
347
+ this.writer?.close();
348
+ this.failAll(/* @__PURE__ */ new Error("O2 connection closed"));
349
+ this.socket = null;
350
+ this.writer = null;
351
+ });
352
+ await new Promise((resolve, reject) => {
353
+ const readyEvent = tlsOptions ? "secureConnect" : "connect";
354
+ const onReady = () => {
355
+ socket.off("error", onError);
356
+ resolve();
357
+ };
358
+ const onError = (error) => {
359
+ socket.off(readyEvent, onReady);
360
+ reject(error);
361
+ };
362
+ socket.once(readyEvent, onReady);
363
+ socket.once("error", onError);
364
+ });
365
+ this.challengeWaiter = deferred();
366
+ await this.writer.frame(0, 0);
367
+ const challenge = await this.challengeWaiter.promise;
368
+ this.controlWaiter = deferred();
369
+ await this.writer.frame(1, 0, `${this.options.keyId}:${calculateResponse(challenge, this.options.secret)}`);
370
+ await this.controlWaiter.promise;
371
+ this.connected = true;
372
+ }
373
+ async close() {
374
+ if (!this.socket) return;
375
+ this.closing = true;
376
+ try {
377
+ if (this.writer && !this.socket.destroyed) {
378
+ this.controlWaiter = deferred();
379
+ await this.writer.frame(255, 0);
380
+ await Promise.race([this.controlWaiter.promise, new Promise((resolve) => setTimeout(() => resolve("timeout"), 1e3))]);
381
+ }
382
+ } finally {
383
+ this.socket?.end();
384
+ this.closing = false;
385
+ }
386
+ }
387
+ async listNamespaces() {
388
+ const message = await this.request(2);
389
+ if (!message) return [];
390
+ return message.split(",").map((entry) => {
391
+ const separator = entry.lastIndexOf(":");
392
+ return {
393
+ name: entry.slice(0, separator),
394
+ length: Number(entry.slice(separator + 1))
395
+ };
396
+ });
397
+ }
398
+ async listObjects(namespace) {
399
+ const message = await this.request(2, namespace);
400
+ if (!message) return [];
401
+ return message.split(",").map((entry) => {
402
+ const separator = entry.lastIndexOf(":");
403
+ return {
404
+ key: entry.slice(0, separator),
405
+ size: Number(entry.slice(separator + 1))
406
+ };
407
+ });
408
+ }
409
+ async createNamespace(namespace) {
410
+ await this.request(3, namespace);
411
+ }
412
+ async deleteNamespace(namespace) {
413
+ await this.request(4, namespace);
414
+ }
415
+ async metadata(namespace, key) {
416
+ const message = await this.request(11, `${namespace}:${key}`);
417
+ const separator = message.lastIndexOf(":");
418
+ if (separator <= 0) throw new O2Error("Invalid METADATA response");
419
+ return {
420
+ sha256: message.slice(0, separator),
421
+ size: Number(message.slice(separator + 1))
422
+ };
423
+ }
424
+ async deleteObject(namespace, key) {
425
+ return this.request(9, `${namespace}:${key}`);
426
+ }
427
+ async putObject(namespace, key, data, options = {}) {
428
+ async function* chunks() {
429
+ const chunkSize = options.chunkSize ?? 262144;
430
+ for (let offset = 0; offset < data.length; offset += chunkSize) yield data.subarray(offset, Math.min(offset + chunkSize, data.length));
431
+ }
432
+ await this.putObjectStream(namespace, key, chunks());
433
+ }
434
+ async putObjectStream(namespace, key, source) {
435
+ this.ensureConnected();
436
+ const streamId = this.allocateStreamId();
437
+ const identifier = `${namespace}:${key}`;
438
+ try {
439
+ await this.requestOnStream(streamId, 5, identifier);
440
+ for await (const chunk of source) {
441
+ if (!(chunk instanceof Uint8Array)) throw new Error("PUT source must yield Uint8Array chunks");
442
+ if (!chunk.length) continue;
443
+ if (chunk.length > 1048576) throw new Error(`PUT chunk exceeds ${MAX_BODY_SIZE} bytes`);
444
+ await this.requestOnStream(streamId, 6, identifier, /* @__PURE__ */ new Uint8Array(), Uint8Array.from(chunk));
445
+ }
446
+ await this.requestOnStream(streamId, 7, identifier);
447
+ } catch (error) {
448
+ if (this.writer && this.socket && !this.socket.destroyed) await this.writer.frame(8, streamId, identifier).catch(() => {});
449
+ throw error;
450
+ }
451
+ }
452
+ async signObject(namespace, key, expirySeconds) {
453
+ if (!Number.isSafeInteger(expirySeconds) || expirySeconds <= 0) throw new Error("expirySeconds must be a positive integer");
454
+ return this.request(12, `${namespace}:${key}`, encoder.encode(`EXPIRY=${expirySeconds}`));
455
+ }
456
+ async retrieve(namespace, key, options = {}) {
457
+ const chunkSize = options.chunkSize ?? 262144;
458
+ if (!Number.isSafeInteger(chunkSize) || chunkSize < 1 || chunkSize > 1048576) throw new Error(`chunkSize must be between 1 and ${MAX_BODY_SIZE}`);
459
+ if (options.offset !== void 0 && (!Number.isSafeInteger(options.offset) || options.offset < 0)) throw new Error("offset must be a non-negative integer");
460
+ if (options.length !== void 0 && (!Number.isSafeInteger(options.length) || options.length <= 0)) throw new Error("length must be a positive integer");
461
+ if (options.length !== void 0 && options.offset === void 0) throw new Error("length requires offset");
462
+ this.ensureConnected();
463
+ const streamId = this.allocateStreamId();
464
+ const begin = deferred();
465
+ const state = {
466
+ begin,
467
+ queue: new AsyncByteQueue(this.transferBufferBytes),
468
+ identifier: `${namespace}:${key}`,
469
+ namespace,
470
+ key,
471
+ finished: false
472
+ };
473
+ this.transfers.set(streamId, state);
474
+ const flags = [`CHUNK_SIZE=${chunkSize}`];
475
+ if (options.offset !== void 0) flags.push(`OFFSET=${options.offset}`);
476
+ if (options.length !== void 0) flags.push(`LENGTH=${options.length}`);
477
+ try {
478
+ await this.writer.frame(10, streamId, state.identifier, encoder.encode(flags.join(",")));
479
+ } catch (error) {
480
+ this.transfers.delete(streamId);
481
+ throw error;
482
+ }
483
+ return begin.promise;
484
+ }
485
+ async request(opcode, param = "", flags = /* @__PURE__ */ new Uint8Array()) {
486
+ this.ensureConnected();
487
+ const streamId = this.allocateStreamId();
488
+ const waiter = deferred();
489
+ this.requests.set(streamId, waiter);
490
+ try {
491
+ await this.writer.frame(opcode, streamId, param, flags);
492
+ return await waiter.promise;
493
+ } finally {
494
+ this.requests.delete(streamId);
495
+ }
496
+ }
497
+ async handleFrame(frame) {
498
+ const message = decoder.decode(frame.param);
499
+ if (frame.streamId === 0) {
500
+ if (frame.opcode === 2) {
501
+ this.challengeWaiter?.resolve(message);
502
+ this.challengeWaiter = null;
503
+ return;
504
+ }
505
+ if (frame.opcode === 1) {
506
+ this.controlWaiter?.resolve(message);
507
+ this.controlWaiter = null;
508
+ return;
509
+ }
510
+ if (frame.opcode === 0) {
511
+ const error = new O2Error(message, 0);
512
+ this.controlWaiter?.reject(error);
513
+ this.challengeWaiter?.reject(error);
514
+ this.controlWaiter = null;
515
+ this.challengeWaiter = null;
516
+ return;
517
+ }
518
+ throw new O2Error(`Unexpected control response ${frame.opcode}`, 0);
519
+ }
520
+ const transfer = this.transfers.get(frame.streamId);
521
+ if (transfer) {
522
+ if (frame.opcode === 0) {
523
+ const error = new O2Error(message, frame.streamId);
524
+ transfer.begin.reject(error);
525
+ transfer.queue.fail(error);
526
+ this.transfers.delete(frame.streamId);
527
+ return;
528
+ }
529
+ if (frame.opcode === 3) {
530
+ if (message !== transfer.identifier || frame.body.length) throw new O2Error("Invalid RET_BEGIN", frame.streamId);
531
+ const metadata = parseFlags(decoder.decode(frame.flags));
532
+ const size = Number(metadata.SIZE);
533
+ if (!Number.isSafeInteger(size) || size < 0) throw new O2Error("RET_BEGIN missing valid SIZE", frame.streamId);
534
+ const sha256 = metadata.SHA256;
535
+ const retrieval = {
536
+ streamId: frame.streamId,
537
+ namespace: transfer.namespace,
538
+ key: transfer.key,
539
+ size,
540
+ sha256,
541
+ [Symbol.asyncIterator]: () => transfer.queue,
542
+ cancel: async () => this.cancelTransfer(frame.streamId)
543
+ };
544
+ transfer.begin.resolve(retrieval);
545
+ return;
546
+ }
547
+ if (frame.opcode === 4) {
548
+ if (frame.param.length || frame.flags.length || !frame.body.length) throw new O2Error("Invalid RET_CHUNK", frame.streamId);
549
+ await transfer.queue.push(frame.body.slice());
550
+ return;
551
+ }
552
+ if (frame.opcode === 5) {
553
+ if (message !== transfer.identifier || frame.flags.length || frame.body.length) throw new O2Error("Invalid RET_END", frame.streamId);
554
+ transfer.finished = true;
555
+ transfer.queue.close();
556
+ this.transfers.delete(frame.streamId);
557
+ return;
558
+ }
559
+ throw new O2Error(`Unexpected transfer response ${frame.opcode}`, frame.streamId);
560
+ }
561
+ const request = this.requests.get(frame.streamId);
562
+ if (!request) return;
563
+ if (frame.flags.length || frame.body.length) {
564
+ request.reject(new O2Error("Invalid response payload", frame.streamId));
565
+ return;
566
+ }
567
+ if (frame.opcode === 1) request.resolve(message);
568
+ else if (frame.opcode === 0) request.reject(new O2Error(message, frame.streamId));
569
+ else request.reject(new O2Error(`Unexpected response ${frame.opcode}`, frame.streamId));
570
+ }
571
+ async requestOnStream(streamId, opcode, param = "", flags = /* @__PURE__ */ new Uint8Array(), body = /* @__PURE__ */ new Uint8Array()) {
572
+ this.ensureConnected();
573
+ if (this.requests.has(streamId)) throw new Error(`Stream ${streamId} already has a pending request`);
574
+ const waiter = deferred();
575
+ this.requests.set(streamId, waiter);
576
+ try {
577
+ await this.writer.frame(opcode, streamId, param, flags, body);
578
+ return await waiter.promise;
579
+ } finally {
580
+ this.requests.delete(streamId);
581
+ }
582
+ }
583
+ async cancelTransfer(streamId) {
584
+ const transfer = this.transfers.get(streamId);
585
+ if (!transfer) return;
586
+ this.transfers.delete(streamId);
587
+ const error = new O2Error("Transfer cancelled", streamId);
588
+ transfer.begin.reject(error);
589
+ transfer.queue.fail(error);
590
+ if (this.writer && this.socket && !this.socket.destroyed) await this.writer.frame(13, streamId).catch(() => {});
591
+ }
592
+ allocateStreamId() {
593
+ for (let attempts = 0; attempts < 4294967295; attempts++) {
594
+ const id = this.nextStreamId++;
595
+ if (this.nextStreamId > 4294967295) this.nextStreamId = 1;
596
+ if (!this.requests.has(id) && !this.transfers.has(id)) return id;
597
+ }
598
+ throw new Error("No O2 stream IDs available");
599
+ }
600
+ ensureConnected() {
601
+ if (!this.connected || !this.writer || !this.socket || this.socket.destroyed) throw new Error("O2 client is not connected");
602
+ }
603
+ failAll(error) {
604
+ if (this.closing) return;
605
+ this.challengeWaiter?.reject(error);
606
+ this.controlWaiter?.reject(error);
607
+ this.challengeWaiter = null;
608
+ this.controlWaiter = null;
609
+ for (const waiter of this.requests.values()) waiter.reject(error);
610
+ this.requests.clear();
611
+ for (const transfer of this.transfers.values()) {
612
+ transfer.begin.reject(error);
613
+ transfer.queue.fail(error);
614
+ }
615
+ this.transfers.clear();
616
+ }
617
+ };
618
+ //#endregion
619
+ exports.O2Client = O2Client;
620
+ exports.O2Error = O2Error;