@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/runtime",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
- obj.text = async () => __sbFromUtf8(take());
11044
- obj.json = async () => JSON.parse(__sbFromUtf8(take()));
11045
- obj.arrayBuffer = async () => __sbBufFrom(take());
11046
- obj.blob = async () => new Blob([__sbBufFrom(take())], { type: obj.httpMetadata.contentType || "" });
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 r = __sbR2Get(name, __sbR2Key(key, "get"));
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, "e" end. Returns a short string.
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 == 'e') { sb_stream_end(__id); snprintf(__r, sizeof(__r), "0"); }
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 && __sbStreamOp("w", id, __sbToBytes(bytes)) !== "0") {
14152
- // The client went away: stop the source.
14153
- reader.cancel();
14154
- break;
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
+ });
@@ -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();