@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/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;
|