@sproutboat/toolchain 0.4.4 → 0.4.6

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,17 @@
1
1
  # @sproutboat/toolchain
2
2
 
3
+ ## 0.4.6
4
+
5
+ ### Patch Changes
6
+
7
+ - a357b91: Compile direct R2 transfer support only into artifacts that declare an R2 binding.
8
+
9
+ ## 0.4.5
10
+
11
+ ### Patch Changes
12
+
13
+ - 5ee065c: Stream standalone direct R2 byte ranges safely and support ETag conditionals.
14
+
3
15
  ## 0.4.4
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/toolchain",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -28,7 +28,9 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
28
28
  " }\n" +
29
29
  " return -1;\n" +
30
30
  "}\n",
31
- "compiler/index.js": " '-xc', '-', '-c',\n uSocketsArchive,\n '-lm'\n",
31
+ "compiler/index.js":
32
+ " '-xc', '-', '-c',\n uSocketsArchive,\n '-lm'\n" +
33
+ " '-I', `${uwsDir}/uSockets/src`,\n",
32
34
  "compiler/uwebsockets.js":
33
35
  "static const size_t REQUEST_BODY_MAX_BYTES = 1024u * 1024u;\n" +
34
36
  "int porf_native_fetch_read_value(struct jsval value, const char** out_buf, size_t* out_len, char** out_owned);\n" +
package/src/patch.test.ts CHANGED
@@ -2,7 +2,8 @@ import { expect, test } from "bun:test";
2
2
  import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { patchRenderJs, patchUwebsockets } from "./patch";
5
+ import { EMPTY_BINDINGS, wrapNativeFetchHandler } from "../../runtime/src/wrap";
6
+ import { patchCompilerArgs, patchRenderJs, patchUwebsockets } from "./patch";
6
7
 
7
8
  // The parts of Porffor's compiler/render.js the patch anchors to.
8
9
  const RENDER = `void porf_native_fetch_runtime_init(void) {
@@ -36,6 +37,16 @@ int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_
36
37
  }
37
38
  `;
38
39
 
40
+ const COMPILER_INDEX = `const compileArgs = [
41
+ '-xc', '-', '-c',
42
+ ];
43
+ const linkArgs = [
44
+ '-I', \`${"${uwsDir}"}/uSockets/src\`,
45
+ uSocketsArchive,
46
+ '-lm'
47
+ ];
48
+ `;
49
+
39
50
  // Porffor's compiler/uwebsockets.js, trimmed to the parts the patch touches:
40
51
  // body limit (#56), the #156 status-line switch, #163's collect_headers, and
41
52
  // #176's read_value forward decl / is_forbidden_response_header /
@@ -171,6 +182,8 @@ test("#156: status-line fallback synthesizes a line for unlisted codes, idempote
171
182
  expect(once).toContain("try_handle_r2_transfer(res, req, method)");
172
183
  expect(once).toContain("sb_r2_transfer_download_open");
173
184
  expect(once).toContain("sb_r2_transfer_download_v1");
185
+ expect(once).toContain("sb_r2_transfer_range_v1");
186
+ expect(once).toContain("sb_r2_transfer_conditional_v1");
174
187
 
175
188
  // #163: collect_headers takes res, drops a client-sent x-sb-remote-addr, and
176
189
  // appends the real peer; the call site passes res through.
@@ -228,6 +241,86 @@ test("a drifted shim fails loudly instead of silently no-op'ing", async () => {
228
241
  }
229
242
  });
230
243
 
244
+ test("native capability flags reach both the generated C and socket-shim C++ compiles", async () => {
245
+ const root = await mkdtemp(join(tmpdir(), "sb-patch-compiler-"));
246
+ try {
247
+ await mkdir(join(root, "compiler"), { recursive: true });
248
+ await writeFile(join(root, "compiler/index.js"), COMPILER_INDEX);
249
+ await patchCompilerArgs(root);
250
+ const once = await readFile(join(root, "compiler/index.js"), "utf8");
251
+ expect(once.match(/SB_EXTRA_CFLAGS/g)).toHaveLength(4);
252
+ expect(once).toContain("SB_UWS_EXTRA_CFLAGS");
253
+ await patchCompilerArgs(root);
254
+ expect(await readFile(join(root, "compiler/index.js"), "utf8")).toBe(once);
255
+ } finally {
256
+ await rm(root, { recursive: true, force: true });
257
+ }
258
+ });
259
+
260
+ test("R2 builds define every direct transfer symbol the gated socket shim calls", async () => {
261
+ const root = await shimDir();
262
+ try {
263
+ await patchUwebsockets(root);
264
+ const shim = await readFile(join(root, "compiler/uwebsockets.js"), "utf8");
265
+ const abi = shim.match(/extern "C" \{([\s\S]*?)\n\}/)?.[1];
266
+ if (abi === undefined) throw new Error("patched shim did not contain the R2 transfer ABI");
267
+ const required = [...abi.matchAll(/\b(sb_r2_transfer_[a-z_]+)\s*\(/g)].map((match) => match[1]);
268
+ const wrapped = wrapNativeFetchHandler(
269
+ "export default { fetch() { return new Response('ok'); } }",
270
+ "",
271
+ {},
272
+ { ...EMPTY_BINDINGS, r2: ["OBJECTS"] },
273
+ 8080,
274
+ "2026-08-26",
275
+ "test",
276
+ undefined,
277
+ "broker",
278
+ );
279
+
280
+ for (const symbol of new Set(required)) {
281
+ expect(wrapped).toMatch(new RegExp(`\\b${symbol}\\s*\\([^)]*\\)\\s*\\{`));
282
+ }
283
+ } finally {
284
+ await rm(root, { recursive: true, force: true });
285
+ }
286
+ });
287
+
288
+ test("the direct R2 socket path is excluded unless the compiler enables it", async () => {
289
+ const root = await shimDir();
290
+ try {
291
+ await patchUwebsockets(root);
292
+ const shim = await readFile(join(root, "compiler/uwebsockets.js"), "utf8");
293
+ expect(shim).toContain("#ifdef SB_R2_TRANSFER");
294
+ expect(shim).toContain("#endif\n __porffor_js_enter();");
295
+ } finally {
296
+ await rm(root, { recursive: true, force: true });
297
+ }
298
+ });
299
+
300
+ test("an existing compiler cache upgrades to the gated R2 socket path", async () => {
301
+ const root = await shimDir();
302
+ try {
303
+ await patchUwebsockets(root);
304
+ const file = join(root, "compiler/uwebsockets.js");
305
+ const current = await readFile(file, "utf8");
306
+ const legacy = current
307
+ .replace('#ifdef SB_R2_TRANSFER\nextern "C" {', 'extern "C" {')
308
+ .replace("\n#endif\nstatic void on_request", "\nstatic void on_request")
309
+ .replace(
310
+ "#ifdef SB_R2_TRANSFER\n if (try_handle_r2_transfer(res, req, method)) return;\n#endif\n",
311
+ " if (try_handle_r2_transfer(res, req, method)) return;\n",
312
+ );
313
+ await writeFile(file, legacy);
314
+
315
+ await patchUwebsockets(root);
316
+ const upgraded = await readFile(file, "utf8");
317
+ expect(upgraded).toContain('#ifdef SB_R2_TRANSFER\nextern "C" {');
318
+ expect(upgraded).toContain("#endif\n __porffor_js_enter();");
319
+ } finally {
320
+ await rm(root, { recursive: true, force: true });
321
+ }
322
+ });
323
+
231
324
  test("#165: render.js routes console output to stderr, unbuffered, idempotently", async () => {
232
325
  const root = await mkdtemp(join(tmpdir(), "sb-patch-render-"));
233
326
  try {
package/src/patch.ts CHANGED
@@ -167,6 +167,14 @@ const CFLAGS_INJECT =
167
167
  " ...(process.env.SB_EXTRA_CFLAGS ? process.env.SB_EXTRA_CFLAGS.split(' ').filter(Boolean) : []),\n";
168
168
  const CFLAGS_MARKER = "SB_EXTRA_CFLAGS";
169
169
 
170
+ /** The native-fetch socket shim is a separately compiled C++ translation unit.
171
+ * It needs the same optional capability flags as the generated C object. */
172
+ const UWS_CFLAGS_ANCHOR = " '-I', `${uwsDir}/uSockets/src`,\n";
173
+ const UWS_CFLAGS_INJECT =
174
+ " // SB_UWS_EXTRA_CFLAGS\n" +
175
+ " ...(process.env.SB_EXTRA_CFLAGS ? process.env.SB_EXTRA_CFLAGS.split(' ').filter(Boolean) : []),\n";
176
+ const UWS_CFLAGS_MARKER = "SB_UWS_EXTRA_CFLAGS";
177
+
170
178
  /**
171
179
  * #56 — make the inbound request-body limit configurable.
172
180
  *
@@ -416,8 +424,10 @@ const WRITE_RESPONSE_MARKER = "porf_native_fetch_read_raw_bytes(body_value";
416
424
  // #195: reserve a direct-transfer ticket before native-fetch allocates the
417
425
  // ordinary in-memory PendingRequest. uWS then gives each wire chunk directly
418
426
  // to the runtime C ABI, which owns the temporary object file.
419
- const R2_TRANSFER_ANCHOR = "static void on_request(uWS::HttpResponse<false>* res, uWS::HttpRequest* req) {\n";
420
- const R2_TRANSFER_INJECT = `extern "C" {
427
+ const R2_TRANSFER_ANCHOR =
428
+ "static void on_request(uWS::HttpResponse<false>* res, uWS::HttpRequest* req) {\n";
429
+ const R2_TRANSFER_INJECT = `#ifdef SB_R2_TRANSFER
430
+ extern "C" {
421
431
  struct sb_r2_transfer_ctx;
422
432
  int sb_r2_transfer_open(const char*, const char*, size_t, sb_r2_transfer_ctx**);
423
433
  int sb_r2_transfer_write(sb_r2_transfer_ctx*, const char*, size_t);
@@ -425,6 +435,7 @@ const R2_TRANSFER_INJECT = `extern "C" {
425
435
  void sb_r2_transfer_abort(sb_r2_transfer_ctx*);
426
436
  int sb_r2_transfer_download_open(const char*, const char*, sb_r2_transfer_ctx**);
427
437
  size_t sb_r2_transfer_download_size(sb_r2_transfer_ctx*);
438
+ const char* sb_r2_transfer_download_etag(sb_r2_transfer_ctx*);
428
439
  size_t sb_r2_transfer_download_read(sb_r2_transfer_ctx*, size_t, char*, size_t);
429
440
  void sb_r2_transfer_download_close(sb_r2_transfer_ctx*);
430
441
  }
@@ -452,22 +463,58 @@ static bool try_handle_r2_transfer(uWS::HttpResponse<false>* res, uWS::HttpReque
452
463
  std::string bucket, token;
453
464
  if (!sb_r2_transfer_path(req->getUrl(), &bucket, &token)) return false;
454
465
  // sb_r2_transfer_download_v1
466
+ // sb_r2_transfer_range_v1
467
+ // sb_r2_transfer_range_header_v2
468
+ // sb_r2_transfer_conditional_v1
469
+ // sb_r2_transfer_streaming_v2
455
470
  if (method == "GET") {
456
471
  sb_r2_transfer_ctx* download = nullptr;
457
472
  const int open_status = sb_r2_transfer_download_open(bucket.c_str(), token.c_str(), &download);
458
473
  if (open_status != 0 || !download) { respond_with_error(res, sb_r2_transfer_status(open_status), "R2 transfer unavailable", true); return true; }
459
- struct Download { sb_r2_transfer_ctx* ctx; uintmax_t size; bool closed = false; };
460
- auto state = std::make_shared<Download>(Download{download, sb_r2_transfer_download_size(download)});
474
+ const std::string etag = sb_r2_transfer_download_etag(download);
475
+ const std::string_view if_none_match = req->getHeader("if-none-match");
476
+ if (if_none_match == "*" || if_none_match == etag || (if_none_match.size() == etag.size() + 2 && if_none_match.front() == '"' && if_none_match.back() == '"' && if_none_match.substr(1, etag.size()) == etag)) {
477
+ sb_r2_transfer_download_close(download); res->writeStatus("304 Not Modified"); res->writeHeader("ETag", etag); res->end(); return true;
478
+ }
479
+ const uintmax_t full_size = sb_r2_transfer_download_size(download);
480
+ uintmax_t first = 0, last = full_size ? full_size - 1 : 0;
481
+ std::string_view range = req->getHeader("range"); if (range.empty()) range = req->getHeader("Range");
482
+ if (!range.empty()) {
483
+ if (range.substr(0, 6) != "bytes=") { sb_r2_transfer_download_close(download); respond_with_error(res, "416 Range Not Satisfiable", "invalid range", true); return true; }
484
+ const std::string_view spec = range.substr(6); const size_t dash = spec.find('-');
485
+ if (dash == std::string_view::npos || dash == 0) { sb_r2_transfer_download_close(download); respond_with_error(res, "416 Range Not Satisfiable", "invalid range", true); return true; }
486
+ first = 0; for (char c : spec.substr(0, dash)) { if (c < '0' || c > '9') { sb_r2_transfer_download_close(download); respond_with_error(res, "416 Range Not Satisfiable", "invalid range", true); return true; } first = first * 10 + (c - '0'); }
487
+ if (dash + 1 < spec.size()) { last = 0; for (char c : spec.substr(dash + 1)) { if (c < '0' || c > '9') { sb_r2_transfer_download_close(download); respond_with_error(res, "416 Range Not Satisfiable", "invalid range", true); return true; } last = last * 10 + (c - '0'); } }
488
+ if (first >= full_size || last < first) { sb_r2_transfer_download_close(download); respond_with_error(res, "416 Range Not Satisfiable", "range outside object", true); return true; }
489
+ if (last >= full_size) last = full_size - 1;
490
+ char content_range[96]; snprintf(content_range, sizeof(content_range), "bytes %llu-%llu/%llu", (unsigned long long)first, (unsigned long long)last, (unsigned long long)full_size);
491
+ res->writeStatus("206 Partial Content"); res->writeHeader("Content-Range", content_range);
492
+ }
493
+ res->writeHeader("ETag", etag); res->writeHeader("Accept-Ranges", "bytes");
494
+ struct Download { sb_r2_transfer_ctx* ctx; uintmax_t base; uintmax_t size; uintmax_t sent = 0; bool closed = false; char chunk[65536]; };
495
+ auto state = std::make_shared<Download>(Download{download, first, last - first + 1});
461
496
  auto pump = std::make_shared<std::function<bool()>>();
462
497
  *pump = [res, state, pump]() {
463
498
  if (state->closed) return false;
464
- const uintmax_t offset = res->getWriteOffset();
465
- char chunk[65536];
466
- const size_t bytes = sb_r2_transfer_download_read(state->ctx, (size_t)offset, chunk, sizeof(chunk));
467
- const auto result = res->tryEnd(std::string_view(chunk, bytes), state->size);
468
- if (result.second) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return false; }
469
- if (!result.first) res->onWritable([pump](uintmax_t) { return (*pump)(); });
470
- return false;
499
+ for (;;) {
500
+ const size_t want = state->size - state->sent < sizeof(state->chunk) ? (size_t)(state->size - state->sent) : sizeof(state->chunk);
501
+ const size_t bytes = sb_r2_transfer_download_read(state->ctx, (size_t)(state->base + state->sent), state->chunk, want);
502
+ const auto result = res->tryEnd(std::string_view(state->chunk, bytes), state->size);
503
+ if (result.second || bytes == 0) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return false; }
504
+ if (!result.first) {
505
+ res->onWritable([res, state](uintmax_t offset) {
506
+ if (state->closed) return true;
507
+ const size_t want = state->size - offset < sizeof(state->chunk) ? (size_t)(state->size - offset) : sizeof(state->chunk);
508
+ const size_t bytes = sb_r2_transfer_download_read(state->ctx, (size_t)(state->base + offset), state->chunk, want);
509
+ const auto resumed = res->tryEnd(std::string_view(state->chunk, bytes), state->size);
510
+ if (resumed.second || bytes == 0) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return true; }
511
+ return resumed.first;
512
+ });
513
+ return false;
514
+ }
515
+ state->sent += bytes;
516
+ if (state->sent == state->size) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return false; }
517
+ }
471
518
  };
472
519
  res->writeHeader("Content-Type", "application/octet-stream");
473
520
  res->onAborted([state]() { if (!state->closed) { sb_r2_transfer_download_close(state->ctx); state->closed = true; } });
@@ -496,37 +543,84 @@ static bool try_handle_r2_transfer(uWS::HttpResponse<false>* res, uWS::HttpReque
496
543
  return true;
497
544
  }
498
545
  static void on_request(uWS::HttpResponse<false>* res, uWS::HttpRequest* req) {
546
+ #endif
499
547
  `;
500
548
  const R2_TRANSFER_MARKER = "sb_r2_transfer_path(std::string_view path";
501
- const R2_TRANSFER_CALL_ANCHOR = " __porffor_js_enter();\n const i32 url_ptr = alloc_request_url(req);";
549
+ const R2_TRANSFER_CALL_ANCHOR =
550
+ " __porffor_js_enter();\n const i32 url_ptr = alloc_request_url(req);";
502
551
  const R2_TRANSFER_CALL_INJECT =
503
- " if (try_handle_r2_transfer(res, req, method)) return;\n" + R2_TRANSFER_CALL_ANCHOR;
552
+ "#ifdef SB_R2_TRANSFER\n if (try_handle_r2_transfer(res, req, method)) return;\n#endif\n" +
553
+ R2_TRANSFER_CALL_ANCHOR;
504
554
  const R2_TRANSFER_CALL_MARKER = "try_handle_r2_transfer(res, req, method)";
555
+ const R2_GATE_MARKER = "#ifdef SB_R2_TRANSFER";
505
556
  const R2_DOWNLOAD_CACHE_MARKER = "sb_r2_transfer_download_v1";
557
+ const R2_RANGE_CACHE_MARKER = "sb_r2_transfer_range_v1";
558
+ const R2_CONDITIONAL_CACHE_MARKER = "sb_r2_transfer_conditional_v1";
559
+ const R2_STREAMING_CACHE_MARKER = "sb_r2_transfer_streaming_v2";
560
+ const R2_RANGE_HEADER_CACHE_MARKER = "sb_r2_transfer_range_header_v2";
506
561
  const R2_DOWNLOAD_DECL_ANCHOR = " void sb_r2_transfer_abort(sb_r2_transfer_ctx*);\n}";
507
562
  const R2_DOWNLOAD_DECL_INJECT =
508
563
  " void sb_r2_transfer_abort(sb_r2_transfer_ctx*);\n" +
509
564
  " int sb_r2_transfer_download_open(const char*, const char*, sb_r2_transfer_ctx**);\n" +
510
565
  " size_t sb_r2_transfer_download_size(sb_r2_transfer_ctx*);\n" +
566
+ " const char* sb_r2_transfer_download_etag(sb_r2_transfer_ctx*);\n" +
511
567
  " size_t sb_r2_transfer_download_read(sb_r2_transfer_ctx*, size_t, char*, size_t);\n" +
512
568
  " void sb_r2_transfer_download_close(sb_r2_transfer_ctx*);\n}";
513
- const R2_DOWNLOAD_BRANCH_ANCHOR = ' if (method != "PUT") { respond_with_error(res, "405 Method Not Allowed", "R2 transfer requires PUT", true); return true; }';
569
+ const R2_DOWNLOAD_BRANCH_ANCHOR =
570
+ ' if (method != "PUT") { respond_with_error(res, "405 Method Not Allowed", "R2 transfer requires PUT", true); return true; }';
514
571
  const R2_DOWNLOAD_BRANCH_INJECT = ` // sb_r2_transfer_download_v1
572
+ // sb_r2_transfer_range_v1
573
+ // sb_r2_transfer_range_header_v2
574
+ // sb_r2_transfer_conditional_v1
575
+ // sb_r2_transfer_streaming_v2
515
576
  if (method == "GET") {
516
577
  sb_r2_transfer_ctx* download = nullptr;
517
578
  const int status = sb_r2_transfer_download_open(bucket.c_str(), token.c_str(), &download);
518
579
  if (status != 0 || !download) { respond_with_error(res, sb_r2_transfer_status(status), "R2 transfer unavailable", true); return true; }
519
- struct Download { sb_r2_transfer_ctx* ctx; uintmax_t size; bool closed = false; };
520
- auto state = std::make_shared<Download>(Download{download, sb_r2_transfer_download_size(download)});
580
+ const std::string etag = sb_r2_transfer_download_etag(download);
581
+ const std::string_view if_none_match = req->getHeader("if-none-match");
582
+ if (if_none_match == "*" || if_none_match == etag || (if_none_match.size() == etag.size() + 2 && if_none_match.front() == '"' && if_none_match.back() == '"' && if_none_match.substr(1, etag.size()) == etag)) {
583
+ sb_r2_transfer_download_close(download); res->writeStatus("304 Not Modified"); res->writeHeader("ETag", etag); res->end(); return true;
584
+ }
585
+ const uintmax_t full_size = sb_r2_transfer_download_size(download);
586
+ uintmax_t first = 0, last = full_size ? full_size - 1 : 0;
587
+ std::string_view range = req->getHeader("range"); if (range.empty()) range = req->getHeader("Range");
588
+ if (!range.empty()) {
589
+ if (range.substr(0, 6) != "bytes=") { sb_r2_transfer_download_close(download); respond_with_error(res, "416 Range Not Satisfiable", "invalid range", true); return true; }
590
+ const std::string_view spec = range.substr(6); const size_t dash = spec.find('-');
591
+ if (dash == std::string_view::npos || dash == 0) { sb_r2_transfer_download_close(download); respond_with_error(res, "416 Range Not Satisfiable", "invalid range", true); return true; }
592
+ first = 0; for (char c : spec.substr(0, dash)) { if (c < '0' || c > '9') { sb_r2_transfer_download_close(download); respond_with_error(res, "416 Range Not Satisfiable", "invalid range", true); return true; } first = first * 10 + (c - '0'); }
593
+ if (dash + 1 < spec.size()) { last = 0; for (char c : spec.substr(dash + 1)) { if (c < '0' || c > '9') { sb_r2_transfer_download_close(download); respond_with_error(res, "416 Range Not Satisfiable", "invalid range", true); return true; } last = last * 10 + (c - '0'); } }
594
+ if (first >= full_size || last < first) { sb_r2_transfer_download_close(download); respond_with_error(res, "416 Range Not Satisfiable", "range outside object", true); return true; }
595
+ if (last >= full_size) last = full_size - 1;
596
+ char content_range[96]; snprintf(content_range, sizeof(content_range), "bytes %llu-%llu/%llu", (unsigned long long)first, (unsigned long long)last, (unsigned long long)full_size);
597
+ res->writeStatus("206 Partial Content"); res->writeHeader("Content-Range", content_range);
598
+ }
599
+ res->writeHeader("ETag", etag); res->writeHeader("Accept-Ranges", "bytes");
600
+ struct Download { sb_r2_transfer_ctx* ctx; uintmax_t base; uintmax_t size; uintmax_t sent = 0; bool closed = false; char chunk[65536]; };
601
+ auto state = std::make_shared<Download>(Download{download, first, last - first + 1});
521
602
  auto pump = std::make_shared<std::function<bool()>>();
522
603
  *pump = [res, state, pump]() {
523
604
  if (state->closed) return false;
524
- const uintmax_t offset = res->getWriteOffset(); char chunk[65536];
525
- const size_t bytes = sb_r2_transfer_download_read(state->ctx, (size_t)offset, chunk, sizeof(chunk));
526
- const auto result = res->tryEnd(std::string_view(chunk, bytes), state->size);
527
- if (result.second) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return false; }
528
- if (!result.first) res->onWritable([pump](uintmax_t) { return (*pump)(); });
529
- return false;
605
+ for (;;) {
606
+ const size_t want = state->size - state->sent < sizeof(state->chunk) ? (size_t)(state->size - state->sent) : sizeof(state->chunk);
607
+ const size_t bytes = sb_r2_transfer_download_read(state->ctx, (size_t)(state->base + state->sent), state->chunk, want);
608
+ const auto result = res->tryEnd(std::string_view(state->chunk, bytes), state->size);
609
+ if (result.second || bytes == 0) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return false; }
610
+ if (!result.first) {
611
+ res->onWritable([res, state](uintmax_t offset) {
612
+ if (state->closed) return true;
613
+ const size_t want = state->size - offset < sizeof(state->chunk) ? (size_t)(state->size - offset) : sizeof(state->chunk);
614
+ const size_t bytes = sb_r2_transfer_download_read(state->ctx, (size_t)(state->base + offset), state->chunk, want);
615
+ const auto resumed = res->tryEnd(std::string_view(state->chunk, bytes), state->size);
616
+ if (resumed.second || bytes == 0) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return true; }
617
+ return resumed.first;
618
+ });
619
+ return false;
620
+ }
621
+ state->sent += bytes;
622
+ if (state->sent == state->size) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return false; }
623
+ }
530
624
  };
531
625
  res->writeHeader("Content-Type", "application/octet-stream");
532
626
  res->onAborted([state]() { if (!state->closed) { sb_r2_transfer_download_close(state->ctx); state->closed = true; } });
@@ -566,8 +660,18 @@ const UWS_EDITS = [
566
660
  WRITE_RESPONSE_INJECT,
567
661
  "detect x-sb-raw-body, steer the body read (#176)",
568
662
  ],
569
- [R2_TRANSFER_MARKER, R2_TRANSFER_ANCHOR, R2_TRANSFER_INJECT, "native direct R2 transfer ingress (#195)"],
570
- [R2_TRANSFER_CALL_MARKER, R2_TRANSFER_CALL_ANCHOR, R2_TRANSFER_CALL_INJECT, "direct R2 transfer dispatch (#195)"],
663
+ [
664
+ R2_TRANSFER_MARKER,
665
+ R2_TRANSFER_ANCHOR,
666
+ R2_TRANSFER_INJECT,
667
+ "native direct R2 transfer ingress (#195)",
668
+ ],
669
+ [
670
+ R2_TRANSFER_CALL_MARKER,
671
+ R2_TRANSFER_CALL_ANCHOR,
672
+ R2_TRANSFER_CALL_INJECT,
673
+ "direct R2 transfer dispatch (#195)",
674
+ ],
571
675
  ] as const;
572
676
 
573
677
  /** The uWebSockets shim source: body limit + the #156 status-line fix. Exported for tests. */
@@ -575,16 +679,237 @@ export async function patchUwebsockets(root: string): Promise<void> {
575
679
  const file = resolve(root, "compiler/uwebsockets.js");
576
680
  let src = await readFile(file, "utf8");
577
681
  let changed = false;
682
+ // Existing shared compiler caches already contain the direct-transfer patch.
683
+ // Upgrade them in place so a cache built before #202 does not keep linking
684
+ // the R2 path into every subsequent artifact.
685
+ if (src.includes(R2_TRANSFER_MARKER) && !src.includes(R2_GATE_MARKER)) {
686
+ const oldOpen = 'extern "C" {\n struct sb_r2_transfer_ctx;';
687
+ const newOpen = '#ifdef SB_R2_TRANSFER\nextern "C" {\n struct sb_r2_transfer_ctx;';
688
+ const oldClose = `}\n${R2_TRANSFER_ANCHOR}`;
689
+ const newClose = `}\n#endif\n${R2_TRANSFER_ANCHOR}`;
690
+ const oldCall =
691
+ " if (try_handle_r2_transfer(res, req, method)) return;\n" + R2_TRANSFER_CALL_ANCHOR;
692
+ if (!src.includes(oldOpen) || !src.includes(oldClose) || !src.includes(oldCall))
693
+ throw new Error(
694
+ `could not gate Porffor's direct R2 transfer bridge: anchor not found in ${file}.`,
695
+ );
696
+ src = src
697
+ .replace(oldOpen, newOpen)
698
+ .replace(oldClose, newClose)
699
+ .replace(oldCall, R2_TRANSFER_CALL_INJECT);
700
+ changed = true;
701
+ }
578
702
  // Upgrade already-patched caches from the initial upload-only bridge. New
579
703
  // caches receive the full bridge below; old ones need declarations plus the
580
704
  // GET dispatch grafted onto their existing transfer handler.
581
705
  if (src.includes(R2_TRANSFER_MARKER) && !src.includes(R2_DOWNLOAD_CACHE_MARKER)) {
582
706
  if (!src.includes(R2_DOWNLOAD_DECL_ANCHOR) || !src.includes(R2_DOWNLOAD_BRANCH_ANCHOR))
583
- throw new Error(`could not upgrade Porffor's direct R2 transfer bridge: anchor not found in ${file}.`);
707
+ throw new Error(
708
+ `could not upgrade Porffor's direct R2 transfer bridge: anchor not found in ${file}.`,
709
+ );
584
710
  src = src.replace(R2_DOWNLOAD_DECL_ANCHOR, R2_DOWNLOAD_DECL_INJECT);
585
711
  src = src.replace(R2_DOWNLOAD_BRANCH_ANCHOR, R2_DOWNLOAD_BRANCH_INJECT);
586
712
  changed = true;
587
713
  }
714
+ if (src.includes(R2_DOWNLOAD_CACHE_MARKER) && !src.includes(R2_RANGE_CACHE_MARKER)) {
715
+ const stateAnchor =
716
+ " struct Download { sb_r2_transfer_ctx* ctx; uintmax_t size; bool closed = false; };\n auto state = std::make_shared<Download>(Download{download, sb_r2_transfer_download_size(download)});";
717
+ const range = ` const uintmax_t full_size = sb_r2_transfer_download_size(download);
718
+ uintmax_t first = 0, last = full_size ? full_size - 1 : 0;
719
+ const std::string_view range = req->getHeader("range");
720
+ if (!range.empty()) {
721
+ if (range.substr(0, 6) != "bytes=") { sb_r2_transfer_download_close(download); respond_with_error(res, "416 Range Not Satisfiable", "invalid range", true); return true; }
722
+ const std::string_view spec = range.substr(6); const size_t dash = spec.find('-');
723
+ if (dash == std::string_view::npos || dash == 0) { sb_r2_transfer_download_close(download); respond_with_error(res, "416 Range Not Satisfiable", "invalid range", true); return true; }
724
+ first = 0; for (char c : spec.substr(0, dash)) { if (c < '0' || c > '9') { sb_r2_transfer_download_close(download); respond_with_error(res, "416 Range Not Satisfiable", "invalid range", true); return true; } first = first * 10 + (c - '0'); }
725
+ if (dash + 1 < spec.size()) { last = 0; for (char c : spec.substr(dash + 1)) { if (c < '0' || c > '9') { sb_r2_transfer_download_close(download); respond_with_error(res, "416 Range Not Satisfiable", "invalid range", true); return true; } last = last * 10 + (c - '0'); } }
726
+ if (first >= full_size || last < first) { sb_r2_transfer_download_close(download); respond_with_error(res, "416 Range Not Satisfiable", "range outside object", true); return true; }
727
+ if (last >= full_size) last = full_size - 1;
728
+ char content_range[96]; snprintf(content_range, sizeof(content_range), "bytes %llu-%llu/%llu", (unsigned long long)first, (unsigned long long)last, (unsigned long long)full_size);
729
+ res->writeStatus("206 Partial Content"); res->writeHeader("Content-Range", content_range);
730
+ }
731
+ `;
732
+ const state =
733
+ " struct Download { sb_r2_transfer_ctx* ctx; uintmax_t base; uintmax_t size; bool closed = false; };\n auto state = std::make_shared<Download>(Download{download, first, last - first + 1});";
734
+ if (!src.includes(stateAnchor))
735
+ throw new Error(
736
+ `could not upgrade Porffor's direct R2 range bridge: anchor not found in ${file}.`,
737
+ );
738
+ src = src.replace(
739
+ " // sb_r2_transfer_download_v1\n",
740
+ " // sb_r2_transfer_download_v1\n // sb_r2_transfer_range_v1\n",
741
+ );
742
+ src = src.replace(stateAnchor, range + state);
743
+ src = src.replace(
744
+ "sb_r2_transfer_download_read(state->ctx, (size_t)offset, chunk, sizeof(chunk))",
745
+ "sb_r2_transfer_download_read(state->ctx, (size_t)(state->base + offset), chunk, sizeof(chunk))",
746
+ );
747
+ changed = true;
748
+ }
749
+ if (src.includes(R2_RANGE_CACHE_MARKER) && !src.includes(R2_CONDITIONAL_CACHE_MARKER)) {
750
+ const declaration = " size_t sb_r2_transfer_download_size(sb_r2_transfer_ctx*);\n";
751
+ const declarationReplacement =
752
+ declaration + " const char* sb_r2_transfer_download_etag(sb_r2_transfer_ctx*);\n";
753
+ const anchor = " const uintmax_t full_size = sb_r2_transfer_download_size(download);\n";
754
+ const inject =
755
+ ` // sb_r2_transfer_conditional_v1
756
+ const std::string etag = sb_r2_transfer_download_etag(download);
757
+ res->writeHeader("ETag", etag); res->writeHeader("Accept-Ranges", "bytes");
758
+ const std::string_view if_none_match = req->getHeader("if-none-match");
759
+ if (if_none_match == "*" || if_none_match == etag || (if_none_match.size() == etag.size() + 2 && if_none_match.front() == '"' && if_none_match.back() == '"' && if_none_match.substr(1, etag.size()) == etag)) {
760
+ sb_r2_transfer_download_close(download); res->writeStatus("304 Not Modified"); res->end(); return true;
761
+ }
762
+ ` + anchor;
763
+ if (!src.includes(declaration) || !src.includes(anchor))
764
+ throw new Error(
765
+ `could not upgrade Porffor's direct R2 conditional bridge: anchor not found in ${file}.`,
766
+ );
767
+ src = src.replace(declaration, declarationReplacement).replace(anchor, inject);
768
+ changed = true;
769
+ }
770
+ if (src.includes(R2_CONDITIONAL_CACHE_MARKER) && !src.includes(R2_STREAMING_CACHE_MARKER)) {
771
+ const oldPump = ` const uintmax_t offset = res->getWriteOffset();
772
+ char chunk[65536];
773
+ const size_t bytes = sb_r2_transfer_download_read(state->ctx, (size_t)(state->base + offset), chunk, sizeof(chunk));
774
+ const auto result = res->tryEnd(std::string_view(chunk, bytes), state->size);
775
+ if (result.second) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return false; }
776
+ if (!result.first) res->onWritable([pump](uintmax_t) { return (*pump)(); });
777
+ return false;`;
778
+ const newPump = ` for (;;) {
779
+ const uintmax_t offset = res->getWriteOffset(); char chunk[65536];
780
+ const size_t bytes = sb_r2_transfer_download_read(state->ctx, (size_t)(state->base + offset), chunk, sizeof(chunk));
781
+ const auto result = res->tryEnd(std::string_view(chunk, bytes), state->size);
782
+ if (result.second) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return false; }
783
+ if (!result.first) { res->onWritable([pump](uintmax_t) { return (*pump)(); }); return false; }
784
+ }`;
785
+ if (!src.includes(oldPump))
786
+ throw new Error(
787
+ `could not upgrade Porffor's direct R2 streaming bridge: anchor not found in ${file}.`,
788
+ );
789
+ src = src
790
+ .replace(
791
+ " // sb_r2_transfer_conditional_v1\n",
792
+ " // sb_r2_transfer_conditional_v1\n // sb_r2_transfer_streaming_v2\n",
793
+ )
794
+ .replace(oldPump, newPump);
795
+ changed = true;
796
+ }
797
+ // A short-lived v2 cache marker was written before its pump fix. Repair
798
+ // those caches as well as applying the current form to future upgrades.
799
+ if (
800
+ src.includes("state->base + offset") &&
801
+ src.includes("uintmax_t size; bool closed = false;")
802
+ ) {
803
+ const oldState =
804
+ "struct Download { sb_r2_transfer_ctx* ctx; uintmax_t base; uintmax_t size; bool closed = false; };";
805
+ const newState =
806
+ "struct Download { sb_r2_transfer_ctx* ctx; uintmax_t base; uintmax_t size; uintmax_t sent = 0; bool closed = false; };";
807
+ const oldPump = ` const uintmax_t offset = res->getWriteOffset(); char chunk[65536];
808
+ const size_t bytes = sb_r2_transfer_download_read(state->ctx, (size_t)(state->base + offset), chunk, sizeof(chunk));
809
+ const auto result = res->tryEnd(std::string_view(chunk, bytes), state->size);
810
+ if (result.second) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return false; }
811
+ if (!result.first) { res->onWritable([pump](uintmax_t) { return (*pump)(); }); return false; }
812
+ }`;
813
+ const newPump = ` char chunk[65536];
814
+ const size_t bytes = sb_r2_transfer_download_read(state->ctx, (size_t)(state->base + state->sent), chunk, sizeof(chunk));
815
+ const auto result = res->tryEnd(std::string_view(chunk, bytes), state->size);
816
+ if (result.second) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return false; }
817
+ if (!result.first) { res->onWritable([pump, state](uintmax_t offset) { state->sent = offset; return (*pump)(); }); return false; }
818
+ state->sent += bytes;
819
+ }`;
820
+ if (!src.includes(oldState) || !src.includes(oldPump))
821
+ throw new Error(
822
+ `could not repair Porffor's direct R2 streaming bridge: anchor not found in ${file}.`,
823
+ );
824
+ src = src.replace(oldState, newState).replace(oldPump, newPump);
825
+ changed = true;
826
+ }
827
+ const duplicatePump = " for (;;) {\n char chunk[65536];\n for (;;) {\n";
828
+ if (src.includes(duplicatePump)) {
829
+ src = src.replace(duplicatePump, " for (;;) {\n char chunk[65536];\n");
830
+ changed = true;
831
+ }
832
+ const unclosedPump = " state->sent += bytes;\n };\n";
833
+ if (src.includes(unclosedPump)) {
834
+ src = src.replace(unclosedPump, " state->sent += bytes;\n }\n };\n");
835
+ changed = true;
836
+ }
837
+ if (src.includes(R2_STREAMING_CACHE_MARKER) && !src.includes(R2_RANGE_HEADER_CACHE_MARKER)) {
838
+ const rangeHeader = ' const std::string_view range = req->getHeader("range");\n';
839
+ if (!src.includes(rangeHeader))
840
+ throw new Error(
841
+ `could not upgrade Porffor's direct R2 range-header bridge: anchor not found in ${file}.`,
842
+ );
843
+ src = src.replace(
844
+ " // sb_r2_transfer_range_v1\n",
845
+ " // sb_r2_transfer_range_v1\n // sb_r2_transfer_range_header_v2\n",
846
+ );
847
+ src = src.replace(
848
+ rangeHeader,
849
+ ' std::string_view range = req->getHeader("range"); if (range.empty()) range = req->getHeader("Range");\n',
850
+ );
851
+ changed = true;
852
+ }
853
+ const earlyConditionalHeaders =
854
+ ' res->writeHeader("ETag", etag); res->writeHeader("Accept-Ranges", "bytes");\n';
855
+ if (
856
+ src.includes(earlyConditionalHeaders) &&
857
+ src.indexOf(earlyConditionalHeaders) <
858
+ src.indexOf(" const uintmax_t full_size = sb_r2_transfer_download_size(download);")
859
+ ) {
860
+ const old304 =
861
+ ' sb_r2_transfer_download_close(download); res->writeStatus("304 Not Modified"); res->end(); return true;';
862
+ const new304 =
863
+ ' sb_r2_transfer_download_close(download); res->writeStatus("304 Not Modified"); res->writeHeader("ETag", etag); res->end(); return true;';
864
+ const rangeEnd =
865
+ ' res->writeStatus("206 Partial Content"); res->writeHeader("Content-Range", content_range);\n }\n';
866
+ if (!src.includes(old304) || !src.includes(rangeEnd))
867
+ throw new Error(
868
+ `could not repair Porffor's direct R2 response ordering: anchor not found in ${file}.`,
869
+ );
870
+ src = src
871
+ .replace(earlyConditionalHeaders, "")
872
+ .replace(old304, new304)
873
+ .replace(
874
+ rangeEnd,
875
+ rangeEnd +
876
+ ' res->writeHeader("ETag", etag); res->writeHeader("Accept-Ranges", "bytes");\n',
877
+ );
878
+ changed = true;
879
+ }
880
+ if (src.includes("state->sent") && src.includes(R2_DOWNLOAD_CACHE_MARKER)) {
881
+ const stateStart = src.indexOf(" struct Download {", src.indexOf(R2_DOWNLOAD_CACHE_MARKER));
882
+ const stateEnd = src.indexOf(' res->writeHeader("Content-Type"', stateStart);
883
+ const freshStart = R2_DOWNLOAD_BRANCH_INJECT.indexOf(" struct Download {");
884
+ const freshEnd = R2_DOWNLOAD_BRANCH_INJECT.indexOf(
885
+ ' res->writeHeader("Content-Type"',
886
+ freshStart,
887
+ );
888
+ if (stateStart < 0 || stateEnd < 0 || freshStart < 0 || freshEnd < 0)
889
+ throw new Error(
890
+ `could not replace Porffor's direct R2 stream pump: anchor not found in ${file}.`,
891
+ );
892
+ src =
893
+ src.slice(0, stateStart) +
894
+ R2_DOWNLOAD_BRANCH_INJECT.slice(freshStart, freshEnd) +
895
+ src.slice(stateEnd);
896
+ changed = true;
897
+ }
898
+ if (src.includes("writable_offset") && src.includes(R2_DOWNLOAD_CACHE_MARKER)) {
899
+ const stateStart = src.indexOf(" struct Download {", src.indexOf(R2_DOWNLOAD_CACHE_MARKER));
900
+ const stateEnd = src.indexOf(' res->writeHeader("Content-Type"', stateStart);
901
+ const freshStart = R2_TRANSFER_INJECT.indexOf(" struct Download {");
902
+ const freshEnd = R2_TRANSFER_INJECT.indexOf(' res->writeHeader("Content-Type"', freshStart);
903
+ if (stateStart < 0 || stateEnd < 0 || freshStart < 0 || freshEnd < 0)
904
+ throw new Error(
905
+ `could not restore Porffor's direct R2 stream pump: anchor not found in ${file}.`,
906
+ );
907
+ src =
908
+ src.slice(0, stateStart) +
909
+ R2_TRANSFER_INJECT.slice(freshStart, freshEnd) +
910
+ src.slice(stateEnd);
911
+ changed = true;
912
+ }
588
913
  for (const [marker, anchor, inject, what] of UWS_EDITS) {
589
914
  if (src.includes(marker)) continue;
590
915
  if (!src.includes(anchor)) {
@@ -599,13 +924,14 @@ export async function patchUwebsockets(root: string): Promise<void> {
599
924
  if (changed) await writeFile(file, src);
600
925
  }
601
926
 
602
- async function patchCompilerArgs(root: string): Promise<void> {
927
+ export async function patchCompilerArgs(root: string): Promise<void> {
603
928
  const file = resolve(root, "compiler/index.js");
604
929
  let src = await readFile(file, "utf8");
605
930
  let changed = false;
606
931
  for (const [marker, anchor, inject, what] of [
607
932
  [LINK_MARKER, LINK_ANCHOR, LINK_INJECT, "extra link args"],
608
933
  [CFLAGS_MARKER, CFLAGS_ANCHOR, CFLAGS_INJECT, "extra compiler flags"],
934
+ [UWS_CFLAGS_MARKER, UWS_CFLAGS_ANCHOR, UWS_CFLAGS_INJECT, "native-fetch shim compiler flags"],
609
935
  ] as const) {
610
936
  if (src.includes(marker)) continue;
611
937
  const at = src.indexOf(anchor);
@@ -615,10 +941,10 @@ async function patchCompilerArgs(root: string): Promise<void> {
615
941
  "Porffor's native-fetch build changed — check patches/UPSTREAM.md.",
616
942
  );
617
943
  }
618
- // After the anchor for cflags (the args follow it), before it for the link
619
- // line (the object list ends with it).
944
+ // After the anchor for compiler flags (the args follow it), before it for
945
+ // the link line (the object list ends with it).
620
946
  src =
621
- marker === CFLAGS_MARKER
947
+ marker === CFLAGS_MARKER || marker === UWS_CFLAGS_MARKER
622
948
  ? src.slice(0, at + anchor.length) + inject + src.slice(at + anchor.length)
623
949
  : src.slice(0, at) + inject + src.slice(at);
624
950
  changed = true;