@midsummer-ai/sdk 0.0.0-stage → 0.1.0-next.419
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-CDk7H1FL.mjs +1871 -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 +126 -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 +96 -0
- package/database/stand-in.mjs +422 -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-BzI9at5x.mjs +237 -0
- package/tabs-D-P8_ci_.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/contract.d.mts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { p as Storage } from "./index-sgdyoA2P.mjs";
|
|
2
|
+
//#region src/contract.d.ts
|
|
3
|
+
interface ContractCase {
|
|
4
|
+
/** A line of the specification. */
|
|
5
|
+
readonly name: string;
|
|
6
|
+
run(storage: Storage): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
declare const storageContract: ReadonlyArray<ContractCase>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { ContractCase, storageContract };
|
package/contract.mjs
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { h as StorageError } from "./names-DfCBKupJ.mjs";
|
|
2
|
+
import "./index.mjs";
|
|
3
|
+
//#region src/contract.ts
|
|
4
|
+
/**
|
|
5
|
+
* Storage's contract: what every engine promises, as cases any test runner can run. The Stand-in
|
|
6
|
+
* runs them in this package; the real Capability runs the same cases against its own engine,
|
|
7
|
+
* through the wire. A behaviour that is not a case here is not promised.
|
|
8
|
+
*
|
|
9
|
+
* for (const c of storageContract) test(c.name, () => c.run(storage(makeEngine())))
|
|
10
|
+
*
|
|
11
|
+
* Each case gets a Storage with nothing in it and throws when the engine breaks the promise.
|
|
12
|
+
* Expected values are written out, never computed with the SDK's own code.
|
|
13
|
+
*/
|
|
14
|
+
const HELLO = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
|
|
15
|
+
const HELLO_WORLD = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
|
|
16
|
+
const storageContract = [
|
|
17
|
+
{
|
|
18
|
+
name: "an Object put is read back byte for byte, with its size, content type, metadata and SHA-256 etag",
|
|
19
|
+
async run(storage) {
|
|
20
|
+
const bucket = storage.bucket("avatars");
|
|
21
|
+
const put = await bucket.put("ada.txt", "hello", {
|
|
22
|
+
contentType: "text/plain",
|
|
23
|
+
metadata: { owner: "ada" }
|
|
24
|
+
});
|
|
25
|
+
equal(pick(put), {
|
|
26
|
+
key: "ada.txt",
|
|
27
|
+
size: 5,
|
|
28
|
+
etag: HELLO,
|
|
29
|
+
contentType: "text/plain",
|
|
30
|
+
metadata: { owner: "ada" }
|
|
31
|
+
}, "the put's answer");
|
|
32
|
+
const read = await bucket.get("ada.txt");
|
|
33
|
+
equal(read && text(read.body), "hello", "the bytes read");
|
|
34
|
+
equal(read && pick(read), pick(put), "the read's description");
|
|
35
|
+
equal(await bucket.head("ada.txt"), put, "the head");
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "a key that holds nothing reads and heads as null, and deleting it is not an error",
|
|
40
|
+
async run(storage) {
|
|
41
|
+
const bucket = storage.bucket("avatars");
|
|
42
|
+
equal(await bucket.get("nobody.png"), null, "the read");
|
|
43
|
+
equal(await bucket.head("nobody.png"), null, "the head");
|
|
44
|
+
await bucket.delete("nobody.png");
|
|
45
|
+
await bucket.put("ada.txt", "hello");
|
|
46
|
+
await bucket.delete("ada.txt");
|
|
47
|
+
equal(await bucket.get("ada.txt"), null, "the read after its delete");
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "a put replaces the Object and the next read sees it, whatever the caller does to its buffer afterwards",
|
|
52
|
+
async run(storage) {
|
|
53
|
+
const bucket = storage.bucket("avatars");
|
|
54
|
+
await bucket.put("ada.txt", "hello");
|
|
55
|
+
const bytes = new TextEncoder().encode("hello world");
|
|
56
|
+
const put = await bucket.put("ada.txt", bytes);
|
|
57
|
+
bytes.fill(0);
|
|
58
|
+
const read = await bucket.get("ada.txt");
|
|
59
|
+
equal(read && text(read.body), "hello world", "the bytes read");
|
|
60
|
+
equal(put.etag, HELLO_WORLD, "the new etag");
|
|
61
|
+
equal(put.contentType, "application/octet-stream", "the default content type");
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "a conditional put refuses to overwrite: ifNoneMatch when the key holds something, ifMatch when the etag differs",
|
|
66
|
+
async run(storage) {
|
|
67
|
+
const bucket = storage.bucket("avatars");
|
|
68
|
+
await bucket.put("ada.txt", "hello", { ifNoneMatch: "*" });
|
|
69
|
+
await fails(bucket.put("ada.txt", "again", { ifNoneMatch: "*" }), "conflict", "ifNoneMatch on a key that holds something");
|
|
70
|
+
await fails(bucket.put("ada.txt", "again", { ifMatch: HELLO_WORLD }), "conflict", "ifMatch with another etag");
|
|
71
|
+
await fails(bucket.put("grace.txt", "hello", { ifMatch: HELLO }), "conflict", "ifMatch on a key that holds nothing");
|
|
72
|
+
await bucket.put("ada.txt", "hello world", { ifMatch: HELLO });
|
|
73
|
+
const read = await bucket.get("ada.txt");
|
|
74
|
+
equal(read && text(read.body), "hello world", "the bytes after the conditional put that held");
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "a range read returns exactly those bytes, cut at the end, and a range that starts past the end is invalid",
|
|
79
|
+
async run(storage) {
|
|
80
|
+
const bucket = storage.bucket("videos");
|
|
81
|
+
await bucket.put("clip.bin", "0123456789");
|
|
82
|
+
for (const [options, bytes, range] of [
|
|
83
|
+
[
|
|
84
|
+
{ range: {
|
|
85
|
+
offset: 2,
|
|
86
|
+
length: 3
|
|
87
|
+
} },
|
|
88
|
+
"234",
|
|
89
|
+
{
|
|
90
|
+
offset: 2,
|
|
91
|
+
length: 3
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
{ range: { offset: 7 } },
|
|
96
|
+
"789",
|
|
97
|
+
{
|
|
98
|
+
offset: 7,
|
|
99
|
+
length: 3
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
[
|
|
103
|
+
{ range: {
|
|
104
|
+
offset: 8,
|
|
105
|
+
length: 100
|
|
106
|
+
} },
|
|
107
|
+
"89",
|
|
108
|
+
{
|
|
109
|
+
offset: 8,
|
|
110
|
+
length: 2
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
[
|
|
114
|
+
{ range: { suffix: 3 } },
|
|
115
|
+
"789",
|
|
116
|
+
{
|
|
117
|
+
offset: 7,
|
|
118
|
+
length: 3
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
[
|
|
122
|
+
{ range: { suffix: 100 } },
|
|
123
|
+
"0123456789",
|
|
124
|
+
{
|
|
125
|
+
offset: 0,
|
|
126
|
+
length: 10
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
]) {
|
|
130
|
+
const read = await bucket.get("clip.bin", options);
|
|
131
|
+
equal(read && [
|
|
132
|
+
text(read.body),
|
|
133
|
+
read.range,
|
|
134
|
+
read.size
|
|
135
|
+
], [
|
|
136
|
+
bytes,
|
|
137
|
+
range,
|
|
138
|
+
10
|
|
139
|
+
], `the read of ${JSON.stringify(options)}`);
|
|
140
|
+
}
|
|
141
|
+
await fails(bucket.get("clip.bin", { range: { offset: 10 } }), "invalid", "a range that starts at the end");
|
|
142
|
+
equal(await bucket.get("missing.bin", { range: {
|
|
143
|
+
offset: 0,
|
|
144
|
+
length: 1
|
|
145
|
+
} }), null, "a range of a key that holds nothing");
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "a listing is in UTF-8 byte order under its prefix, folds keys at the delimiter into prefixes, and pages with after",
|
|
150
|
+
async run(storage) {
|
|
151
|
+
const bucket = storage.bucket("photos");
|
|
152
|
+
for (const key of [
|
|
153
|
+
"b.txt",
|
|
154
|
+
"photos/dog.png",
|
|
155
|
+
"a.txt",
|
|
156
|
+
"photos/2026/2.png",
|
|
157
|
+
"Z.txt",
|
|
158
|
+
"photos/2026/1.png",
|
|
159
|
+
"photos/cat.png",
|
|
160
|
+
"😀",
|
|
161
|
+
"�"
|
|
162
|
+
]) await bucket.put(key, "x");
|
|
163
|
+
const keys = (listing) => [
|
|
164
|
+
listing.objects.map((o) => o.key),
|
|
165
|
+
listing.prefixes,
|
|
166
|
+
listing.next ?? null
|
|
167
|
+
];
|
|
168
|
+
equal(keys(await bucket.list()), [
|
|
169
|
+
[
|
|
170
|
+
"Z.txt",
|
|
171
|
+
"a.txt",
|
|
172
|
+
"b.txt",
|
|
173
|
+
"photos/2026/1.png",
|
|
174
|
+
"photos/2026/2.png",
|
|
175
|
+
"photos/cat.png",
|
|
176
|
+
"photos/dog.png",
|
|
177
|
+
"�",
|
|
178
|
+
"😀"
|
|
179
|
+
],
|
|
180
|
+
[],
|
|
181
|
+
null
|
|
182
|
+
], "the whole listing");
|
|
183
|
+
equal(keys(await bucket.list({
|
|
184
|
+
prefix: "photos/",
|
|
185
|
+
delimiter: "/"
|
|
186
|
+
})), [
|
|
187
|
+
["photos/cat.png", "photos/dog.png"],
|
|
188
|
+
["photos/2026/"],
|
|
189
|
+
null
|
|
190
|
+
], "one level of photos/");
|
|
191
|
+
equal(keys(await bucket.list({
|
|
192
|
+
prefix: "photos/",
|
|
193
|
+
delimiter: "/",
|
|
194
|
+
limit: 2
|
|
195
|
+
})), [
|
|
196
|
+
["photos/cat.png"],
|
|
197
|
+
["photos/2026/"],
|
|
198
|
+
"photos/cat.png"
|
|
199
|
+
], "the first page of two");
|
|
200
|
+
equal(keys(await bucket.list({
|
|
201
|
+
prefix: "photos/",
|
|
202
|
+
delimiter: "/",
|
|
203
|
+
limit: 2,
|
|
204
|
+
after: "photos/cat.png"
|
|
205
|
+
})), [
|
|
206
|
+
["photos/dog.png"],
|
|
207
|
+
[],
|
|
208
|
+
null
|
|
209
|
+
], "the second page");
|
|
210
|
+
equal(keys(await bucket.list({
|
|
211
|
+
prefix: "photos/",
|
|
212
|
+
delimiter: "/",
|
|
213
|
+
after: "photos/2026/"
|
|
214
|
+
})), [
|
|
215
|
+
["photos/cat.png", "photos/dog.png"],
|
|
216
|
+
[],
|
|
217
|
+
null
|
|
218
|
+
], "after a folded prefix, none of its keys");
|
|
219
|
+
equal(keys(await bucket.list({ limit: 9 })), [
|
|
220
|
+
(await bucket.list()).objects.map((o) => o.key),
|
|
221
|
+
[],
|
|
222
|
+
null
|
|
223
|
+
], "a page that ends exactly at the last key has no next");
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: "a copy puts the same content under a second key, with its content type and metadata",
|
|
228
|
+
async run(storage) {
|
|
229
|
+
const bucket = storage.bucket("avatars");
|
|
230
|
+
await bucket.put("ada.txt", "hello", {
|
|
231
|
+
contentType: "text/plain",
|
|
232
|
+
metadata: { owner: "ada" }
|
|
233
|
+
});
|
|
234
|
+
equal(pick(await bucket.copy("ada.txt", "ada-copy.txt")), {
|
|
235
|
+
key: "ada-copy.txt",
|
|
236
|
+
size: 5,
|
|
237
|
+
etag: HELLO,
|
|
238
|
+
contentType: "text/plain",
|
|
239
|
+
metadata: { owner: "ada" }
|
|
240
|
+
}, "the copy");
|
|
241
|
+
const read = await bucket.get("ada-copy.txt");
|
|
242
|
+
equal(read && text(read.body), "hello", "the copy's bytes");
|
|
243
|
+
await fails(bucket.copy("nobody.txt", "x.txt"), "not_found", "a copy from a key that holds nothing");
|
|
244
|
+
await fails(bucket.copy("ada.txt", "ada-copy.txt", { ifNoneMatch: "*" }), "conflict", "a conditional copy onto a key that holds something");
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "Buckets are separate: what one holds is not in another",
|
|
249
|
+
async run(storage) {
|
|
250
|
+
await storage.bucket("avatars").put("ada.txt", "hello");
|
|
251
|
+
equal(await storage.bucket("invoices").get("ada.txt"), null, "the read in another Bucket");
|
|
252
|
+
equal((await storage.bucket("invoices").list()).objects.length, 0, "the other Bucket's listing");
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: "names, ranges and limits are checked: Bucket names, empty keys, keys over 1,024 UTF-8 bytes, metadata names",
|
|
257
|
+
async run(storage) {
|
|
258
|
+
for (const name of [
|
|
259
|
+
"Avatars",
|
|
260
|
+
"ab",
|
|
261
|
+
"-avatars",
|
|
262
|
+
"avatars_2",
|
|
263
|
+
"a".repeat(64)
|
|
264
|
+
]) await fails(Promise.resolve().then(() => storage.bucket(name)), "invalid", `the Bucket name "${name}"`);
|
|
265
|
+
const bucket = storage.bucket("avatars");
|
|
266
|
+
await fails(bucket.put("", "x"), "invalid", "an empty key");
|
|
267
|
+
await fails(bucket.put("é".repeat(513), "x"), "invalid", "a key of 1,026 bytes");
|
|
268
|
+
await bucket.put("é".repeat(512), "x");
|
|
269
|
+
await fails(bucket.put("ada.txt", "x", { metadata: { Owner: "ada" } }), "invalid", "a metadata name with a capital");
|
|
270
|
+
await fails(bucket.get("ada.txt", { range: { offset: -1 } }), "invalid", "a negative range offset");
|
|
271
|
+
await fails(bucket.list({ limit: 0 }), "invalid", "a listing limit of 0");
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: "an upload in parts becomes one Object of their bytes in part-number order when it completes, and until then the key holds what it held",
|
|
276
|
+
async run(storage) {
|
|
277
|
+
const bucket = storage.bucket("media");
|
|
278
|
+
await bucket.put("film.mp4", "old");
|
|
279
|
+
const { uploadId } = await bucket.createUpload("film.mp4", {
|
|
280
|
+
contentType: "video/mp4",
|
|
281
|
+
metadata: { owner: "ada" }
|
|
282
|
+
});
|
|
283
|
+
const second = await bucket.uploadPart("film.mp4", uploadId, 2, "hello");
|
|
284
|
+
const first = await bucket.uploadPart("film.mp4", uploadId, 1, new Uint8Array(FIVE_MIB).fill(97));
|
|
285
|
+
equal([
|
|
286
|
+
second.partNumber,
|
|
287
|
+
second.etag,
|
|
288
|
+
second.size
|
|
289
|
+
], [
|
|
290
|
+
2,
|
|
291
|
+
HELLO,
|
|
292
|
+
5
|
|
293
|
+
], "the second part, sent first");
|
|
294
|
+
equal(first.size, FIVE_MIB, "the first part's size");
|
|
295
|
+
const before = await bucket.get("film.mp4");
|
|
296
|
+
equal(before && text(before.body), "old", "the key before the upload completes");
|
|
297
|
+
const done = await bucket.completeUpload("film.mp4", uploadId, [first, second]);
|
|
298
|
+
equal([
|
|
299
|
+
done.size,
|
|
300
|
+
done.contentType,
|
|
301
|
+
done.metadata
|
|
302
|
+
], [
|
|
303
|
+
FIVE_MIB + 5,
|
|
304
|
+
"video/mp4",
|
|
305
|
+
{ owner: "ada" }
|
|
306
|
+
], "the completed Object");
|
|
307
|
+
const read = await bucket.get("film.mp4");
|
|
308
|
+
equal(read && [
|
|
309
|
+
read.body.length,
|
|
310
|
+
read.body[0],
|
|
311
|
+
read.body[FIVE_MIB - 1],
|
|
312
|
+
text(read.body.subarray(FIVE_MIB))
|
|
313
|
+
], [
|
|
314
|
+
FIVE_MIB + 5,
|
|
315
|
+
97,
|
|
316
|
+
97,
|
|
317
|
+
"hello"
|
|
318
|
+
], "the joined bytes");
|
|
319
|
+
const whole = new Uint8Array(FIVE_MIB + 5).fill(97);
|
|
320
|
+
whole.set(new TextEncoder().encode("hello"), FIVE_MIB);
|
|
321
|
+
equal(done.etag, (await bucket.put("same.bin", whole)).etag, "the etag, the same as one put of the same bytes");
|
|
322
|
+
await fails(bucket.uploadPart("film.mp4", uploadId, 3, "more"), "not_found", "a part sent after completing", "upload");
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: "a part sent again replaces it, and completing checks each part's etag, their order, the 5 MiB floor and that parts but the last are one size, leaving the upload open when it refuses",
|
|
327
|
+
async run(storage) {
|
|
328
|
+
const bucket = storage.bucket("media");
|
|
329
|
+
const { uploadId } = await bucket.createUpload("film.mp4");
|
|
330
|
+
const stale = await bucket.uploadPart("film.mp4", uploadId, 1, "small");
|
|
331
|
+
const first = await bucket.uploadPart("film.mp4", uploadId, 1, new Uint8Array(FIVE_MIB));
|
|
332
|
+
const second = await bucket.uploadPart("film.mp4", uploadId, 2, "hello");
|
|
333
|
+
await fails(bucket.uploadPart("film.mp4", uploadId, 10001, "x"), "invalid", "part number 10,001");
|
|
334
|
+
await fails(bucket.completeUpload("film.mp4", uploadId, [stale, second]), "invalid", "a part whose etag was replaced", "part");
|
|
335
|
+
await fails(bucket.completeUpload("film.mp4", uploadId, [second, first]), "invalid", "parts out of order", "part-order");
|
|
336
|
+
await fails(bucket.completeUpload("film.mp4", uploadId, [first, {
|
|
337
|
+
partNumber: 3,
|
|
338
|
+
etag: HELLO
|
|
339
|
+
}]), "invalid", "a part never sent", "part");
|
|
340
|
+
const small = await bucket.createUpload("small.bin");
|
|
341
|
+
const a = await bucket.uploadPart("small.bin", small.uploadId, 1, "hello");
|
|
342
|
+
const b = await bucket.uploadPart("small.bin", small.uploadId, 2, "hello");
|
|
343
|
+
await fails(bucket.completeUpload("small.bin", small.uploadId, [a, b]), "invalid", "a first part under 5 MiB", "part-too-small");
|
|
344
|
+
const uneven = await bucket.createUpload("uneven.bin");
|
|
345
|
+
const big = await bucket.uploadPart("uneven.bin", uneven.uploadId, 1, new Uint8Array(FIVE_MIB + 1));
|
|
346
|
+
const less = await bucket.uploadPart("uneven.bin", uneven.uploadId, 2, new Uint8Array(FIVE_MIB));
|
|
347
|
+
const last = await bucket.uploadPart("uneven.bin", uneven.uploadId, 3, "hello");
|
|
348
|
+
await fails(bucket.completeUpload("uneven.bin", uneven.uploadId, [
|
|
349
|
+
big,
|
|
350
|
+
less,
|
|
351
|
+
last
|
|
352
|
+
]), "invalid", "a middle part smaller than the first", "part");
|
|
353
|
+
const growing = await bucket.createUpload("growing.bin");
|
|
354
|
+
const one = await bucket.uploadPart("growing.bin", growing.uploadId, 1, new Uint8Array(FIVE_MIB));
|
|
355
|
+
const two = await bucket.uploadPart("growing.bin", growing.uploadId, 2, new Uint8Array(FIVE_MIB + 1));
|
|
356
|
+
await fails(bucket.completeUpload("growing.bin", growing.uploadId, [one, two]), "invalid", "a last part larger than the first", "part");
|
|
357
|
+
equal((await bucket.completeUpload("film.mp4", uploadId, [first, second])).size, FIVE_MIB + 5, "the upload completed after its refusals");
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: "an Object larger than one call carries is put, read whole and read by any range, with the etag one put of its bytes has",
|
|
362
|
+
async run(storage) {
|
|
363
|
+
const bucket = storage.bucket("media");
|
|
364
|
+
const body = pattern(LARGE);
|
|
365
|
+
const put = await bucket.put("big.bin", body);
|
|
366
|
+
equal([put.size, put.etag], [LARGE, await hex(body)], "its size, and the hash of its bytes as its etag");
|
|
367
|
+
const whole = await bucket.get("big.bin");
|
|
368
|
+
equal([
|
|
369
|
+
whole?.size,
|
|
370
|
+
whole?.range,
|
|
371
|
+
await hex(whole.body)
|
|
372
|
+
], [
|
|
373
|
+
LARGE,
|
|
374
|
+
void 0,
|
|
375
|
+
put.etag
|
|
376
|
+
], "read whole, every byte");
|
|
377
|
+
const ranges = [
|
|
378
|
+
{
|
|
379
|
+
offset: 5242887,
|
|
380
|
+
length: 31457280
|
|
381
|
+
},
|
|
382
|
+
{ suffix: 20971523 },
|
|
383
|
+
{ offset: LARGE - 1048576 }
|
|
384
|
+
];
|
|
385
|
+
for (const range of ranges) {
|
|
386
|
+
const read = await bucket.get("big.bin", { range });
|
|
387
|
+
const offset = "suffix" in range ? LARGE - range.suffix : range.offset;
|
|
388
|
+
const length = "suffix" in range ? range.suffix : range.length ?? LARGE - offset;
|
|
389
|
+
equal(read?.range, {
|
|
390
|
+
offset,
|
|
391
|
+
length
|
|
392
|
+
}, `the range ${JSON.stringify(range)}`);
|
|
393
|
+
equal(await hex(read.body), await hex(body.subarray(offset, offset + length)), `the bytes of ${JSON.stringify(range)}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: "an aborted upload takes no more parts and cannot complete, and an unknown one is not found",
|
|
399
|
+
async run(storage) {
|
|
400
|
+
const bucket = storage.bucket("media");
|
|
401
|
+
await bucket.put("film.mp4", "old");
|
|
402
|
+
const { uploadId } = await bucket.createUpload("film.mp4");
|
|
403
|
+
const part = await bucket.uploadPart("film.mp4", uploadId, 1, "hello");
|
|
404
|
+
await bucket.abortUpload("film.mp4", uploadId);
|
|
405
|
+
await fails(bucket.uploadPart("film.mp4", uploadId, 2, "x"), "not_found", "a part after aborting", "upload");
|
|
406
|
+
await fails(bucket.completeUpload("film.mp4", uploadId, [part]), "not_found", "completing after aborting", "upload");
|
|
407
|
+
await fails(bucket.abortUpload("film.mp4", uploadId), "not_found", "aborting twice", "upload");
|
|
408
|
+
await fails(bucket.uploadPart("film.mp4", "no-such-upload", 1, "x"), "not_found", "a part of an unknown upload", "upload");
|
|
409
|
+
const read = await bucket.get("film.mp4");
|
|
410
|
+
equal(read && text(read.body), "old", "the key after the abort");
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: "an upload made with ifNoneMatch refuses to complete over a key that holds something",
|
|
415
|
+
async run(storage) {
|
|
416
|
+
const bucket = storage.bucket("media");
|
|
417
|
+
const { uploadId } = await bucket.createUpload("film.mp4", { ifNoneMatch: "*" });
|
|
418
|
+
const part = await bucket.uploadPart("film.mp4", uploadId, 1, "hello");
|
|
419
|
+
await bucket.put("film.mp4", "someone else's");
|
|
420
|
+
await fails(bucket.completeUpload("film.mp4", uploadId, [part]), "conflict", "completing over a key that now holds something");
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
];
|
|
424
|
+
function pick(o) {
|
|
425
|
+
return {
|
|
426
|
+
key: o.key,
|
|
427
|
+
size: o.size,
|
|
428
|
+
etag: o.etag,
|
|
429
|
+
contentType: o.contentType,
|
|
430
|
+
metadata: o.metadata
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const text = (bytes) => new TextDecoder().decode(bytes);
|
|
434
|
+
function equal(actual, expected, what) {
|
|
435
|
+
const a = JSON.stringify(actual), e = JSON.stringify(expected);
|
|
436
|
+
if (a !== e) throw new Error(`${what}: expected ${e}, got ${a}`);
|
|
437
|
+
}
|
|
438
|
+
async function fails(call, code, what, reason) {
|
|
439
|
+
const expected = `a StorageError "${code}"${reason ? ` for ${reason}` : ""}`;
|
|
440
|
+
try {
|
|
441
|
+
await call;
|
|
442
|
+
} catch (error) {
|
|
443
|
+
if (error instanceof StorageError && error.code === code && (reason === void 0 || error.reason === reason)) return;
|
|
444
|
+
throw new Error(`${what}: expected ${expected}, got ${error instanceof StorageError ? `"${error.code}" (${error.reason ?? "no reason"}): ${error.message}` : String(error)}`);
|
|
445
|
+
}
|
|
446
|
+
throw new Error(`${what}: expected ${expected}, but it succeeded`);
|
|
447
|
+
}
|
|
448
|
+
const FIVE_MIB = 5242880;
|
|
449
|
+
const LARGE = 41943040;
|
|
450
|
+
/** `size` bytes that differ along their length, so a byte read from the wrong place shows. */
|
|
451
|
+
function pattern(size) {
|
|
452
|
+
const bytes = new Uint8Array(size);
|
|
453
|
+
let state = 2463534242;
|
|
454
|
+
for (let i = 0; i < size; i += 4) {
|
|
455
|
+
state ^= state << 13;
|
|
456
|
+
state ^= state >>> 17;
|
|
457
|
+
state ^= state << 5;
|
|
458
|
+
bytes[i] = state & 255;
|
|
459
|
+
bytes[i + 1] = state >>> 8 & 255;
|
|
460
|
+
bytes[i + 2] = state >>> 16 & 255;
|
|
461
|
+
bytes[i + 3] = state >>> 24 & 255;
|
|
462
|
+
}
|
|
463
|
+
return bytes;
|
|
464
|
+
}
|
|
465
|
+
async function hex(bytes) {
|
|
466
|
+
return [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
467
|
+
}
|
|
468
|
+
//#endregion
|
|
469
|
+
export { storageContract };
|