@sproutboat/runtime 0.44.0 → 0.46.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/CHANGELOG.md +44 -0
- package/package.json +1 -1
- package/src/asset-rules.test.js +51 -30
- package/src/native-fetch-prelude.js +1213 -67
- package/src/r2-lazy.test.js +101 -0
- package/src/transport-broker.js +13 -0
- package/src/transport-embedded.js +534 -47
- package/test/fixtures/asset-redirect-cases.ts +28 -0
- package/test/fixtures/do-evict-app.js +172 -0
- package/test/native-asset-redirects.ts +80 -0
- package/test/native-cc.ts +178 -0
- package/test/native-compile.ts +53 -22
- package/test/native-db-table.ts +244 -0
- package/test/native-do-evict.ts +345 -0
- package/test/native-lto.ts +79 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// R2 get() reads an object as asked (sproutd #99): a range reads only its window, a body streams one chunk at
|
|
2
|
+
// a time, and nothing reads the whole object unless text(), json(), arrayBuffer() or blob() ask for all of it.
|
|
3
|
+
// The prelude is text, so this lifts the strict R2 object block out and runs it against a fake file whose reads
|
|
4
|
+
// are recorded, as r2-object.test.js does for the legacy object.
|
|
5
|
+
import { expect, test } from "bun:test";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const src = readFileSync(fileURLToPath(new URL("./native-fetch-prelude.js", import.meta.url)), "utf8");
|
|
10
|
+
const start = src.indexOf("function __sbR2Key(");
|
|
11
|
+
const end = src.indexOf("function __sbMakeR2(");
|
|
12
|
+
const isStrStart = src.indexOf("function __sbIsStr(");
|
|
13
|
+
const isStrEnd = src.indexOf("function __sbIsFn(");
|
|
14
|
+
const bytesStart = src.indexOf("const __SB_BYTES_WINDOW");
|
|
15
|
+
const utf8Start = src.indexOf("function __sbBufFrom(");
|
|
16
|
+
const utf8End = src.indexOf("function __sbRawBodyResponse(");
|
|
17
|
+
const lazyStart = src.indexOf("const __SB_R2_CHUNK");
|
|
18
|
+
if (
|
|
19
|
+
[start, end, isStrStart, isStrEnd, bytesStart, utf8Start, utf8End, lazyStart].some((i) => i === -1) ||
|
|
20
|
+
end < start ||
|
|
21
|
+
lazyStart < start ||
|
|
22
|
+
lazyStart > end
|
|
23
|
+
)
|
|
24
|
+
throw new Error("the strict R2 object block moved in the prelude");
|
|
25
|
+
|
|
26
|
+
// oxlint-disable-next-line no-function-constructor -- lifting pure blocks out of the text-only prelude for test
|
|
27
|
+
const load = new Function(
|
|
28
|
+
"__sbR2ReadRange",
|
|
29
|
+
`function __sbFromUtf8Native() { return ""; }\nfunction __sbToBytesNative() { return ""; }\nfunction __sbFillBytes() { return false; }\n${src.slice(isStrStart, isStrEnd)}\n${src.slice(bytesStart, utf8Start)}\n${src.slice(utf8Start, utf8End)}\n${src.slice(start, end)}\nreturn { __sbR2StrictObject, __SB_R2_CHUNK };`,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// An "object file" of SIZE bytes, byte i being i % 251, and the reads asked of it.
|
|
33
|
+
const reads = [];
|
|
34
|
+
const file = (size) => (_bucket, _key, _blob, offset, view) => {
|
|
35
|
+
reads.push({ offset, length: view.length });
|
|
36
|
+
const n = Math.max(0, Math.min(view.length, size - offset));
|
|
37
|
+
for (let i = 0; i < n; i++) view[i] = (offset + i) % 251;
|
|
38
|
+
return n;
|
|
39
|
+
};
|
|
40
|
+
const meta = (size) => ({ key: "big.bin", size, etag: "abc123", uploaded: "2026-09-14T00:00:00.000Z" });
|
|
41
|
+
const lazy = { lazy: true, bucket: "B", key: "big.bin", blobId: "abc123" };
|
|
42
|
+
const drain = async (stream) => {
|
|
43
|
+
const reader = stream.getReader();
|
|
44
|
+
const chunks = [];
|
|
45
|
+
for (;;) {
|
|
46
|
+
const { value, done } = await reader.read();
|
|
47
|
+
if (done) return chunks;
|
|
48
|
+
chunks.push(value);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
test("a body streams in chunks and never reads the object whole", async () => {
|
|
53
|
+
const { __sbR2StrictObject, __SB_R2_CHUNK } = load(file(3 * 1024 * 1024 + 100));
|
|
54
|
+
reads.length = 0;
|
|
55
|
+
const obj = __sbR2StrictObject(meta(3 * 1024 * 1024 + 100), lazy, { offset: 0, length: 3 * 1024 * 1024 + 100 });
|
|
56
|
+
expect(reads.length).toBe(0);
|
|
57
|
+
const chunks = await drain(obj.body);
|
|
58
|
+
expect(chunks.length).toBe(Math.ceil((3 * 1024 * 1024 + 100) / __SB_R2_CHUNK));
|
|
59
|
+
expect(Math.max(...reads.map((r) => r.length))).toBe(__SB_R2_CHUNK);
|
|
60
|
+
expect(reads.map((r) => r.offset)).toEqual(chunks.map((_, i) => i * __SB_R2_CHUNK));
|
|
61
|
+
expect(chunks[0][250]).toBe(250);
|
|
62
|
+
expect(chunks[1][0]).toBe(__SB_R2_CHUNK % 251);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("a range reads only its window", async () => {
|
|
66
|
+
const { __sbR2StrictObject } = load(file(10 * 1024 * 1024));
|
|
67
|
+
reads.length = 0;
|
|
68
|
+
const range = { offset: 5 * 1024 * 1024 + 7, length: 1000 };
|
|
69
|
+
const obj = __sbR2StrictObject(meta(10 * 1024 * 1024), lazy, range);
|
|
70
|
+
const chunks = await drain(obj.body);
|
|
71
|
+
expect(chunks.length).toBe(1);
|
|
72
|
+
expect(reads).toEqual([{ offset: range.offset, length: 1000 }]);
|
|
73
|
+
expect(chunks[0].length).toBe(1000);
|
|
74
|
+
expect(chunks[0][0]).toBe(range.offset % 251);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("arrayBuffer() and text() read the window in one read", async () => {
|
|
78
|
+
const { __sbR2StrictObject } = load(file(600));
|
|
79
|
+
reads.length = 0;
|
|
80
|
+
const range = { offset: 100, length: 400 };
|
|
81
|
+
const obj = __sbR2StrictObject(meta(600), lazy, range);
|
|
82
|
+
const buffer = await obj.arrayBuffer();
|
|
83
|
+
expect(buffer.byteLength).toBe(400);
|
|
84
|
+
expect(new Uint8Array(buffer)[0]).toBe(100);
|
|
85
|
+
expect(reads).toEqual([{ offset: 100, length: 400 }]);
|
|
86
|
+
await expect(obj.text()).rejects.toThrow(/already been used/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("an empty window closes at once without reading", async () => {
|
|
90
|
+
const { __sbR2StrictObject } = load(file(0));
|
|
91
|
+
reads.length = 0;
|
|
92
|
+
const obj = __sbR2StrictObject(meta(0), lazy, { offset: 0, length: 0 });
|
|
93
|
+
expect((await drain(obj.body)).length).toBe(0);
|
|
94
|
+
expect(reads.length).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("a read that cannot be completed errors the stream", async () => {
|
|
98
|
+
const { __sbR2StrictObject } = load(() => -1);
|
|
99
|
+
const obj = __sbR2StrictObject(meta(100), lazy, { offset: 0, length: 100 });
|
|
100
|
+
await expect(drain(obj.body)).rejects.toThrow(/could not be read/);
|
|
101
|
+
});
|
package/src/transport-broker.js
CHANGED
|
@@ -1400,6 +1400,19 @@ globalThis.__sbR2Get = function (bucket, key) {
|
|
|
1400
1400
|
return { found: true, object: reply.object, body: __sbTakeBin() };
|
|
1401
1401
|
};
|
|
1402
1402
|
|
|
1403
|
+
// The broker returns an object whole (r2.get); only the embedded transport reads a file as asked.
|
|
1404
|
+
globalThis.__sbR2Open = function () {
|
|
1405
|
+
return null;
|
|
1406
|
+
};
|
|
1407
|
+
globalThis.__sbR2ReadRange = function () {
|
|
1408
|
+
return -1;
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
// The broker's own transfer listener owns /__sb/r2/transfer/: this runtime serves none of it.
|
|
1412
|
+
function __sbR2XferOp() {
|
|
1413
|
+
return "404";
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1403
1416
|
globalThis.__sbR2MultipartPut = function (bucket, key, uploadId, partNumber, body) {
|
|
1404
1417
|
const token = __sbEnv("SB_BROKER_TOKEN");
|
|
1405
1418
|
const reply = JSON.parse(
|