@midsummer-ai/sdk 0.0.0-stage → 0.1.0-next.1305
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/LICENSE +177 -0
- package/README.md +4 -2
- package/agents/contract.d.mts +9 -0
- package/agents/contract.mjs +221 -0
- package/agents/index.d.mts +128 -0
- package/agents/index.mjs +203 -0
- package/agents/stand-in.d.mts +35 -0
- package/agents/stand-in.mjs +149 -0
- package/ai/contract.d.mts +10 -0
- package/ai/contract.mjs +77 -0
- package/ai/index.d.mts +94 -0
- package/ai/index.mjs +193 -0
- package/ai/stand-in.d.mts +7 -0
- package/ai/stand-in.mjs +41 -0
- package/build-SQ44jwSa.d.mts +41 -0
- package/client.d.mts +45 -0
- package/client.mjs +106 -0
- package/connection-CxD_Nry5.mjs +1911 -0
- package/contract.d.mts +10 -0
- package/contract.mjs +469 -0
- package/database/build.d.mts +2 -0
- package/database/build.mjs +579 -0
- package/database/client.d.mts +60 -0
- package/database/client.mjs +133 -0
- package/database/contract.d.mts +11 -0
- package/database/contract.mjs +164 -0
- package/database/errors.d.mts +23 -0
- package/database/errors.mjs +13 -0
- package/database/server.d.mts +3 -0
- package/database/server.mjs +320 -0
- package/database/stand-in.d.mts +99 -0
- package/database/stand-in.mjs +430 -0
- package/email/client.d.mts +34 -0
- package/email/client.mjs +131 -0
- package/email/contract.d.mts +11 -0
- package/email/contract.mjs +99 -0
- package/email/index.d.mts +74 -0
- package/email/index.mjs +59 -0
- package/email/smtp.d.mts +22 -0
- package/email/smtp.mjs +296 -0
- package/email/stand-in.d.mts +13 -0
- package/email/stand-in.mjs +51 -0
- package/index-DdoK0y7D.d.mts +192 -0
- package/index-sgdyoA2P.d.mts +212 -0
- package/index.d.mts +2 -0
- package/index.mjs +137 -0
- package/names-DfCBKupJ.mjs +115 -0
- package/package.json +152 -4
- package/predict-Drfm_8pO.mjs +179 -0
- package/queues/contract.d.mts +13 -0
- package/queues/contract.mjs +278 -0
- package/queues/index.d.mts +184 -0
- package/queues/index.mjs +126 -0
- package/queues/stand-in.d.mts +7 -0
- package/queues/stand-in.mjs +110 -0
- package/queues/wire.d.mts +9 -0
- package/queues/wire.mjs +199 -0
- package/s3.d.mts +43 -0
- package/s3.mjs +1003 -0
- package/schema-BF-Y1EVu.mjs +142 -0
- package/schema-DS8x1Dn8.mjs +115 -0
- package/schema-f7E9uzUn.d.mts +74 -0
- package/server-C7O7CICU.d.mts +182 -0
- package/socket.d.mts +67 -0
- package/socket.mjs +263 -0
- package/stand-in.d.mts +7 -0
- package/stand-in.mjs +185 -0
- package/store-DDuOr-_F.mjs +237 -0
- package/tabs-4FLTfg1c.mjs +457 -0
- package/vector/contract.d.mts +10 -0
- package/vector/contract.mjs +635 -0
- package/vector/index.d.mts +2 -0
- package/vector/index.mjs +108 -0
- package/vector/stand-in.d.mts +11 -0
- package/vector/stand-in.mjs +169 -0
- package/vector/wire.d.mts +25 -0
- package/vector/wire.mjs +175 -0
- package/wire.d.mts +54 -0
- package/wire.mjs +731 -0
package/wire.mjs
ADDED
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import { _ as storageErrorReasons, a as checkMetadata, c as checkRange, g as storageErrorCodes, h as StorageError, i as checkListLimit, l as checkUploadId, n as checkBucketName, o as checkPartNumber, r as checkKey, s as checkParts } from "./names-DfCBKupJ.mjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/wire.ts
|
|
4
|
+
/**
|
|
5
|
+
* Storage's wire: how a Storage call crosses a network. `remote(transport)` is the engine a client
|
|
6
|
+
* uses; `serve(engine)` replies on the other side. A server implements `serve` over its own engine
|
|
7
|
+
* and nothing else, so the contract run through the wire holds for it too.
|
|
8
|
+
*
|
|
9
|
+
* const files = storage(remote({ send: (frame) => lane.request(frame) }))
|
|
10
|
+
* lane.onRequest(serve(engine)) // on the server
|
|
11
|
+
*
|
|
12
|
+
* A frame is a 4-byte little-endian length, that many bytes of JSON header, then the body: an
|
|
13
|
+
* Object's bytes on a put and a read, nothing otherwise. One request frame gets one answer frame;
|
|
14
|
+
* the transport carries them and matches replies to requests. Changing a frame changes the
|
|
15
|
+
* contract with every server.
|
|
16
|
+
*
|
|
17
|
+
* With `remote(transport, { batch: true })`, the calls made in the same turn travel as one `batch`
|
|
18
|
+
* frame: its header lists the byte lengths of the ordinary frames its body holds, end to end, and
|
|
19
|
+
* its answer lists the lengths of their answer frames, in the same order. Each call inside is
|
|
20
|
+
* replied to as it would be alone, refusals included; a batch costs one request where it had many.
|
|
21
|
+
*/
|
|
22
|
+
const range = z.union([z.object({
|
|
23
|
+
offset: z.number(),
|
|
24
|
+
length: z.number().optional()
|
|
25
|
+
}).strict(), z.object({ suffix: z.number() }).strict()]);
|
|
26
|
+
const at = {
|
|
27
|
+
bucket: z.string(),
|
|
28
|
+
key: z.string()
|
|
29
|
+
};
|
|
30
|
+
const request = z.discriminatedUnion("op", [
|
|
31
|
+
z.object({
|
|
32
|
+
op: z.literal("put"),
|
|
33
|
+
...at,
|
|
34
|
+
contentType: z.string().max(256),
|
|
35
|
+
metadata: z.record(z.string(), z.string()),
|
|
36
|
+
ifNoneMatch: z.literal("*").optional(),
|
|
37
|
+
ifMatch: z.string().optional()
|
|
38
|
+
}).strict(),
|
|
39
|
+
z.object({
|
|
40
|
+
op: z.literal("get"),
|
|
41
|
+
...at,
|
|
42
|
+
range: range.optional(),
|
|
43
|
+
limit: z.number().int().positive().optional()
|
|
44
|
+
}).strict(),
|
|
45
|
+
z.object({
|
|
46
|
+
op: z.literal("head"),
|
|
47
|
+
...at
|
|
48
|
+
}).strict(),
|
|
49
|
+
z.object({
|
|
50
|
+
op: z.literal("delete"),
|
|
51
|
+
...at
|
|
52
|
+
}).strict(),
|
|
53
|
+
z.object({
|
|
54
|
+
op: z.literal("list"),
|
|
55
|
+
bucket: z.string(),
|
|
56
|
+
prefix: z.string(),
|
|
57
|
+
delimiter: z.string().min(1).optional(),
|
|
58
|
+
after: z.string().optional(),
|
|
59
|
+
limit: z.number()
|
|
60
|
+
}).strict(),
|
|
61
|
+
z.object({
|
|
62
|
+
op: z.literal("copy"),
|
|
63
|
+
bucket: z.string(),
|
|
64
|
+
from: z.string(),
|
|
65
|
+
to: z.string(),
|
|
66
|
+
ifNoneMatch: z.literal("*").optional()
|
|
67
|
+
}).strict(),
|
|
68
|
+
z.object({
|
|
69
|
+
op: z.literal("createUpload"),
|
|
70
|
+
...at,
|
|
71
|
+
contentType: z.string().max(256),
|
|
72
|
+
metadata: z.record(z.string(), z.string()),
|
|
73
|
+
ifNoneMatch: z.literal("*").optional()
|
|
74
|
+
}).strict(),
|
|
75
|
+
z.object({
|
|
76
|
+
op: z.literal("uploadPart"),
|
|
77
|
+
...at,
|
|
78
|
+
uploadId: z.string(),
|
|
79
|
+
partNumber: z.number()
|
|
80
|
+
}).strict(),
|
|
81
|
+
z.object({
|
|
82
|
+
op: z.literal("completeUpload"),
|
|
83
|
+
...at,
|
|
84
|
+
uploadId: z.string(),
|
|
85
|
+
parts: z.array(z.object({
|
|
86
|
+
partNumber: z.number(),
|
|
87
|
+
etag: z.string()
|
|
88
|
+
}).strict())
|
|
89
|
+
}).strict(),
|
|
90
|
+
z.object({
|
|
91
|
+
op: z.literal("abortUpload"),
|
|
92
|
+
...at,
|
|
93
|
+
uploadId: z.string()
|
|
94
|
+
}).strict(),
|
|
95
|
+
z.object({
|
|
96
|
+
op: z.literal("usage"),
|
|
97
|
+
bucket: z.string()
|
|
98
|
+
}).strict(),
|
|
99
|
+
z.object({
|
|
100
|
+
op: z.literal("operations"),
|
|
101
|
+
bucket: z.string(),
|
|
102
|
+
from: z.number(),
|
|
103
|
+
to: z.number()
|
|
104
|
+
}).strict()
|
|
105
|
+
]);
|
|
106
|
+
const stored = z.object({
|
|
107
|
+
key: z.string(),
|
|
108
|
+
size: z.number(),
|
|
109
|
+
etag: z.string(),
|
|
110
|
+
contentType: z.string(),
|
|
111
|
+
metadata: z.record(z.string(), z.string()),
|
|
112
|
+
uploadedAt: z.number()
|
|
113
|
+
}).strict();
|
|
114
|
+
const answer = z.union([
|
|
115
|
+
z.object({
|
|
116
|
+
error: z.enum(storageErrorCodes),
|
|
117
|
+
why: z.string(),
|
|
118
|
+
reason: z.enum(storageErrorReasons).optional()
|
|
119
|
+
}).strict(),
|
|
120
|
+
z.object({ uploadId: z.string() }).strict(),
|
|
121
|
+
z.object({ part: z.object({
|
|
122
|
+
partNumber: z.number(),
|
|
123
|
+
etag: z.string(),
|
|
124
|
+
size: z.number()
|
|
125
|
+
}).strict() }).strict(),
|
|
126
|
+
z.object({
|
|
127
|
+
object: stored.nullable(),
|
|
128
|
+
range: z.object({
|
|
129
|
+
offset: z.number(),
|
|
130
|
+
length: z.number()
|
|
131
|
+
}).strict().optional()
|
|
132
|
+
}).strict(),
|
|
133
|
+
z.object({ listing: z.object({
|
|
134
|
+
objects: z.array(stored),
|
|
135
|
+
prefixes: z.array(z.string()),
|
|
136
|
+
next: z.string().optional()
|
|
137
|
+
}).strict() }).strict(),
|
|
138
|
+
z.object({ done: z.literal(true) }).strict(),
|
|
139
|
+
z.object({ usage: z.object({
|
|
140
|
+
objects: z.number(),
|
|
141
|
+
bytes: z.number()
|
|
142
|
+
}).strict() }).strict(),
|
|
143
|
+
z.object({ operations: z.object({
|
|
144
|
+
reads: z.number(),
|
|
145
|
+
writes: z.number()
|
|
146
|
+
}).strict() }).strict(),
|
|
147
|
+
z.object({ again: z.literal(true) }).strict()
|
|
148
|
+
]);
|
|
149
|
+
const BATCH_MAX = 256;
|
|
150
|
+
const BATCH_BYTES = 16777216;
|
|
151
|
+
const PART_BYTES = 8388608;
|
|
152
|
+
const ANSWER_BYTES = 29360128;
|
|
153
|
+
let again;
|
|
154
|
+
const AGAIN = () => again ??= encodeFrame({ again: true });
|
|
155
|
+
const PARTS_AT_ONCE = 4;
|
|
156
|
+
const batchRequest = z.object({
|
|
157
|
+
op: z.literal("batch"),
|
|
158
|
+
frames: z.array(z.number().int().min(4)).min(1).max(BATCH_MAX)
|
|
159
|
+
}).strict();
|
|
160
|
+
const batchAnswer = z.object({ batch: z.array(z.number().int().min(4)) }).strict();
|
|
161
|
+
/** Frames joined end to end, with their lengths. */
|
|
162
|
+
function sameBytes(a, b) {
|
|
163
|
+
return a.length === b.length && a.every((byte, i) => byte === b[i]);
|
|
164
|
+
}
|
|
165
|
+
function join(frames) {
|
|
166
|
+
const body = new Uint8Array(frames.reduce((n, f) => n + f.length, 0));
|
|
167
|
+
let at = 0;
|
|
168
|
+
for (const frame of frames) body.set(frame, at), at += frame.length;
|
|
169
|
+
return {
|
|
170
|
+
lengths: frames.map((f) => f.length),
|
|
171
|
+
body
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** `body` cut into frames of `lengths`; `invalid` when they do not add up to it. */
|
|
175
|
+
function split(lengths, body) {
|
|
176
|
+
if (lengths.reduce((n, l) => n + l, 0) !== body.length) throw new StorageError("invalid", "the batch's frames do not add up to its body");
|
|
177
|
+
let at = 0;
|
|
178
|
+
return lengths.map((length) => body.subarray(at, at += length));
|
|
179
|
+
}
|
|
180
|
+
/** Sends the frames of one turn as batch frames over `transport`; a lone frame goes as it is. */
|
|
181
|
+
function batched(transport) {
|
|
182
|
+
let waiting = [];
|
|
183
|
+
const flush = async (group) => {
|
|
184
|
+
if (group.length === 1) return group[0].done(await transport.send(group[0].frame));
|
|
185
|
+
const { lengths, body } = join(group.map((w) => w.frame));
|
|
186
|
+
const { header, body: answers } = decodeFrame(await transport.send(encodeFrame({
|
|
187
|
+
op: "batch",
|
|
188
|
+
frames: lengths
|
|
189
|
+
}, body)));
|
|
190
|
+
const parsed = batchAnswer.safeParse(header);
|
|
191
|
+
if (!parsed.success) {
|
|
192
|
+
const whole = encodeFrame(header);
|
|
193
|
+
return group.forEach((w) => w.done(whole));
|
|
194
|
+
}
|
|
195
|
+
const each = split(parsed.data.batch, answers);
|
|
196
|
+
if (each.length !== group.length) throw new StorageError("unavailable", "the batch's answer does not match its calls");
|
|
197
|
+
await Promise.all(group.map(async (w, i) => {
|
|
198
|
+
const answer = each[i];
|
|
199
|
+
w.done(sameBytes(answer, AGAIN()) ? await transport.send(w.frame) : answer);
|
|
200
|
+
}));
|
|
201
|
+
};
|
|
202
|
+
return (frame) => new Promise((done, fail) => {
|
|
203
|
+
if (waiting.length === 0) queueMicrotask(() => {
|
|
204
|
+
const all = waiting;
|
|
205
|
+
waiting = [];
|
|
206
|
+
for (let i = 0; i < all.length;) {
|
|
207
|
+
let bytes = 0, j = i;
|
|
208
|
+
while (j < all.length && j - i < BATCH_MAX && (j === i || bytes + all[j].frame.length <= BATCH_BYTES)) bytes += all[j++].frame.length;
|
|
209
|
+
const group = all.slice(i, j);
|
|
210
|
+
i = j;
|
|
211
|
+
flush(group).catch((error) => group.forEach((w) => w.fail(error)));
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
waiting.push({
|
|
215
|
+
frame,
|
|
216
|
+
done,
|
|
217
|
+
fail
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The engine on the client's side of the wire. With `batch`, the calls made in the same turn travel
|
|
223
|
+
* as one batch frame (the platform's own calls do not batch; clients that issue many at once do).
|
|
224
|
+
*/
|
|
225
|
+
function remote(transport, options = {}) {
|
|
226
|
+
const send = options.batch ? batched(transport) : (frame) => transport.send(frame);
|
|
227
|
+
const call = async (header, body) => {
|
|
228
|
+
const { header: reply, body: bytes } = decodeFrame(await send(encodeFrame(header, body)));
|
|
229
|
+
const parsed = answer.parse(reply);
|
|
230
|
+
if ("error" in parsed) throw new StorageError(parsed.error, parsed.why, parsed.reason);
|
|
231
|
+
return {
|
|
232
|
+
reply: parsed,
|
|
233
|
+
bytes
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
const objectOf = (reply) => "object" in reply ? reply.object : null;
|
|
237
|
+
/** One get as the wire carries it: at most BATCH_BYTES of body. */
|
|
238
|
+
const getOne = async (bucket, key, range) => {
|
|
239
|
+
const { reply, bytes } = await call({
|
|
240
|
+
op: "get",
|
|
241
|
+
bucket,
|
|
242
|
+
key,
|
|
243
|
+
...range ? { range } : { limit: BATCH_BYTES }
|
|
244
|
+
});
|
|
245
|
+
const object = objectOf(reply);
|
|
246
|
+
if (!object) return null;
|
|
247
|
+
const at = "range" in reply ? reply.range : void 0;
|
|
248
|
+
return {
|
|
249
|
+
...object,
|
|
250
|
+
body: bytes,
|
|
251
|
+
...at ? { range: at } : {}
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
const engine = {
|
|
255
|
+
async put(bucket, key, body, put) {
|
|
256
|
+
if (body.length <= BATCH_BYTES) return objectOf((await call({
|
|
257
|
+
op: "put",
|
|
258
|
+
bucket,
|
|
259
|
+
key,
|
|
260
|
+
...put,
|
|
261
|
+
metadata: { ...put.metadata }
|
|
262
|
+
}, body)).reply);
|
|
263
|
+
if (put.ifMatch !== void 0) throw new StorageError("invalid", `ifMatch is not answered for a body over ${BATCH_BYTES} bytes, which is uploaded in parts`);
|
|
264
|
+
const { uploadId } = await engine.createUpload(bucket, key, {
|
|
265
|
+
contentType: put.contentType,
|
|
266
|
+
metadata: put.metadata,
|
|
267
|
+
...put.ifNoneMatch ? { ifNoneMatch: put.ifNoneMatch } : {}
|
|
268
|
+
});
|
|
269
|
+
try {
|
|
270
|
+
const count = Math.ceil(body.length / PART_BYTES), parts = [];
|
|
271
|
+
for (let first = 0; first < count; first += PARTS_AT_ONCE) parts.push(...await Promise.all(Array.from({ length: Math.min(PARTS_AT_ONCE, count - first) }, (_, k) => {
|
|
272
|
+
const i = first + k;
|
|
273
|
+
return engine.uploadPart(bucket, key, uploadId, i + 1, body.subarray(i * PART_BYTES, (i + 1) * PART_BYTES));
|
|
274
|
+
})));
|
|
275
|
+
return await engine.completeUpload(bucket, key, uploadId, parts);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
await engine.abortUpload(bucket, key, uploadId).catch(() => {});
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
async get(bucket, key, range) {
|
|
282
|
+
for (let attempt = 1;; attempt += 1) {
|
|
283
|
+
const first = await getOne(bucket, key, range && clamp(range));
|
|
284
|
+
if (!first) return null;
|
|
285
|
+
const wanted = range ? spanOf(range, first.size) : {
|
|
286
|
+
offset: 0,
|
|
287
|
+
length: first.size
|
|
288
|
+
};
|
|
289
|
+
if (first.body.length === wanted.length) {
|
|
290
|
+
if (range || first.range === void 0) return first;
|
|
291
|
+
const { range: _whole, ...object } = first;
|
|
292
|
+
return object;
|
|
293
|
+
}
|
|
294
|
+
const whole = new Uint8Array(wanted.length);
|
|
295
|
+
const at = (offset) => offset - wanted.offset;
|
|
296
|
+
const got = first.range ?? {
|
|
297
|
+
offset: 0,
|
|
298
|
+
length: first.body.length
|
|
299
|
+
};
|
|
300
|
+
whole.set(first.body, at(got.offset));
|
|
301
|
+
let changed = false;
|
|
302
|
+
const missing = [[wanted.offset, got.offset], [got.offset + got.length, wanted.offset + wanted.length]];
|
|
303
|
+
for (const [from, to] of missing) for (let offset = from; offset < to && !changed; offset += BATCH_BYTES) {
|
|
304
|
+
const piece = await getOne(bucket, key, {
|
|
305
|
+
offset,
|
|
306
|
+
length: Math.min(BATCH_BYTES, to - offset)
|
|
307
|
+
});
|
|
308
|
+
if (!piece || piece.etag !== first.etag) changed = true;
|
|
309
|
+
else whole.set(piece.body, at(offset));
|
|
310
|
+
}
|
|
311
|
+
if (!changed) {
|
|
312
|
+
const { body: _first, range: _at, ...object } = first;
|
|
313
|
+
return {
|
|
314
|
+
...object,
|
|
315
|
+
body: whole,
|
|
316
|
+
...range ? { range: wanted } : {}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (attempt === 3) throw new StorageError("unavailable", `"${key}" changed while it was read in ranges`);
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
async head(bucket, key) {
|
|
323
|
+
return objectOf((await call({
|
|
324
|
+
op: "head",
|
|
325
|
+
bucket,
|
|
326
|
+
key
|
|
327
|
+
})).reply);
|
|
328
|
+
},
|
|
329
|
+
async delete(bucket, key) {
|
|
330
|
+
await call({
|
|
331
|
+
op: "delete",
|
|
332
|
+
bucket,
|
|
333
|
+
key
|
|
334
|
+
});
|
|
335
|
+
},
|
|
336
|
+
async list(bucket, options) {
|
|
337
|
+
const { reply } = await call({
|
|
338
|
+
op: "list",
|
|
339
|
+
bucket,
|
|
340
|
+
...options
|
|
341
|
+
});
|
|
342
|
+
return "listing" in reply ? reply.listing : {
|
|
343
|
+
objects: [],
|
|
344
|
+
prefixes: []
|
|
345
|
+
};
|
|
346
|
+
},
|
|
347
|
+
async copy(bucket, from, to, options) {
|
|
348
|
+
return objectOf((await call({
|
|
349
|
+
op: "copy",
|
|
350
|
+
bucket,
|
|
351
|
+
from,
|
|
352
|
+
to,
|
|
353
|
+
...options
|
|
354
|
+
})).reply);
|
|
355
|
+
},
|
|
356
|
+
async createUpload(bucket, key, upload) {
|
|
357
|
+
const { reply } = await call({
|
|
358
|
+
op: "createUpload",
|
|
359
|
+
bucket,
|
|
360
|
+
key,
|
|
361
|
+
...upload,
|
|
362
|
+
metadata: { ...upload.metadata }
|
|
363
|
+
});
|
|
364
|
+
return { uploadId: reply.uploadId };
|
|
365
|
+
},
|
|
366
|
+
async uploadPart(bucket, key, uploadId, partNumber, body) {
|
|
367
|
+
const { reply } = await call({
|
|
368
|
+
op: "uploadPart",
|
|
369
|
+
bucket,
|
|
370
|
+
key,
|
|
371
|
+
uploadId,
|
|
372
|
+
partNumber
|
|
373
|
+
}, body);
|
|
374
|
+
return reply.part;
|
|
375
|
+
},
|
|
376
|
+
async completeUpload(bucket, key, uploadId, parts) {
|
|
377
|
+
return objectOf((await call({
|
|
378
|
+
op: "completeUpload",
|
|
379
|
+
bucket,
|
|
380
|
+
key,
|
|
381
|
+
uploadId,
|
|
382
|
+
parts: parts.map((p) => ({
|
|
383
|
+
partNumber: p.partNumber,
|
|
384
|
+
etag: p.etag
|
|
385
|
+
}))
|
|
386
|
+
})).reply);
|
|
387
|
+
},
|
|
388
|
+
async abortUpload(bucket, key, uploadId) {
|
|
389
|
+
await call({
|
|
390
|
+
op: "abortUpload",
|
|
391
|
+
bucket,
|
|
392
|
+
key,
|
|
393
|
+
uploadId
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
async usage(bucket) {
|
|
397
|
+
const { reply } = await call({
|
|
398
|
+
op: "usage",
|
|
399
|
+
bucket
|
|
400
|
+
});
|
|
401
|
+
return reply.usage;
|
|
402
|
+
},
|
|
403
|
+
async operations(bucket, from, to) {
|
|
404
|
+
const { reply } = await call({
|
|
405
|
+
op: "operations",
|
|
406
|
+
bucket,
|
|
407
|
+
from,
|
|
408
|
+
to
|
|
409
|
+
});
|
|
410
|
+
return reply.operations;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
return engine;
|
|
414
|
+
}
|
|
415
|
+
/** `range` cut to what one frame carries: its first BATCH_BYTES, or its last for a suffix. */
|
|
416
|
+
function clamp(range) {
|
|
417
|
+
if ("suffix" in range) return { suffix: Math.min(range.suffix, BATCH_BYTES) };
|
|
418
|
+
return {
|
|
419
|
+
offset: range.offset,
|
|
420
|
+
length: Math.min(range.length ?? BATCH_BYTES, BATCH_BYTES)
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
/** The bytes `range` names in an Object of `size` bytes, cut at its end, as an engine resolves it. */
|
|
424
|
+
function spanOf(range, size) {
|
|
425
|
+
if ("suffix" in range) return {
|
|
426
|
+
offset: Math.max(0, size - range.suffix),
|
|
427
|
+
length: Math.min(range.suffix, size)
|
|
428
|
+
};
|
|
429
|
+
return {
|
|
430
|
+
offset: range.offset,
|
|
431
|
+
length: Math.min(range.length ?? size - range.offset, size - range.offset)
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Whether sending `frame` a second time, after the first may have been carried out, returns the same as the
|
|
436
|
+
* first would have and stores nothing more: reads, an unconditional put, a delete, a part sent again.
|
|
437
|
+
* A conditional put, a copy (its source may have changed since), and starting, completing or aborting
|
|
438
|
+
* an upload are not: a second `ifNoneMatch` put is a `conflict`, a second completion `not_found`. A
|
|
439
|
+
* batch is when all it holds is and no call in it writes what another call reads or writes, so sending
|
|
440
|
+
* it again cannot see one call's effect in another's answer. A transport resends a frame it lost the
|
|
441
|
+
* answer to only when this allows it.
|
|
442
|
+
*/
|
|
443
|
+
function repeatable(frame) {
|
|
444
|
+
let header, body;
|
|
445
|
+
try {
|
|
446
|
+
({header, body} = decodeFrame(frame));
|
|
447
|
+
} catch {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
const batch = batchRequest.safeParse(header);
|
|
451
|
+
if (!batch.success) {
|
|
452
|
+
const call = request.safeParse(header);
|
|
453
|
+
return call.success && repeatableCall(call.data);
|
|
454
|
+
}
|
|
455
|
+
let calls;
|
|
456
|
+
try {
|
|
457
|
+
calls = split(batch.data.frames, body).map((f) => request.parse(decodeFrame(f).header));
|
|
458
|
+
} catch {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
if (!calls.every(repeatableCall)) return false;
|
|
462
|
+
const reach = (call) => call.op === "uploadPart" ? `${call.bucket}/${call.key}#${call.uploadId}#${call.partNumber}` : "key" in call ? `${call.bucket}/${call.key}` : null;
|
|
463
|
+
const written = /* @__PURE__ */ new Set(), all = /* @__PURE__ */ new Map();
|
|
464
|
+
for (const call of calls) {
|
|
465
|
+
const at = reach(call);
|
|
466
|
+
if (at === null) continue;
|
|
467
|
+
if (call.op === "put" || call.op === "delete" || call.op === "uploadPart") written.add(at);
|
|
468
|
+
all.set(at, (all.get(at) ?? 0) + 1);
|
|
469
|
+
}
|
|
470
|
+
if (written.size > 0 && calls.some((c) => c.op === "list")) return false;
|
|
471
|
+
return [...written].every((at) => all.get(at) === 1);
|
|
472
|
+
}
|
|
473
|
+
function repeatableCall(call) {
|
|
474
|
+
switch (call.op) {
|
|
475
|
+
case "get":
|
|
476
|
+
case "head":
|
|
477
|
+
case "list":
|
|
478
|
+
case "delete":
|
|
479
|
+
case "uploadPart":
|
|
480
|
+
case "usage":
|
|
481
|
+
case "operations": return true;
|
|
482
|
+
case "put": return call.ifNoneMatch === void 0 && call.ifMatch === void 0;
|
|
483
|
+
case "copy":
|
|
484
|
+
case "createUpload":
|
|
485
|
+
case "completeUpload":
|
|
486
|
+
case "abortUpload": return false;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* The Bucket names that the calls in `frame` use, so a server can look up only those before serving it.
|
|
491
|
+
* A call it cannot read uses none: `serve` returns `invalid` for it without reaching a Bucket.
|
|
492
|
+
*/
|
|
493
|
+
function bucketsOf(frame) {
|
|
494
|
+
const names = /* @__PURE__ */ new Set();
|
|
495
|
+
const read = (f) => {
|
|
496
|
+
try {
|
|
497
|
+
return decodeFrame(f);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
if (error instanceof StorageError) return null;
|
|
500
|
+
throw error;
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
const add = (header) => {
|
|
504
|
+
const call = request.safeParse(header);
|
|
505
|
+
if (call.success) names.add(call.data.bucket);
|
|
506
|
+
};
|
|
507
|
+
const outer = read(frame);
|
|
508
|
+
if (!outer) return [];
|
|
509
|
+
const batch = batchRequest.safeParse(outer.header);
|
|
510
|
+
if (!batch.success) add(outer.header);
|
|
511
|
+
else {
|
|
512
|
+
let frames = [];
|
|
513
|
+
try {
|
|
514
|
+
frames = split(batch.data.frames, outer.body);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
if (!(error instanceof StorageError)) throw error;
|
|
517
|
+
}
|
|
518
|
+
for (const f of frames) add(read(f)?.header);
|
|
519
|
+
}
|
|
520
|
+
return [...names];
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Serves request frames from `engine`. A frame comes from the network, so its header is checked
|
|
524
|
+
* here as the client would check it; a refusal is returned as `invalid`. A failure that is not a
|
|
525
|
+
* `StorageError` is the engine's own fault and is thrown to the transport, never returned.
|
|
526
|
+
*/
|
|
527
|
+
function serve(engine) {
|
|
528
|
+
const one = async (header, body) => {
|
|
529
|
+
try {
|
|
530
|
+
const parsed = request.safeParse(header);
|
|
531
|
+
if (!parsed.success) throw new StorageError("invalid", "the request frame is not a Storage call");
|
|
532
|
+
return await answerTo(engine, parsed.data, body);
|
|
533
|
+
} catch (error) {
|
|
534
|
+
if (error instanceof StorageError) return encodeFrame({
|
|
535
|
+
error: error.code,
|
|
536
|
+
why: error.message,
|
|
537
|
+
...error.reason ? { reason: error.reason } : {}
|
|
538
|
+
});
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
return async (frame) => {
|
|
543
|
+
let header, body;
|
|
544
|
+
try {
|
|
545
|
+
({header, body} = decodeFrame(frame));
|
|
546
|
+
} catch (error) {
|
|
547
|
+
if (error instanceof StorageError) return encodeFrame({
|
|
548
|
+
error: error.code,
|
|
549
|
+
why: error.message
|
|
550
|
+
});
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
553
|
+
const batch = batchRequest.safeParse(header);
|
|
554
|
+
if (!batch.success) return one(header, body);
|
|
555
|
+
let frames;
|
|
556
|
+
try {
|
|
557
|
+
frames = split(batch.data.frames, body);
|
|
558
|
+
} catch (error) {
|
|
559
|
+
if (error instanceof StorageError) return encodeFrame({
|
|
560
|
+
error: error.code,
|
|
561
|
+
why: error.message
|
|
562
|
+
});
|
|
563
|
+
throw error;
|
|
564
|
+
}
|
|
565
|
+
const answers = await Promise.all(frames.map(async (f) => {
|
|
566
|
+
try {
|
|
567
|
+
const inner = decodeFrame(f);
|
|
568
|
+
return await one(inner.header, inner.body);
|
|
569
|
+
} catch (error) {
|
|
570
|
+
if (error instanceof StorageError) return encodeFrame({
|
|
571
|
+
error: error.code,
|
|
572
|
+
why: error.message
|
|
573
|
+
});
|
|
574
|
+
throw error;
|
|
575
|
+
}
|
|
576
|
+
}));
|
|
577
|
+
let bytes = 0;
|
|
578
|
+
const { lengths, body: joined } = join(answers.map((a) => bytes > 0 && bytes + a.length > ANSWER_BYTES && a.length > 1024 ? AGAIN() : (bytes += a.length, a)));
|
|
579
|
+
return encodeFrame({ batch: lengths }, joined);
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
async function answerTo(engine, call, body) {
|
|
583
|
+
checkBucketName(call.bucket);
|
|
584
|
+
switch (call.op) {
|
|
585
|
+
case "put": {
|
|
586
|
+
checkKey(call.key);
|
|
587
|
+
checkMetadata(call.metadata);
|
|
588
|
+
const put = {
|
|
589
|
+
contentType: call.contentType,
|
|
590
|
+
metadata: call.metadata,
|
|
591
|
+
...call.ifNoneMatch ? { ifNoneMatch: call.ifNoneMatch } : {},
|
|
592
|
+
...call.ifMatch !== void 0 ? { ifMatch: call.ifMatch } : {}
|
|
593
|
+
};
|
|
594
|
+
return encodeFrame({ object: await engine.put(call.bucket, call.key, body, put) });
|
|
595
|
+
}
|
|
596
|
+
case "get": {
|
|
597
|
+
checkKey(call.key);
|
|
598
|
+
const at = call.range && rangeOf(call.range);
|
|
599
|
+
if (at) checkRange(at);
|
|
600
|
+
const read = await engine.get(call.bucket, call.key, at, at ? void 0 : call.limit);
|
|
601
|
+
if (!read) return encodeFrame({ object: null });
|
|
602
|
+
const { body: bytes, range, ...object } = read;
|
|
603
|
+
return encodeFrame({
|
|
604
|
+
object,
|
|
605
|
+
...range ? { range } : {}
|
|
606
|
+
}, bytes);
|
|
607
|
+
}
|
|
608
|
+
case "head":
|
|
609
|
+
checkKey(call.key);
|
|
610
|
+
return encodeFrame({ object: await engine.head(call.bucket, call.key) });
|
|
611
|
+
case "delete":
|
|
612
|
+
checkKey(call.key);
|
|
613
|
+
await engine.delete(call.bucket, call.key);
|
|
614
|
+
return encodeFrame({ done: true });
|
|
615
|
+
case "list": {
|
|
616
|
+
checkListLimit(call.limit);
|
|
617
|
+
const options = {
|
|
618
|
+
prefix: call.prefix,
|
|
619
|
+
limit: call.limit,
|
|
620
|
+
...call.delimiter !== void 0 ? { delimiter: call.delimiter } : {},
|
|
621
|
+
...call.after !== void 0 ? { after: call.after } : {}
|
|
622
|
+
};
|
|
623
|
+
return encodeFrame({ listing: await engine.list(call.bucket, options) });
|
|
624
|
+
}
|
|
625
|
+
case "copy":
|
|
626
|
+
checkKey(call.from);
|
|
627
|
+
checkKey(call.to);
|
|
628
|
+
return encodeFrame({ object: await engine.copy(call.bucket, call.from, call.to, call.ifNoneMatch ? { ifNoneMatch: call.ifNoneMatch } : {}) });
|
|
629
|
+
case "createUpload": {
|
|
630
|
+
checkKey(call.key);
|
|
631
|
+
checkMetadata(call.metadata);
|
|
632
|
+
const upload = {
|
|
633
|
+
contentType: call.contentType,
|
|
634
|
+
metadata: call.metadata,
|
|
635
|
+
...call.ifNoneMatch ? { ifNoneMatch: call.ifNoneMatch } : {}
|
|
636
|
+
};
|
|
637
|
+
return encodeFrame(await engine.createUpload(call.bucket, call.key, upload));
|
|
638
|
+
}
|
|
639
|
+
case "uploadPart":
|
|
640
|
+
checkKey(call.key);
|
|
641
|
+
checkUploadId(call.uploadId);
|
|
642
|
+
checkPartNumber(call.partNumber);
|
|
643
|
+
return encodeFrame({ part: await engine.uploadPart(call.bucket, call.key, call.uploadId, call.partNumber, body) });
|
|
644
|
+
case "completeUpload":
|
|
645
|
+
checkKey(call.key);
|
|
646
|
+
checkUploadId(call.uploadId);
|
|
647
|
+
checkParts(call.parts);
|
|
648
|
+
return encodeFrame({ object: await engine.completeUpload(call.bucket, call.key, call.uploadId, call.parts) });
|
|
649
|
+
case "abortUpload":
|
|
650
|
+
checkKey(call.key);
|
|
651
|
+
checkUploadId(call.uploadId);
|
|
652
|
+
await engine.abortUpload(call.bucket, call.key, call.uploadId);
|
|
653
|
+
return encodeFrame({ done: true });
|
|
654
|
+
case "usage": return encodeFrame({ usage: await engine.usage(call.bucket) });
|
|
655
|
+
case "operations": return encodeFrame({ operations: await engine.operations(call.bucket, call.from, call.to) });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* A transport over HTTP: each frame is one POST to `url` with the engine's bearer token, replied to
|
|
660
|
+
* by `serveHttp` on the other side. Refused credentials are `forbidden`; a server that cannot be
|
|
661
|
+
* reached, or replies with anything but a frame, is `unavailable`.
|
|
662
|
+
*/
|
|
663
|
+
function http(url, options) {
|
|
664
|
+
const send = options.fetch ?? ((to, init) => fetch(to, init));
|
|
665
|
+
return { async send(frame) {
|
|
666
|
+
let response;
|
|
667
|
+
try {
|
|
668
|
+
response = await send(url, {
|
|
669
|
+
method: "POST",
|
|
670
|
+
headers: {
|
|
671
|
+
authorization: `Bearer ${options.token}`,
|
|
672
|
+
"content-type": "application/octet-stream"
|
|
673
|
+
},
|
|
674
|
+
body: frame
|
|
675
|
+
});
|
|
676
|
+
} catch {
|
|
677
|
+
throw new StorageError("unavailable", "Storage could not be reached");
|
|
678
|
+
}
|
|
679
|
+
if (response.status === 401 || response.status === 403) throw new StorageError("forbidden", "Storage refused this deployment's credentials");
|
|
680
|
+
if (response.status !== 200) throw new StorageError("unavailable", `Storage answered ${response.status}`);
|
|
681
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
682
|
+
} };
|
|
683
|
+
}
|
|
684
|
+
/** Answers `http` transports: a POST of one frame, with `token` as its bearer token, gets one answer frame. */
|
|
685
|
+
function serveHttp(engine, options) {
|
|
686
|
+
const answer = serve(engine);
|
|
687
|
+
const expected = new TextEncoder().encode(`Bearer ${options.token}`);
|
|
688
|
+
return async (request) => {
|
|
689
|
+
if (request.method !== "POST") return new Response(null, { status: 405 });
|
|
690
|
+
const given = new TextEncoder().encode(request.headers.get("authorization") ?? "");
|
|
691
|
+
let difference = given.length ^ expected.length;
|
|
692
|
+
for (let i = 0; i < expected.length; i += 1) difference |= expected[i] ^ (given[i] ?? 0);
|
|
693
|
+
if (difference !== 0) return new Response(null, { status: 401 });
|
|
694
|
+
const frame = await answer(new Uint8Array(await request.arrayBuffer()));
|
|
695
|
+
return new Response(frame, {
|
|
696
|
+
status: 200,
|
|
697
|
+
headers: { "content-type": "application/octet-stream" }
|
|
698
|
+
});
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
const rangeOf = (r) => "suffix" in r ? { suffix: r.suffix } : r.length === void 0 ? { offset: r.offset } : {
|
|
702
|
+
offset: r.offset,
|
|
703
|
+
length: r.length
|
|
704
|
+
};
|
|
705
|
+
const encoder = new TextEncoder();
|
|
706
|
+
const decoder = new TextDecoder();
|
|
707
|
+
function encodeFrame(header, body = /* @__PURE__ */ new Uint8Array(0)) {
|
|
708
|
+
const json = encoder.encode(JSON.stringify(header));
|
|
709
|
+
const frame = new Uint8Array(4 + json.length + body.length);
|
|
710
|
+
new DataView(frame.buffer).setUint32(0, json.length, true);
|
|
711
|
+
frame.set(json, 4);
|
|
712
|
+
frame.set(body, 4 + json.length);
|
|
713
|
+
return frame;
|
|
714
|
+
}
|
|
715
|
+
function decodeFrame(frame) {
|
|
716
|
+
if (frame.length < 4) throw new StorageError("invalid", "the frame is shorter than its length");
|
|
717
|
+
const length = new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getUint32(0, true);
|
|
718
|
+
if (4 + length > frame.length) throw new StorageError("invalid", "the frame is shorter than its header");
|
|
719
|
+
let header;
|
|
720
|
+
try {
|
|
721
|
+
header = JSON.parse(decoder.decode(frame.subarray(4, 4 + length)));
|
|
722
|
+
} catch {
|
|
723
|
+
throw new StorageError("invalid", "the frame's header is not JSON");
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
header,
|
|
727
|
+
body: frame.subarray(4 + length)
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
//#endregion
|
|
731
|
+
export { bucketsOf, decodeFrame, encodeFrame, http, remote, repeatable, serve, serveHttp };
|