@sproutboat/runtime 0.44.0 → 0.45.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 +6 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +255 -13
- package/src/r2-lazy.test.js +101 -0
- package/src/transport-broker.js +13 -0
- package/src/transport-embedded.js +224 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.45.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2b6d912: R2 large objects on the embedded transport (sproutd #99 and #100). `get()` reads an object as asked: a `range` reads only its window from the object's file, and `body` is a stream of 256 KiB reads, so an object of any size can be returned with `new Response(object.body)`; `text()`, `json()`, `arrayBuffer()` and `blob()` still read the whole window. A streamed response now has flow control on a host that offers it (sproutd): the pump reads no more of its source while a window of its bytes is unacknowledged, and a response with a declared `content-length` is sent with it. A worker whose host forwards `/__sb/r2/transfer/BUCKET/TOKEN` (sproutd) serves the ticket itself, with the standalone server's statuses and headers: uploads and downloads move a window at a time between the connection and the object's file. A standalone executable answers a HEAD request with the length the app declared.
|
|
8
|
+
|
|
3
9
|
## 0.44.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -11024,28 +11024,63 @@ function __sbR2StrictObject(meta, bytes, range, include) {
|
|
|
11024
11024
|
};
|
|
11025
11025
|
if (bytes == null) return obj;
|
|
11026
11026
|
let used = false;
|
|
11027
|
+
// A lazy body (bytes.lazy: the object's file, read as asked) is never read whole unless text(), json(),
|
|
11028
|
+
// arrayBuffer() or blob() ask for all of it; its stream reads one chunk at a time.
|
|
11029
|
+
const lazy = bytes.lazy === true;
|
|
11027
11030
|
const take = () => {
|
|
11028
11031
|
if (used) throw new TypeError("Body has already been used. It can only be used once. Use tee() first if you need to read it twice.");
|
|
11029
11032
|
used = true;
|
|
11030
|
-
return bytes;
|
|
11033
|
+
return lazy ? __sbR2ReadAll(bytes, range) : bytes;
|
|
11031
11034
|
};
|
|
11032
11035
|
// The stream is made when first asked for: text() and friends read the bytes directly,
|
|
11033
11036
|
// so an object read as text never holds a second copy (sproutd #22).
|
|
11034
11037
|
let stream = null;
|
|
11035
11038
|
Object.defineProperty(obj, "body", {
|
|
11036
11039
|
get: () => {
|
|
11037
|
-
if (stream === null) stream = new Response(__sbBufFrom(bytes)).body;
|
|
11040
|
+
if (stream === null) stream = lazy ? __sbR2LazyStream(bytes, range) : new Response(__sbBufFrom(bytes)).body;
|
|
11038
11041
|
return stream;
|
|
11039
11042
|
},
|
|
11040
11043
|
enumerable: true,
|
|
11041
11044
|
});
|
|
11042
11045
|
Object.defineProperty(obj, "bodyUsed", { get: () => used, enumerable: true });
|
|
11043
|
-
|
|
11044
|
-
obj.
|
|
11045
|
-
obj.
|
|
11046
|
-
obj.
|
|
11046
|
+
// A lazy body reads into a Uint8Array (never a bytestring), so each of these takes bytes, not text.
|
|
11047
|
+
obj.text = async () => (lazy ? new TextDecoder().decode(take()) : __sbFromUtf8(take()));
|
|
11048
|
+
obj.json = async () => JSON.parse(lazy ? new TextDecoder().decode(take()) : __sbFromUtf8(take()));
|
|
11049
|
+
obj.arrayBuffer = async () => (lazy ? take().buffer : __sbBufFrom(take()));
|
|
11050
|
+
obj.blob = async () => new Blob([lazy ? take().buffer : __sbBufFrom(take())], { type: obj.httpMetadata.contentType || "" });
|
|
11047
11051
|
return obj;
|
|
11048
11052
|
}
|
|
11053
|
+
// The chunk a streamed R2 body reads at a time (a Uint8Array of this size, then collected): a chunk is
|
|
11054
|
+
// what the worker's heap holds per read, and what the response's flow control hands to the host.
|
|
11055
|
+
const __SB_R2_CHUNK = 256 * 1024;
|
|
11056
|
+
// The bytes of a lazy body's window, all of them: text(), json(), arrayBuffer() and blob() need the whole
|
|
11057
|
+
// object in memory whatever they are called on.
|
|
11058
|
+
function __sbR2ReadAll(src, range) {
|
|
11059
|
+
const out = new Uint8Array(range.length);
|
|
11060
|
+
if (range.length > 0 && __sbR2ReadRange(src.bucket, src.key, src.blobId, range.offset, out) !== range.length)
|
|
11061
|
+
throw new Error("sproutboat r2.get: the object could not be read");
|
|
11062
|
+
return out;
|
|
11063
|
+
}
|
|
11064
|
+
// A lazy body's window as a ReadableStream of __SB_R2_CHUNK-byte reads.
|
|
11065
|
+
function __sbR2LazyStream(src, range) {
|
|
11066
|
+
let at = 0;
|
|
11067
|
+
return new ReadableStream({
|
|
11068
|
+
pull(controller) {
|
|
11069
|
+
if (at >= range.length) {
|
|
11070
|
+
controller.close();
|
|
11071
|
+
return;
|
|
11072
|
+
}
|
|
11073
|
+
const n = Math.min(__SB_R2_CHUNK, range.length - at);
|
|
11074
|
+
const chunk = new Uint8Array(n);
|
|
11075
|
+
if (__sbR2ReadRange(src.bucket, src.key, src.blobId, range.offset + at, chunk) !== n) {
|
|
11076
|
+
controller.error(new Error("sproutboat r2.get: the object could not be read"));
|
|
11077
|
+
return;
|
|
11078
|
+
}
|
|
11079
|
+
at += n;
|
|
11080
|
+
controller.enqueue(chunk);
|
|
11081
|
+
},
|
|
11082
|
+
});
|
|
11083
|
+
}
|
|
11049
11084
|
// A hex string's bytes, one char per byte.
|
|
11050
11085
|
function __sbR2Bytes(hex) {
|
|
11051
11086
|
let out = "";
|
|
@@ -11092,7 +11127,19 @@ function __sbMakeR2(name) {
|
|
|
11092
11127
|
get(key, options) {
|
|
11093
11128
|
return attempt(() => {
|
|
11094
11129
|
const o = options || {};
|
|
11095
|
-
const
|
|
11130
|
+
const objectKey = __sbR2Key(key, "get");
|
|
11131
|
+
// Where the transport can read an object's file as asked (embedded: sproutd and standalone), get() takes its
|
|
11132
|
+
// metadata and no bytes: a range reads only its own window, a body streams. The broker answers null here
|
|
11133
|
+
// and returns the object whole, as before.
|
|
11134
|
+
const opened = __sbR2Open(name, objectKey);
|
|
11135
|
+
if (opened !== null) {
|
|
11136
|
+
if (!opened.found) return null;
|
|
11137
|
+
const meta = opened.object;
|
|
11138
|
+
if (!__sbR2OnlyIf(o.onlyIf, meta.md5 || meta.etag, meta.uploaded)) return __sbR2StrictObject(meta, null, { offset: 0, length: meta.size });
|
|
11139
|
+
const window = o.range == null ? { offset: 0, length: meta.size } : __sbR2Range(o.range, meta.size);
|
|
11140
|
+
return __sbR2StrictObject(meta, { lazy: true, bucket: name, key: objectKey, blobId: opened.blobId }, window);
|
|
11141
|
+
}
|
|
11142
|
+
const r = __sbR2Get(name, objectKey);
|
|
11096
11143
|
if (!r.found) return null;
|
|
11097
11144
|
const meta = r.object;
|
|
11098
11145
|
if (!__sbR2OnlyIf(o.onlyIf, meta.md5 || meta.etag, meta.uploaded)) return __sbR2StrictObject(meta, null, { offset: 0, length: meta.size });
|
|
@@ -14090,7 +14137,8 @@ function __sbHasStreamBody(res) {
|
|
|
14090
14137
|
// x-sb-stream header, the native writer sends the status and headers and holds the
|
|
14091
14138
|
// connection open, and each chunk is written as it arrives. Hosts that do not stream
|
|
14092
14139
|
// (sproutd's workers, for now) link the weak fallbacks, report unsupported and keep buffering.
|
|
14093
|
-
// op: "s" supported, "o" open, "w" write data, "
|
|
14140
|
+
// op: "s" supported, "o" open, "w" write data (a bytestring), "W" write the Uint8Array in data, "p" the bytes written
|
|
14141
|
+
// the client's connection has not taken yet, "k" park the timer in data until that falls, "e" end. Returns a short string.
|
|
14094
14142
|
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block, not by JS.
|
|
14095
14143
|
function __sbStreamOp(op, id, data) {
|
|
14096
14144
|
let out = "";
|
|
@@ -14100,6 +14148,11 @@ function __sbStreamOp(op, id, data) {
|
|
|
14100
14148
|
extern unsigned int sb_stream_open(void);
|
|
14101
14149
|
extern int sb_stream_write(unsigned int, const char*, size_t);
|
|
14102
14150
|
extern void sb_stream_end(unsigned int);
|
|
14151
|
+
// Flow control (sproutd): the bytes written and not yet taken by the client's connection, and a timer the
|
|
14152
|
+
// host fires when that falls. A host without them (a standalone executable, whose server buffers by itself)
|
|
14153
|
+
// reports none, so a stream is never held back there.
|
|
14154
|
+
extern size_t sb_stream_pending(unsigned int) __attribute__((weak));
|
|
14155
|
+
extern void sb_stream_wait(unsigned int, unsigned int) __attribute__((weak));
|
|
14103
14156
|
const char* __o; size_t __ol; char* __oo = 0;
|
|
14104
14157
|
porf_native_fetch_read_value(op, &__o, &__ol, &__oo);
|
|
14105
14158
|
char __op = __ol ? __o[0] : 0;
|
|
@@ -14119,7 +14172,17 @@ function __sbStreamOp(op, id, data) {
|
|
|
14119
14172
|
int __w = -1;
|
|
14120
14173
|
if (porf_native_fetch_read_raw_bytes(data, &__d, &__dl) == 0) __w = sb_stream_write(__id, __d, __dl);
|
|
14121
14174
|
snprintf(__r, sizeof(__r), "%d", __w);
|
|
14122
|
-
} else if (__op == '
|
|
14175
|
+
} else if (__op == 'W' && data.type == 81) {
|
|
14176
|
+
// A Uint8Array written as it is, with no bytestring copy of it.
|
|
14177
|
+
const u32 __ta = (u32)data.val;
|
|
14178
|
+
const int32_t __len = *(int32_t*)(MEM + __ta);
|
|
14179
|
+
const u32 __at = *(u32*)(MEM + __ta + 4) + 4;
|
|
14180
|
+
int __w = -1;
|
|
14181
|
+
if (__len >= 0) __w = sb_stream_write(__id, (const char*)(MEM + __at), (size_t)__len);
|
|
14182
|
+
snprintf(__r, sizeof(__r), "%d", __w);
|
|
14183
|
+
} else if (__op == 'p') snprintf(__r, sizeof(__r), "%zu", sb_stream_pending ? sb_stream_pending(__id) : (size_t)0);
|
|
14184
|
+
else if (__op == 'k') { if (sb_stream_wait && data.type == 1 && data.val > 0) sb_stream_wait(__id, (unsigned int)data.val); }
|
|
14185
|
+
else if (__op == 'e') { sb_stream_end(__id); snprintf(__r, sizeof(__r), "0"); }
|
|
14123
14186
|
}
|
|
14124
14187
|
out = porf_box((f64)porf_native_fetch_alloc_bytestring(__r, strlen(__r)), 195);
|
|
14125
14188
|
`;
|
|
@@ -14130,6 +14193,16 @@ function __sbStreamingSupported() {
|
|
|
14130
14193
|
if (__sbStreaming < 0) __sbStreaming = __sbStreamOp("s", "0", "") === "1" ? 1 : 0;
|
|
14131
14194
|
return __sbStreaming === 1;
|
|
14132
14195
|
}
|
|
14196
|
+
// What the client's connection may hold of a stream before the pump waits for it (the host credits bytes as
|
|
14197
|
+
// they leave): a client slower than the app is waited for, not buffered for or cut off.
|
|
14198
|
+
const __SB_STREAM_WINDOW = 1024 * 1024;
|
|
14199
|
+
// Resolves when the host says the stream's unsent bytes fell (or the client went away).
|
|
14200
|
+
function __sbStreamDrained(id) {
|
|
14201
|
+
return new Promise((resolve) => {
|
|
14202
|
+
const timer = setTimeout(resolve, 2147483647);
|
|
14203
|
+
__sbStreamOp("k", id, timer);
|
|
14204
|
+
});
|
|
14205
|
+
}
|
|
14133
14206
|
function __sbStreamResponse(response) {
|
|
14134
14207
|
const stream = __sbRawBody(response);
|
|
14135
14208
|
const id = __sbStreamOp("o", "0", "");
|
|
@@ -14148,10 +14221,22 @@ function __sbStreamResponse(response) {
|
|
|
14148
14221
|
else if (value instanceof ArrayBuffer) bytes = new Uint8Array(value);
|
|
14149
14222
|
else if (ArrayBuffer.isView(value)) bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
14150
14223
|
else throw new TypeError("This ReadableStream did not return bytes.");
|
|
14151
|
-
if (bytes.length > 0
|
|
14152
|
-
//
|
|
14153
|
-
|
|
14154
|
-
|
|
14224
|
+
if (bytes.length > 0) {
|
|
14225
|
+
// A whole Uint8Array goes to the host without a copy; a view of part of a buffer is copied.
|
|
14226
|
+
const wrote =
|
|
14227
|
+
bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength
|
|
14228
|
+
? __sbStreamOp("W", id, bytes)
|
|
14229
|
+
: __sbStreamOp("w", id, __sbToBytes(bytes));
|
|
14230
|
+
if (wrote !== "0") {
|
|
14231
|
+
// The client went away: stop the source.
|
|
14232
|
+
reader.cancel();
|
|
14233
|
+
break;
|
|
14234
|
+
}
|
|
14235
|
+
// Flow control: while the connection holds more than the window, read no more of the source.
|
|
14236
|
+
while (Number(__sbStreamOp("p", id, "")) > __SB_STREAM_WINDOW) {
|
|
14237
|
+
await __sbStreamDrained(id);
|
|
14238
|
+
// Woken by the client going away: the next write says so (-1).
|
|
14239
|
+
}
|
|
14155
14240
|
}
|
|
14156
14241
|
}
|
|
14157
14242
|
} catch (error) {
|
|
@@ -15891,7 +15976,164 @@ function __sbTrustedProxiesOnce() {
|
|
|
15891
15976
|
return __sbTrustedCache;
|
|
15892
15977
|
}
|
|
15893
15978
|
|
|
15979
|
+
// R2 transfer tickets (createUploadUrl, createDownloadUrl): /__sb/r2/transfer/BUCKET/TOKEN, scoped to one object and
|
|
15980
|
+
// good once. A standalone executable's native server answers them before JavaScript runs (patchR2Transfer, #195);
|
|
15981
|
+
// on a host that forwards every path to the worker (sproutd) the worker answers them here with the same statuses
|
|
15982
|
+
// and headers, before the app's fetch() sees the request. The body goes straight between the connection and the
|
|
15983
|
+
// object's file, one step at a time (__sbR2XferOp): at most a window of it is ever in the worker.
|
|
15984
|
+
const __SB_R2_TRANSFER = "/__sb/r2/transfer/";
|
|
15985
|
+
// Set by the embedded transport (it has the ticket contexts and their steps); the broker's own listener owns the path.
|
|
15986
|
+
// oxlint-disable-next-line no-unassigned-vars -- set by the embedded transport, above
|
|
15987
|
+
var __sbR2TransferOn; // no initializer: the embedded transport (above) sets it, and an initializer here would run after it
|
|
15988
|
+
// [bucket, token] when the request is for a ticket (the same shape the native server accepts), else null.
|
|
15989
|
+
function __sbR2TransferTarget(request) {
|
|
15990
|
+
const method = request.method;
|
|
15991
|
+
if (method !== "GET" && method !== "PUT" && method !== "HEAD" && method !== "POST" && method !== "DELETE") return null;
|
|
15992
|
+
const url = request.url;
|
|
15993
|
+
const at = url.indexOf(__SB_R2_TRANSFER);
|
|
15994
|
+
if (at < 0) return null;
|
|
15995
|
+
const hostEnd = url.indexOf("/", url.indexOf("//") + 2);
|
|
15996
|
+
if (at !== hostEnd) return null;
|
|
15997
|
+
let rest = url.slice(at + __SB_R2_TRANSFER.length);
|
|
15998
|
+
const cut = rest.search(/[?#]/);
|
|
15999
|
+
if (cut >= 0) rest = rest.slice(0, cut);
|
|
16000
|
+
const slash = rest.indexOf("/");
|
|
16001
|
+
if (slash <= 0 || slash > 255 || rest.indexOf("/", slash + 1) >= 0) return null;
|
|
16002
|
+
const bucket = rest.slice(0, slash);
|
|
16003
|
+
const token = rest.slice(slash + 1);
|
|
16004
|
+
if (token.length !== 24 || !/^[0-9a-f]+$/.test(token) || !/^[A-Za-z0-9_]+$/.test(bucket)) return null;
|
|
16005
|
+
return [bucket, token];
|
|
16006
|
+
}
|
|
16007
|
+
function __sbR2TransferError(status, text) {
|
|
16008
|
+
return new Response(text, { status, headers: { "content-type": "text/plain" } });
|
|
16009
|
+
}
|
|
16010
|
+
// A transfer status as a response (the native server's table: anything else is a 500).
|
|
16011
|
+
function __sbR2TransferFailed(status, text) {
|
|
16012
|
+
const known = status === 404 || status === 410 || status === 413 || status === 422 || status === 503 || status === 507;
|
|
16013
|
+
return __sbR2TransferError(known ? status : 500, text);
|
|
16014
|
+
}
|
|
16015
|
+
// Resolves when the streamed request body `body` has more (or ended or failed): the host fires the parked timer.
|
|
16016
|
+
function __sbR2BodyWait(body) {
|
|
16017
|
+
return new Promise((resolve) => {
|
|
16018
|
+
const timer = setTimeout(resolve, 2147483647);
|
|
16019
|
+
__sbReqBodyOp("w", body, timer);
|
|
16020
|
+
});
|
|
16021
|
+
}
|
|
16022
|
+
async function __sbR2TransferUpload(request, bucket, token, body) {
|
|
16023
|
+
let declared = 0;
|
|
16024
|
+
const length = request.headers.get("content-length");
|
|
16025
|
+
if (length !== null && length !== "") {
|
|
16026
|
+
if (!/^[0-9]+$/.test(length) || length.length > 15) return __sbR2TransferError(400, "invalid content-length");
|
|
16027
|
+
declared = Number(length);
|
|
16028
|
+
}
|
|
16029
|
+
const opened = __sbR2XferOp("u", bucket, token, declared).split(" ");
|
|
16030
|
+
if (opened[0] !== "0") return __sbR2TransferFailed(Number(opened[0]), "R2 transfer unavailable");
|
|
16031
|
+
const handle = opened[1];
|
|
16032
|
+
let status = 0;
|
|
16033
|
+
if (body === 0) {
|
|
16034
|
+
// The host delivered the body whole (or there is none): one write.
|
|
16035
|
+
const raw = __sbRawBody(request);
|
|
16036
|
+
if (raw != null && raw !== "") status = Number(__sbR2XferOp("b", handle, raw, 0));
|
|
16037
|
+
} else {
|
|
16038
|
+
for (;;) {
|
|
16039
|
+
const step = __sbR2XferOp("s", handle, "", body).split(" ");
|
|
16040
|
+
const kind = step[0];
|
|
16041
|
+
if (kind === "d") continue;
|
|
16042
|
+
if (kind === "w") {
|
|
16043
|
+
await __sbR2BodyWait(body);
|
|
16044
|
+
continue;
|
|
16045
|
+
}
|
|
16046
|
+
if (kind === "e") break;
|
|
16047
|
+
// 'f': the client went away; 's': a status refused the bytes (the ticket is released with it).
|
|
16048
|
+
status = kind === "s" ? Number(step[1]) : 500;
|
|
16049
|
+
if (kind === "f") __sbR2XferOp("x", "0", "", Number(handle));
|
|
16050
|
+
__sbReqBodyOp("c", body, "");
|
|
16051
|
+
return kind === "f" ? __sbR2TransferError(500, "R2 transfer failed") : __sbR2TransferFailed(status, "R2 transfer failed");
|
|
16052
|
+
}
|
|
16053
|
+
__sbReqBodyOp("c", body, "");
|
|
16054
|
+
}
|
|
16055
|
+
if (status !== 0) return __sbR2TransferFailed(status, "R2 transfer failed");
|
|
16056
|
+
const finished = Number(__sbR2XferOp("f", handle, "", 0));
|
|
16057
|
+
if (finished === 201) return new Response(null, { status: 201 });
|
|
16058
|
+
return __sbR2TransferFailed(finished, "R2 transfer failed");
|
|
16059
|
+
}
|
|
16060
|
+
function __sbR2TransferDownload(request, bucket, token) {
|
|
16061
|
+
const opened = __sbR2XferOp("d", bucket, token, 0).split(" ");
|
|
16062
|
+
if (opened[0] !== "0") return __sbR2TransferFailed(Number(opened[0]), "R2 transfer unavailable");
|
|
16063
|
+
const handle = Number(opened[1]);
|
|
16064
|
+
const size = Number(opened[2]);
|
|
16065
|
+
const etag = opened[3];
|
|
16066
|
+
const close = () => __sbR2XferOp("c", "0", "", handle);
|
|
16067
|
+
const match = request.headers.get("if-none-match");
|
|
16068
|
+
if (match === "*" || match === etag || match === '"' + etag + '"') {
|
|
16069
|
+
close();
|
|
16070
|
+
return new Response(null, { status: 304, headers: { etag } });
|
|
16071
|
+
}
|
|
16072
|
+
let first = 0;
|
|
16073
|
+
let last = size ? size - 1 : 0;
|
|
16074
|
+
let partial = false;
|
|
16075
|
+
const range = request.headers.get("range");
|
|
16076
|
+
if (range) {
|
|
16077
|
+
const refuse = (why) => {
|
|
16078
|
+
close();
|
|
16079
|
+
return __sbR2TransferError(416, why);
|
|
16080
|
+
};
|
|
16081
|
+
if (range.slice(0, 6) !== "bytes=") return refuse("invalid range");
|
|
16082
|
+
const spec = range.slice(6);
|
|
16083
|
+
const dash = spec.indexOf("-");
|
|
16084
|
+
if (dash <= 0) return refuse("invalid range");
|
|
16085
|
+
const from = spec.slice(0, dash);
|
|
16086
|
+
const to = spec.slice(dash + 1);
|
|
16087
|
+
if (!/^[0-9]+$/.test(from) || (to !== "" && !/^[0-9]+$/.test(to))) return refuse("invalid range");
|
|
16088
|
+
first = Number(from);
|
|
16089
|
+
if (to !== "") last = Number(to);
|
|
16090
|
+
if (first >= size || last < first) return refuse("range outside object");
|
|
16091
|
+
if (last >= size) last = size - 1;
|
|
16092
|
+
partial = true;
|
|
16093
|
+
}
|
|
16094
|
+
const total = size === 0 ? 0 : last - first + 1;
|
|
16095
|
+
const headers = { "content-type": "application/octet-stream", etag, "accept-ranges": "bytes", "content-length": String(total) };
|
|
16096
|
+
if (partial) headers["content-range"] = "bytes " + first + "-" + last + "/" + size;
|
|
16097
|
+
let sent = 0;
|
|
16098
|
+
const stream = new ReadableStream({
|
|
16099
|
+
pull(controller) {
|
|
16100
|
+
if (sent >= total) {
|
|
16101
|
+
close();
|
|
16102
|
+
controller.close();
|
|
16103
|
+
return;
|
|
16104
|
+
}
|
|
16105
|
+
const chunk = new Uint8Array(Math.min(__SB_R2_CHUNK, total - sent));
|
|
16106
|
+
const got = Number(__sbR2XferOp("r", String(handle), "", first + sent, chunk));
|
|
16107
|
+
if (got !== chunk.length) {
|
|
16108
|
+
close();
|
|
16109
|
+
controller.error(new Error("R2 transfer failed"));
|
|
16110
|
+
return;
|
|
16111
|
+
}
|
|
16112
|
+
sent += got;
|
|
16113
|
+
controller.enqueue(chunk);
|
|
16114
|
+
},
|
|
16115
|
+
cancel() {
|
|
16116
|
+
close();
|
|
16117
|
+
},
|
|
16118
|
+
});
|
|
16119
|
+
return new Response(stream, { status: partial ? 206 : 200, headers });
|
|
16120
|
+
}
|
|
16121
|
+
// The response for a ticket request, or a Promise of it.
|
|
16122
|
+
function __sbR2Transfer(request, target) {
|
|
16123
|
+
// The streamed body's id, taken now: a ticket request never reaches __sbDecodeIngress.
|
|
16124
|
+
const body = __sbReqBodyTake();
|
|
16125
|
+
const method = request.method;
|
|
16126
|
+
if (method === "GET") return __sbR2TransferDownload(request, target[0], target[1]);
|
|
16127
|
+
if (method !== "PUT") return __sbR2TransferError(405, "R2 transfer requires GET or PUT");
|
|
16128
|
+
return __sbR2TransferUpload(request, target[0], target[1], body);
|
|
16129
|
+
}
|
|
16130
|
+
|
|
15894
16131
|
globalThis.__sbEntry = function (handlers, request) {
|
|
16132
|
+
if (__sbR2TransferOn) {
|
|
16133
|
+
const ticket = __sbR2TransferTarget(request);
|
|
16134
|
+
// Through the same finish as the app's answer (promise, then a stream body is sent as it is read).
|
|
16135
|
+
if (ticket !== null) return __sbFetchWithDrain(__sbR2Transfer(request, ticket), [], false, false, true, null, null);
|
|
16136
|
+
}
|
|
15895
16137
|
__sbDecodeIngress(request);
|
|
15896
16138
|
const trigger = __sbHeader(request.headers, "x-sb-trigger");
|
|
15897
16139
|
if (!trigger) {
|
|
@@ -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(
|
|
@@ -1879,6 +1879,22 @@ static u32 sb_r2_get_c(const char* path, const char* bucket, const char* key, co
|
|
|
1879
1879
|
return out;
|
|
1880
1880
|
}
|
|
1881
1881
|
|
|
1882
|
+
// Reads at most CAP bytes at OFFSET of an object's file into OUT, and nothing else of it: a range read
|
|
1883
|
+
// (R2 get with a range) and each chunk of a streamed body come through here, so an object of any size is
|
|
1884
|
+
// never in memory whole. Returns the byte count (short at the end of the object), or -1 when the file
|
|
1885
|
+
// cannot be read. The file is named by the object's generation, which a put never rewrites.
|
|
1886
|
+
static long sb_r2_read_into_c(const char* path, const char* bucket, const char* key, const char* generation, size_t offset, char* out, size_t cap) {
|
|
1887
|
+
char blobpath[1024];
|
|
1888
|
+
sb_r2_blob_path(blobpath, sizeof(blobpath), path, bucket, key, generation);
|
|
1889
|
+
FILE* f = fopen(blobpath, "rb");
|
|
1890
|
+
if (!f) return -1;
|
|
1891
|
+
if (offset && fseek(f, (long)offset, SEEK_SET) != 0) { fclose(f); return -1; }
|
|
1892
|
+
size_t rd = cap ? fread(out, 1, cap, f) : 0;
|
|
1893
|
+
int bad = ferror(f);
|
|
1894
|
+
fclose(f);
|
|
1895
|
+
return bad ? -1 : (long)rd;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1882
1898
|
// Best-effort: a metadata row deleted with no blob file (or vice versa) is
|
|
1883
1899
|
// still a correct end state, so a missing file here is not an error.
|
|
1884
1900
|
static void sb_r2_delete_blob(const char* path, const char* bucket, const char* key, const char* generation) {
|
|
@@ -2119,6 +2135,95 @@ size_t sb_r2_transfer_download_read(sb_r2_transfer_ctx* ctx, size_t offset, char
|
|
|
2119
2135
|
}
|
|
2120
2136
|
void sb_r2_transfer_download_close(sb_r2_transfer_ctx* ctx) { if (ctx) { if (ctx->file) fclose(ctx->file); free(ctx); } }
|
|
2121
2137
|
|
|
2138
|
+
// A host whose workers serve transfer tickets themselves (sproutd: the frontend forwards /__sb/r2/transfer/...
|
|
2139
|
+
// like any request, with its body streamed) drives the same context from JavaScript, one step at a time.
|
|
2140
|
+
// The contexts live here, in a small table (a handle is its index plus one), so JavaScript never holds a
|
|
2141
|
+
// pointer; a handle is released by finishing, aborting or closing it. An upload step moves what the request
|
|
2142
|
+
// body has buffered (at most 1 MiB, in 64 KiB pieces from the stack) into the object's temporary file, and a
|
|
2143
|
+
// download read fills a Uint8Array from the object's file: object bytes are never strings or JSON.
|
|
2144
|
+
#define SB_XFER_MAX 16
|
|
2145
|
+
static sb_r2_transfer_ctx* sb_xfer[SB_XFER_MAX];
|
|
2146
|
+
static int sb_xfer_hold(sb_r2_transfer_ctx* ctx) {
|
|
2147
|
+
for (int i = 0; i < SB_XFER_MAX; i++) if (!sb_xfer[i]) { sb_xfer[i] = ctx; return i + 1; }
|
|
2148
|
+
return 0;
|
|
2149
|
+
}
|
|
2150
|
+
static sb_r2_transfer_ctx* sb_xfer_get(int handle) {
|
|
2151
|
+
return handle >= 1 && handle <= SB_XFER_MAX ? sb_xfer[handle - 1] : 0;
|
|
2152
|
+
}
|
|
2153
|
+
extern long long sb_reqbody_poll(unsigned int);
|
|
2154
|
+
extern size_t sb_reqbody_take(unsigned int, char*, size_t);
|
|
2155
|
+
// Claims an upload ticket: the HTTP status (0: claimed, HANDLE set).
|
|
2156
|
+
static int sb_xfer_upload_open(const char* bucket, const char* token, size_t declared, int* handle) {
|
|
2157
|
+
sb_r2_transfer_ctx* ctx = 0;
|
|
2158
|
+
*handle = 0;
|
|
2159
|
+
int status = sb_r2_transfer_open(bucket, token, declared, &ctx);
|
|
2160
|
+
if (status != 0 || !ctx) return status ? status : 500;
|
|
2161
|
+
*handle = sb_xfer_hold(ctx);
|
|
2162
|
+
if (!*handle) { sb_r2_transfer_abort(ctx); return 503; }
|
|
2163
|
+
return 0;
|
|
2164
|
+
}
|
|
2165
|
+
// One step of an upload from request body BODY: 'd' some bytes moved (*arg), 'w' none yet (wait for the
|
|
2166
|
+
// body's timer), 'e' the body ended (finish), 'f' the body failed (the client went away, abort), 's' a
|
|
2167
|
+
// status refused them (*arg; the ticket is released). A step stops after 1 MiB so other requests run.
|
|
2168
|
+
static char sb_xfer_upload_step(int handle, unsigned int body, long* arg) {
|
|
2169
|
+
sb_r2_transfer_ctx* ctx = sb_xfer_get(handle);
|
|
2170
|
+
*arg = 0;
|
|
2171
|
+
if (!ctx) { *arg = 500; return 's'; }
|
|
2172
|
+
char piece[65536];
|
|
2173
|
+
long moved = 0;
|
|
2174
|
+
while (moved < (1 << 20)) {
|
|
2175
|
+
long long ready = sb_reqbody_poll(body);
|
|
2176
|
+
if (ready == -1) { *arg = moved; return moved ? 'd' : 'e'; }
|
|
2177
|
+
if (ready == -2) { *arg = moved; return moved ? 'd' : 'f'; }
|
|
2178
|
+
if (ready == 0) { *arg = moved; return moved ? 'd' : 'w'; }
|
|
2179
|
+
size_t n = sb_reqbody_take(body, piece, sizeof(piece));
|
|
2180
|
+
if (!n) { *arg = moved; return moved ? 'd' : 'w'; }
|
|
2181
|
+
int status = sb_r2_transfer_write(ctx, piece, n);
|
|
2182
|
+
if (status != 0) { sb_r2_transfer_abort(ctx); sb_xfer[handle - 1] = 0; *arg = status; return 's'; }
|
|
2183
|
+
moved += (long)n;
|
|
2184
|
+
}
|
|
2185
|
+
*arg = moved;
|
|
2186
|
+
return 'd';
|
|
2187
|
+
}
|
|
2188
|
+
// A body that arrived whole (a request of a host that buffers it): written in one go.
|
|
2189
|
+
static int sb_xfer_upload_write(int handle, const char* bytes, size_t length) {
|
|
2190
|
+
sb_r2_transfer_ctx* ctx = sb_xfer_get(handle);
|
|
2191
|
+
if (!ctx) return 500;
|
|
2192
|
+
int status = sb_r2_transfer_write(ctx, bytes, length);
|
|
2193
|
+
if (status != 0) { sb_r2_transfer_abort(ctx); sb_xfer[handle - 1] = 0; }
|
|
2194
|
+
return status;
|
|
2195
|
+
}
|
|
2196
|
+
static int sb_xfer_finish(int handle) {
|
|
2197
|
+
sb_r2_transfer_ctx* ctx = sb_xfer_get(handle);
|
|
2198
|
+
if (!ctx) return 500;
|
|
2199
|
+
sb_xfer[handle - 1] = 0;
|
|
2200
|
+
return sb_r2_transfer_finish(ctx);
|
|
2201
|
+
}
|
|
2202
|
+
static void sb_xfer_close(int handle) {
|
|
2203
|
+
sb_r2_transfer_ctx* ctx = sb_xfer_get(handle);
|
|
2204
|
+
if (!ctx) return;
|
|
2205
|
+
sb_xfer[handle - 1] = 0;
|
|
2206
|
+
sb_r2_transfer_abort(ctx);
|
|
2207
|
+
}
|
|
2208
|
+
// Claims a download ticket: the HTTP status (0: claimed, HANDLE, *size and *etag set).
|
|
2209
|
+
static int sb_xfer_download_open(const char* bucket, const char* token, int* handle, size_t* size, char* etag, size_t etag_cap) {
|
|
2210
|
+
sb_r2_transfer_ctx* ctx = 0;
|
|
2211
|
+
*handle = 0;
|
|
2212
|
+
int status = sb_r2_transfer_download_open(bucket, token, &ctx);
|
|
2213
|
+
if (status != 0 || !ctx) return status ? status : 500;
|
|
2214
|
+
*handle = sb_xfer_hold(ctx);
|
|
2215
|
+
if (!*handle) { sb_r2_transfer_download_close(ctx); return 503; }
|
|
2216
|
+
*size = sb_r2_transfer_download_size(ctx);
|
|
2217
|
+
snprintf(etag, etag_cap, "%s", sb_r2_transfer_download_etag(ctx));
|
|
2218
|
+
return 0;
|
|
2219
|
+
}
|
|
2220
|
+
static void sb_xfer_download_close(int handle) {
|
|
2221
|
+
sb_r2_transfer_ctx* ctx = sb_xfer_get(handle);
|
|
2222
|
+
if (!ctx) return;
|
|
2223
|
+
sb_xfer[handle - 1] = 0;
|
|
2224
|
+
sb_r2_transfer_download_close(ctx);
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2122
2227
|
// LTX capture (#141) failed after SQLite committed: the write is in the database but not in
|
|
2123
2228
|
// the replication log, and the database refuses writes until the process restarts. Said as
|
|
2124
2229
|
// such (SQLITE_IOERR), never as SQLite's own message, which still reads "not an error".
|
|
@@ -4342,6 +4447,112 @@ function __sbR2GetRaw(path, bucket, key, blobId) {
|
|
|
4342
4447
|
return res;
|
|
4343
4448
|
}
|
|
4344
4449
|
|
|
4450
|
+
// R2 get without the whole object: fills VIEW (a fresh Uint8Array) with the bytes at OFFSET of the object's
|
|
4451
|
+
// file. Returns how many it read (fewer at the end of the object), or -1 when the file cannot be read.
|
|
4452
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
4453
|
+
function __sbR2ReadInto(path, bucket, key, blobId, offset, view) {
|
|
4454
|
+
let got = -1;
|
|
4455
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
4456
|
+
Porffor.c`
|
|
4457
|
+
const char* __p; size_t __pl; char* __po = 0;
|
|
4458
|
+
porf_native_fetch_read_value(path, &__p, &__pl, &__po);
|
|
4459
|
+
char* __path = (char*)malloc(__pl + 1); memcpy(__path, __p, __pl); __path[__pl] = 0;
|
|
4460
|
+
if (__po) free(__po);
|
|
4461
|
+
|
|
4462
|
+
const char* __bk; size_t __bkl; char* __bko = 0;
|
|
4463
|
+
porf_native_fetch_read_value(bucket, &__bk, &__bkl, &__bko);
|
|
4464
|
+
char* __bucket = (char*)malloc(__bkl + 1); memcpy(__bucket, __bk, __bkl); __bucket[__bkl] = 0;
|
|
4465
|
+
if (__bko) free(__bko);
|
|
4466
|
+
|
|
4467
|
+
const char* __k; size_t __kl; char* __ko = 0;
|
|
4468
|
+
porf_native_fetch_read_value(key, &__k, &__kl, &__ko);
|
|
4469
|
+
char* __key = (char*)malloc(__kl + 1); memcpy(__key, __k, __kl); __key[__kl] = 0;
|
|
4470
|
+
if (__ko) free(__ko);
|
|
4471
|
+
|
|
4472
|
+
const char* __g; size_t __gl; char* __go = 0;
|
|
4473
|
+
porf_native_fetch_read_value(blobId, &__g, &__gl, &__go);
|
|
4474
|
+
char* __generation = (char*)malloc(__gl + 1); memcpy(__generation, __g, __gl); __generation[__gl] = 0;
|
|
4475
|
+
if (__go) free(__go);
|
|
4476
|
+
|
|
4477
|
+
long __n = -1;
|
|
4478
|
+
if (view.type == 81 && offset.type == 1 && offset.val >= 0) {
|
|
4479
|
+
const u32 __ta = (u32)view.val;
|
|
4480
|
+
const int32_t __len = *(int32_t*)(MEM + __ta);
|
|
4481
|
+
const u32 __at = *(u32*)(MEM + __ta + 4) + 4;
|
|
4482
|
+
if (__len >= 0) __n = sb_r2_read_into_c(__path, __bucket, __key, __generation, (size_t)offset.val, (char*)(MEM + __at), (size_t)__len);
|
|
4483
|
+
}
|
|
4484
|
+
free(__path); free(__bucket); free(__key); free(__generation);
|
|
4485
|
+
got = (f64)__n;
|
|
4486
|
+
`;
|
|
4487
|
+
return got;
|
|
4488
|
+
}
|
|
4489
|
+
|
|
4490
|
+
// Transfer tickets served by the worker (see sb_xfer_* above). op: "u" claim an upload ticket (a = bucket, b = token,
|
|
4491
|
+
// n = the declared length; "0 HANDLE" or "STATUS"), "s" a step of the upload from body id n ("d BYTES", "w", "e", "f"
|
|
4492
|
+
// or "s STATUS"), "b" write the bytestring in a (a buffered body; the status), "f" finish ("STATUS"), "x" release the
|
|
4493
|
+
// handle in n, "d" claim a download ticket ("0 HANDLE SIZE ETAG" or "STATUS"), "r" read the object's file at offset n
|
|
4494
|
+
// into the Uint8Array in view (handle in a; the count), "c" release a download handle. Returns a short string.
|
|
4495
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
4496
|
+
function __sbR2XferOp(op, a, b, n, view) {
|
|
4497
|
+
let out = "";
|
|
4498
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
4499
|
+
Porffor.c`
|
|
4500
|
+
const char* __o; size_t __ol; char* __oo = 0;
|
|
4501
|
+
porf_native_fetch_read_value(op, &__o, &__ol, &__oo);
|
|
4502
|
+
char __op = __ol ? __o[0] : 0;
|
|
4503
|
+
if (__oo) free(__oo);
|
|
4504
|
+
|
|
4505
|
+
const char* __a; size_t __al; char* __ao = 0;
|
|
4506
|
+
porf_native_fetch_read_value(a, &__a, &__al, &__ao);
|
|
4507
|
+
char* __as = (char*)malloc(__al + 1); memcpy(__as, __a, __al); __as[__al] = 0;
|
|
4508
|
+
if (__ao) free(__ao);
|
|
4509
|
+
const char* __b; size_t __bl; char* __bo = 0;
|
|
4510
|
+
porf_native_fetch_read_value(b, &__b, &__bl, &__bo);
|
|
4511
|
+
char* __bs = (char*)malloc(__bl + 1); memcpy(__bs, __b, __bl); __bs[__bl] = 0;
|
|
4512
|
+
if (__bo) free(__bo);
|
|
4513
|
+
|
|
4514
|
+
const double __n = n.type == 1 ? n.val : 0;
|
|
4515
|
+
const int __handle = (int)strtol(__as, 0, 10);
|
|
4516
|
+
char __r[160]; __r[0] = 0;
|
|
4517
|
+
if (__op == 'u') {
|
|
4518
|
+
int __h = 0;
|
|
4519
|
+
int __s = sb_xfer_upload_open(__as, __bs, (size_t)__n, &__h);
|
|
4520
|
+
if (__s == 0) snprintf(__r, sizeof(__r), "0 %d", __h); else snprintf(__r, sizeof(__r), "%d", __s);
|
|
4521
|
+
} else if (__op == 's') {
|
|
4522
|
+
long __moved = 0;
|
|
4523
|
+
char __k = sb_xfer_upload_step(__handle, (unsigned int)__n, &__moved);
|
|
4524
|
+
snprintf(__r, sizeof(__r), "%c %ld", __k, __moved);
|
|
4525
|
+
} else if (__op == 'b') {
|
|
4526
|
+
const char* __d = 0; size_t __dl = 0;
|
|
4527
|
+
int __s = 500;
|
|
4528
|
+
if (porf_native_fetch_read_raw_bytes(b, &__d, &__dl) == 0) __s = sb_xfer_upload_write(__handle, __d, __dl);
|
|
4529
|
+
snprintf(__r, sizeof(__r), "%d", __s);
|
|
4530
|
+
} else if (__op == 'f') snprintf(__r, sizeof(__r), "%d", sb_xfer_finish(__handle));
|
|
4531
|
+
else if (__op == 'x') sb_xfer_close((int)__n);
|
|
4532
|
+
else if (__op == 'd') {
|
|
4533
|
+
int __h = 0; size_t __size = 0; char __etag[80]; __etag[0] = 0;
|
|
4534
|
+
int __s = sb_xfer_download_open(__as, __bs, &__h, &__size, __etag, sizeof(__etag));
|
|
4535
|
+
if (__s == 0) snprintf(__r, sizeof(__r), "0 %d %zu %s", __h, __size, __etag); else snprintf(__r, sizeof(__r), "%d", __s);
|
|
4536
|
+
} else if (__op == 'r') {
|
|
4537
|
+
long __got = -1;
|
|
4538
|
+
sb_r2_transfer_ctx* __ctx = sb_xfer_get(__handle);
|
|
4539
|
+
if (__ctx && view.type == 81) {
|
|
4540
|
+
const u32 __ta = (u32)view.val;
|
|
4541
|
+
const int32_t __len = *(int32_t*)(MEM + __ta);
|
|
4542
|
+
const u32 __at = *(u32*)(MEM + __ta + 4) + 4;
|
|
4543
|
+
if (__len >= 0) __got = (long)sb_r2_transfer_download_read(__ctx, (size_t)__n, (char*)(MEM + __at), (size_t)__len);
|
|
4544
|
+
}
|
|
4545
|
+
snprintf(__r, sizeof(__r), "%ld", __got);
|
|
4546
|
+
} else if (__op == 'c') sb_xfer_download_close((int)__n);
|
|
4547
|
+
free(__as); free(__bs);
|
|
4548
|
+
out = porf_box((f64)porf_native_fetch_alloc_bytestring(__r, strlen(__r)), 195);
|
|
4549
|
+
`;
|
|
4550
|
+
return out;
|
|
4551
|
+
}
|
|
4552
|
+
// This transport's workers serve transfer tickets themselves, on a host that forwards the path (sproutd); a
|
|
4553
|
+
// standalone executable's native server answers them before JavaScript runs, and never gets here.
|
|
4554
|
+
__sbR2TransferOn = true;
|
|
4555
|
+
|
|
4345
4556
|
// R2 delete: also removes the object's own file, not just the metadata row
|
|
4346
4557
|
// (#56 — sb_r2_delete_blob is best-effort, a missing file is not an error).
|
|
4347
4558
|
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
@@ -5955,6 +6166,19 @@ globalThis.__sbR2Get = function (bucket, key) {
|
|
|
5955
6166
|
};
|
|
5956
6167
|
};
|
|
5957
6168
|
|
|
6169
|
+
// The object's metadata and where its bytes are, without the bytes: get() reads them as the caller asks for them
|
|
6170
|
+
// (range windows and a body stream), through __sbR2ReadRange.
|
|
6171
|
+
globalThis.__sbR2Open = function (bucket, key) {
|
|
6172
|
+
const meta = __sbEmbeddedDispatch({ op: "r2.head", bucket, key });
|
|
6173
|
+
if (!meta.found) return { found: false };
|
|
6174
|
+
return { found: true, object: meta.object, blobId: String(meta.blobId || "") };
|
|
6175
|
+
};
|
|
6176
|
+
|
|
6177
|
+
// Fills the Uint8Array VIEW with the object's bytes from OFFSET; the count read, or -1.
|
|
6178
|
+
globalThis.__sbR2ReadRange = function (bucket, key, blobId, offset, view) {
|
|
6179
|
+
return __sbR2ReadInto(__sbStore(), String(bucket), String(key), String(blobId), offset, view);
|
|
6180
|
+
};
|
|
6181
|
+
|
|
5958
6182
|
globalThis.__sbR2MultipartPut = function (bucket, key, uploadId, partNumber, body) {
|
|
5959
6183
|
__sbQuotaCheck("R2 uploadPart()");
|
|
5960
6184
|
__sbEnsureSchema();
|