@sproutboat/toolchain 0.7.0 → 0.8.1

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/src/patch.ts CHANGED
@@ -15,6 +15,15 @@
15
15
  import { readFile, realpath, writeFile } from "node:fs/promises";
16
16
  import { resolve } from "node:path";
17
17
  import { patchUnicode } from "./unicode";
18
+ import { URL_CLASS_SOURCE } from "./url-class";
19
+ import { TEXT_DECODER_SOURCE } from "./text-decoder";
20
+ import { CASE_LOWER_RUNS, CASE_UPPER_RUNS } from "./case-map";
21
+ import { HEADERS_CLASS_SOURCE } from "./headers-class";
22
+ import { BLOB_CLASS_SOURCE, REQUEST_CLASS_SOURCE, RESPONSE_BYTES_ANCHOR, RESPONSE_BYTES_INJECT, RESPONSE_CLASS_SOURCE } from "./fetch-classes";
23
+ import * as streaming from "./streaming";
24
+ import { PORFFOR_COMMIT_FULL } from "./pin";
25
+ import { PATCH_FINGERPRINT } from "./fingerprint";
26
+ import { version as TOOLCHAIN_VERSION } from "../package.json" with { type: "json" };
18
27
 
19
28
  // --- compiler/render.js: the native-fetch server, rendered as C text ---
20
29
 
@@ -47,7 +56,12 @@ const CONSOLE_MARKER = "sproutboat #165";
47
56
  /** render.js edits: `[marker, anchor, inject, what]`, each inserted after its anchor. */
48
57
  const RENDER_EDITS = [
49
58
  [PORT_MARKER, PORT_ANCHOR, PORT_INJECT, "$PORT support"],
50
- [CONSOLE_MARKER, CONSOLE_ANCHOR, CONSOLE_INJECT, "console output sink (#165)"],
59
+ [
60
+ CONSOLE_MARKER,
61
+ CONSOLE_ANCHOR,
62
+ CONSOLE_INJECT,
63
+ "console output sink (#165)",
64
+ ],
51
65
  ] as const;
52
66
 
53
67
  /**
@@ -131,15 +145,24 @@ const READ_RAW_INJECT =
131
145
  READ_RAW_ANCHOR;
132
146
  const READ_RAW_MARKER = "porf_native_fetch_read_raw_bytes(jsval value";
133
147
 
134
-
135
148
  /**
136
149
  * render.js full-block replacements: `[marker, anchor, inject, what]`, each a
137
150
  * complete swap of the matched region (unlike `RENDER_EDITS`, which only ever
138
151
  * inserts after its anchor).
139
152
  */
140
153
  const RENDER_REPLACEMENTS = [
141
- [BYTESTRING_MARKER, BYTESTRING_ANCHOR, BYTESTRING_INJECT, "bytestring UTF-8 encoding (#172)"],
142
- [READ_RAW_MARKER, READ_RAW_ANCHOR, READ_RAW_INJECT, "raw bytestring passthrough function (#176)"],
154
+ [
155
+ BYTESTRING_MARKER,
156
+ BYTESTRING_ANCHOR,
157
+ BYTESTRING_INJECT,
158
+ "bytestring UTF-8 encoding (#172)",
159
+ ],
160
+ [
161
+ READ_RAW_MARKER,
162
+ READ_RAW_ANCHOR,
163
+ READ_RAW_INJECT,
164
+ "raw bytestring passthrough function (#176)",
165
+ ],
143
166
  ] as const;
144
167
 
145
168
  // #15 — no `--port` flag: Porffor's native-fetch entry point calls
@@ -266,8 +289,10 @@ const CFLAGS_MARKER = "SB_EXTRA_CFLAGS";
266
289
  // into one FMA on arm64, rounding once where JavaScript rounds twice. A seeded
267
290
  // LCG (seed * 1103515245 + 12345, past 2^53) diverged from V8 at step 23.
268
291
  // Every compiled unit keeps JS's separate roundings with contraction off.
269
- const FP_CONTRACT_ANCHOR = " '-fno-ident', '-ffunction-sections', '-fdata-sections',\n";
270
- const FP_CONTRACT_INJECT = " '-ffp-contract=off', // sproutboat #235: JS rounds a * b + c twice\n";
292
+ const FP_CONTRACT_ANCHOR =
293
+ " '-fno-ident', '-ffunction-sections', '-fdata-sections',\n";
294
+ const FP_CONTRACT_INJECT =
295
+ " '-ffp-contract=off', // sproutboat #235: JS rounds a * b + c twice\n";
271
296
  const FP_CONTRACT_MARKER = "sproutboat #235";
272
297
 
273
298
  // baronunread/sproutboat#236: uWebSockets answers any request line that isn't
@@ -313,14 +338,17 @@ export const sbPatchHttp10 = uwsDir => {
313
338
  };
314
339
  `;
315
340
  const HTTP10_IMPORT_ANCHOR = "import { hashId } from './modules.js';\n";
316
- const HTTP10_IMPORT_INJECT = "import { sbPatchHttp10 } from './sb-http10.js'; // sproutboat #236\n";
341
+ const HTTP10_IMPORT_INJECT =
342
+ "import { sbPatchHttp10 } from './sb-http10.js'; // sproutboat #236\n";
317
343
  const HTTP10_IMPORT_MARKER = "./sb-http10.js";
318
- const HTTP10_CALL_ANCHOR = " const uwsDir = uwebsockets.ensureUWebSockets();\n";
344
+ const HTTP10_CALL_ANCHOR =
345
+ " const uwsDir = uwebsockets.ensureUWebSockets();\n";
319
346
  const HTTP10_CALL_INJECT = " sbPatchHttp10(uwsDir);\n";
320
347
  const HTTP10_CALL_MARKER = "sbPatchHttp10(uwsDir)";
321
348
  // The shim is content-cached, so a header edit alone would never recompile it.
322
349
  // This line changes the shim once, which rebuilds it against the edited header.
323
- const HTTP10_SHIM_ANCHOR = 'export const makeUWebSocketsShimSource = () => `\n#include "App.h"\n';
350
+ const HTTP10_SHIM_ANCHOR =
351
+ 'export const makeUWebSocketsShimSource = () => `\n#include "App.h"\n';
324
352
  const HTTP10_SHIM_INJECT =
325
353
  'export const makeUWebSocketsShimSource = () => `\n#include "App.h"\n// sproutboat #236: built against an HttpParser.h that accepts HTTP/1.0\n';
326
354
  const HTTP10_SHIM_MARKER = "sproutboat #236: built against";
@@ -361,56 +389,6 @@ const REPLACE_ALL_EDITS = [
361
389
  ] as const;
362
390
  const REPLACE_ALL_MARKER = "sproutboat #237: pieces joined once";
363
391
 
364
- // baronunread/sproutboat#238: typed-array element reads and writes had no
365
- // bounds check. A read past the end returned whatever sat in the next
366
- // allocation instead of undefined (uuid's SHA-1 reads past its buffer, so v5
367
- // UUIDs changed run to run) and a[-1] / a[1.5] broke outright. Reuse the
368
- // plain-array index check and compare against the typed array's length.
369
- // Builtins keep the unchecked read: they index within bounds, and the check
370
- // broke typed-array join/toString when compiled into them. Writes need the same
371
- // guard, but builtins such as the constructor and fill() store elements before
372
- // they set the length, so that is left for a follow-up.
373
- const TA_GET_ANCHOR =
374
- " const taGet = (ctype, size, signed = true) => () => {\n" +
375
- " const loaded = Load(ctype, taAddr(size), 4);\n" +
376
- " const f = ctype === 'f32' || ctype === 'f64' ? loaded : Convert(T.f64, loaded, signed ? CONVERT_SIGNED : 0);\n" +
377
- " return Box(f, Const(T.i32, TYPES.number));\n" +
378
- " };";
379
- const TA_GET_INJECT =
380
- " const taGet = (ctype, size, signed = true) => () => {\n" +
381
- " // sproutboat #238: only a valid in-range integer index reads; anything else is undefined.\n" +
382
- " // User code only: builtins (precompiled) index within bounds themselves and\n" +
383
- " // typed-array join/toString misbehave with the extra check.\n" +
384
- " if (globalThis.precompile) {\n" +
385
- " const raw = Load(ctype, taAddr(size), 4);\n" +
386
- " return Box(ctype === 'f32' || ctype === 'f64' ? raw : Convert(T.f64, raw, signed ? CONVERT_SIGNED : 0), Const(T.i32, TYPES.number));\n" +
387
- " }\n" +
388
- " const { idx, valid } = denseArrayIndexKey(scope, prop);\n" +
389
- " const loaded = Load(ctype, Bin('+', T.u32, Load('u32', JvPtr(obj), 4), size === 1 ? idx : Bin('*', T.u32, idx, Const(T.u32, size))), 4);\n" +
390
- " const f = ctype === 'f32' || ctype === 'f64' ? loaded : Convert(T.f64, loaded, signed ? CONVERT_SIGNED : 0);\n" +
391
- " return Select(Bin('&&', T.i32, valid, Bin('<', T.i32, idx, Load('u32', JvPtr(obj), 0))),\n" +
392
- " Box(f, Const(T.i32, TYPES.number)), valUndefined());\n" +
393
- " };";
394
- const TA_GET_MARKER = "sproutboat #238: only a valid in-range integer index reads";
395
- // baronunread/sproutboat#241: a typed-array write outside the array stored into
396
- // whatever memory followed it (and a[-1] = x wrote a[0], via the saturating
397
- // index conversion). ECMA-262 ignores the write. Same check as taGet, in user
398
- // code only: builtins such as the constructor store elements before they set
399
- // the length, and they only write in bounds anyway. Alpha-14 fixed the store's
400
- // conversion upstream (ToUint32, which our #238 patch used to add); the bounds
401
- // check is still missing there.
402
- const TA_SET_BOUNDS_ANCHOR =
403
- " stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : toUint32(scope, f)));";
404
- const TA_SET_BOUNDS_INJECT =
405
- " const store = () => stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : toUint32(scope, f)));\n" +
406
- " // sproutboat #241: a write outside the array is ignored, not stored into the next allocation.\n" +
407
- " if (globalThis.precompile) store();\n" +
408
- " else {\n" +
409
- " const { idx: sbIdx, valid: sbValid } = denseArrayIndexKey(scope, prop);\n" +
410
- " emitIf(scope, Bin('&&', T.i32, sbValid, Bin('<', T.i32, sbIdx, Load('u32', JvPtr(obj), 0))), store);\n" +
411
- " }";
412
- const TA_SET_BOUNDS_MARKER = "sproutboat #241: a write outside the array is ignored";
413
-
414
392
  // Porffor alpha 9 treats +02:00 as two milliseconds and adds offset hours
415
393
  // instead of subtracting them. Count fields even when they contain zero, and
416
394
  // apply the signed offset after parsing the local wall-clock fields.
@@ -512,7 +490,8 @@ const DATE_PARSER_INJECT = ` // sproutboat: ISO timezone offset
512
490
  * The default stays 1 MiB: a larger body is held in memory whole, so raising it
513
491
  * is a decision about this deployment's memory, not something to inherit.
514
492
  */
515
- const BODY_ANCHOR = "static const size_t REQUEST_BODY_MAX_BYTES = 1024u * 1024u;";
493
+ const BODY_ANCHOR =
494
+ "static const size_t REQUEST_BODY_MAX_BYTES = 1024u * 1024u;";
516
495
  const BODY_INJECT = `static size_t sb_request_body_max(void) {
517
496
  static size_t cached = 0;
518
497
  if (cached == 0) {
@@ -538,7 +517,8 @@ const BODY_MARKER = "sb_request_body_max";
538
517
  * the sole caller feeds it straight to `writeStatus`, which copies synchronously).
539
518
  * Known codes keep their proper reason phrase.
540
519
  */
541
- const STATUS_SIG_ANCHOR = "static std::string_view lookup_status_line(i32 status) {";
520
+ const STATUS_SIG_ANCHOR =
521
+ "static std::string_view lookup_status_line(i32 status) {";
542
522
  const STATUS_SIG_INJECT = "static std::string lookup_status_line(i32 status) {";
543
523
  const STATUS_SIG_MARKER = "static std::string lookup_status_line(i32 status) {";
544
524
  // 303 is the one this was reported for (POST-redirect-GET); give it the real
@@ -548,7 +528,8 @@ const STATUS_303_INJECT =
548
528
  ' case 302: return "302 Found";\n case 303: return "303 See Other";\n';
549
529
  const STATUS_303_MARKER = 'case 303: return "303 See Other";';
550
530
  const STATUS_DEFAULT_ANCHOR = " default: return {};";
551
- const STATUS_DEFAULT_INJECT = ' default: return std::to_string(status) + " Status";';
531
+ const STATUS_DEFAULT_INJECT =
532
+ ' default: return std::to_string(status) + " Status";';
552
533
  const STATUS_DEFAULT_MARKER = 'std::to_string(status) + " Status"';
553
534
 
554
535
  /**
@@ -564,7 +545,8 @@ const STATUS_DEFAULT_MARKER = 'std::to_string(status) + " Status"';
564
545
  const HDR_SIG_ANCHOR = "static i32 collect_headers(uWS::HttpRequest* req) {\n";
565
546
  const HDR_SIG_INJECT =
566
547
  "static i32 collect_headers(uWS::HttpRequest* req, uWS::HttpResponse<false>* res) {\n";
567
- const HDR_SIG_MARKER = "collect_headers(uWS::HttpRequest* req, uWS::HttpResponse";
548
+ const HDR_SIG_MARKER =
549
+ "collect_headers(uWS::HttpRequest* req, uWS::HttpResponse";
568
550
 
569
551
  const HDR_CALL_ANCHOR = "const i32 headers_ptr = collect_headers(req);";
570
552
  const HDR_CALL_INJECT = "const i32 headers_ptr = collect_headers(req, res);";
@@ -582,7 +564,8 @@ const HDR_SKIP_INJECT =
582
564
  ' if (key == "x-sb-remote-addr") continue; // #163: reserved, the server sets it below\n';
583
565
  const HDR_SKIP_MARKER = 'key == "x-sb-remote-addr"';
584
566
 
585
- const HDR_APPEND_ANCHOR = " *((i32*)(porf_mem + headers_ptr)) = slot;\n\n return headers_ptr;";
567
+ const HDR_APPEND_ANCHOR =
568
+ " *((i32*)(porf_mem + headers_ptr)) = slot;\n\n return headers_ptr;";
586
569
  const HDR_APPEND_INJECT =
587
570
  " {\n" +
588
571
  " const std::string_view __sb_peer = res ? res->getRemoteAddressAsText() : std::string_view();\n" +
@@ -611,7 +594,8 @@ const READ_RAW_DECL_ANCHOR =
611
594
  const READ_RAW_DECL_INJECT =
612
595
  "int porf_native_fetch_read_value(struct jsval value, const char** out_buf, size_t* out_len, char** out_owned);\n" +
613
596
  "int porf_native_fetch_read_raw_bytes(struct jsval value, const char** out_buf, size_t* out_len);";
614
- const READ_RAW_DECL_MARKER = "porf_native_fetch_read_raw_bytes(struct jsval value";
597
+ const READ_RAW_DECL_MARKER =
598
+ "porf_native_fetch_read_raw_bytes(struct jsval value";
615
599
 
616
600
  const FORBIDDEN_HDR_ANCHOR =
617
601
  "static bool is_forbidden_response_header(std::string_view key) {\n" +
@@ -707,8 +691,16 @@ const WRITE_RESPONSE_INJECT =
707
691
  " const char* body_buf = nullptr;\n" +
708
692
  " size_t body_len = 0;\n" +
709
693
  " char* body_owned = nullptr;\n" +
710
- " if (raw_body) porf_native_fetch_read_raw_bytes(body_value, &body_buf, &body_len);\n" +
711
- " else porf_native_fetch_read_value(body_value, &body_buf, &body_len, &body_owned);\n" +
694
+ " if (raw_body && porf_native_fetch_read_raw_bytes(body_value, &body_buf, &body_len) != 0 && body_value.type == 67) {\n" +
695
+ " // Raw bytes can also arrive as a UTF-16 string (a fetch() response body parsed\n" +
696
+ " // from JSON), one byte per unit: narrow each unit, as the R2 upload path does.\n" +
697
+ " const u32 body_ptr = (u32)body_value.val;\n" +
698
+ " body_len = (size_t)*((u32*)(porf_mem + body_ptr));\n" +
699
+ " body_owned = (char*)malloc(body_len + 1);\n" +
700
+ " const unsigned short* units = (const unsigned short*)(porf_mem + body_ptr + 4);\n" +
701
+ " for (size_t i = 0; i < body_len; i++) body_owned[i] = (char)(units[i] & 0xff);\n" +
702
+ " body_buf = body_owned;\n" +
703
+ " } else if (!raw_body || !body_buf) porf_native_fetch_read_value(body_value, &body_buf, &body_len, &body_owned);\n" +
712
704
  "\n" +
713
705
  " if (!aborted || !*aborted) {\n" +
714
706
  " res->cork([res, status, headers_entries_ptr, body_buf, body_len]() {\n" +
@@ -751,7 +743,8 @@ const WRITE_RESPONSE_MARKER = "porf_native_fetch_read_raw_bytes(body_value";
751
743
  // #195: reserve a direct-transfer ticket before native-fetch allocates the
752
744
  // ordinary in-memory PendingRequest. uWS then gives each wire chunk directly
753
745
  // to the runtime C ABI, which owns the temporary object file.
754
- const R2_TRANSFER_ANCHOR = "static void on_request(uWS::HttpResponse<false>* res, uWS::HttpRequest* req) {\n";
746
+ const R2_TRANSFER_ANCHOR =
747
+ "static void on_request(uWS::HttpResponse<false>* res, uWS::HttpRequest* req) {\n";
755
748
  const R2_TRANSFER_INJECT = `extern "C" {
756
749
  struct sb_r2_transfer_ctx;
757
750
  int sb_r2_transfer_open(const char*, const char*, size_t, sb_r2_transfer_ctx**);
@@ -819,7 +812,7 @@ static bool try_handle_r2_transfer(uWS::HttpResponse<false>* res, uWS::HttpReque
819
812
  struct Download { sb_r2_transfer_ctx* ctx; uintmax_t base; uintmax_t size; uintmax_t sent = 0; bool closed = false; char chunk[65536]; };
820
813
  auto state = std::make_shared<Download>(Download{download, first, last - first + 1});
821
814
  auto pump = std::make_shared<std::function<bool()>>();
822
- *pump = [res, state, pump]() {
815
+ *pump = [res, state]() {
823
816
  if (state->closed) return false;
824
817
  for (;;) {
825
818
  const size_t want = state->size - state->sent < sizeof(state->chunk) ? (size_t)(state->size - state->sent) : sizeof(state->chunk);
@@ -829,11 +822,14 @@ static bool try_handle_r2_transfer(uWS::HttpResponse<false>* res, uWS::HttpReque
829
822
  if (!result.first) {
830
823
  res->onWritable([res, state](uintmax_t offset) {
831
824
  if (state->closed) return true;
832
- const size_t want = state->size - offset < sizeof(state->chunk) ? (size_t)(state->size - offset) : sizeof(state->chunk);
833
- const size_t bytes = sb_r2_transfer_download_read(state->ctx, (size_t)(state->base + offset), state->chunk, want);
834
- const auto resumed = res->tryEnd(std::string_view(state->chunk, bytes), state->size);
835
- if (resumed.second || bytes == 0) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return true; }
836
- return resumed.first;
825
+ for (;;) {
826
+ const size_t want = state->size - offset < sizeof(state->chunk) ? (size_t)(state->size - offset) : sizeof(state->chunk);
827
+ const size_t bytes = sb_r2_transfer_download_read(state->ctx, (size_t)(state->base + offset), state->chunk, want);
828
+ const auto resumed = res->tryEnd(std::string_view(state->chunk, bytes), state->size);
829
+ if (resumed.second || bytes == 0) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return true; }
830
+ if (!resumed.first) return false;
831
+ offset += bytes;
832
+ }
837
833
  });
838
834
  return false;
839
835
  }
@@ -870,16 +866,19 @@ static bool try_handle_r2_transfer(uWS::HttpResponse<false>* res, uWS::HttpReque
870
866
  static void on_request(uWS::HttpResponse<false>* res, uWS::HttpRequest* req) {
871
867
  `;
872
868
  const R2_TRANSFER_MARKER = "sb_r2_transfer_path(std::string_view path";
873
- const R2_TRANSFER_CALL_ANCHOR = " __porffor_js_enter();\n const i32 url_ptr = alloc_request_url(req);";
869
+ const R2_TRANSFER_CALL_ANCHOR =
870
+ " __porffor_js_enter();\n const i32 url_ptr = alloc_request_url(req);";
874
871
  const R2_TRANSFER_CALL_INJECT =
875
- " if (try_handle_r2_transfer(res, req, method)) return;\n" + R2_TRANSFER_CALL_ANCHOR;
872
+ " if (try_handle_r2_transfer(res, req, method)) return;\n" +
873
+ R2_TRANSFER_CALL_ANCHOR;
876
874
  const R2_TRANSFER_CALL_MARKER = "try_handle_r2_transfer(res, req, method)";
877
875
  const R2_DOWNLOAD_CACHE_MARKER = "sb_r2_transfer_download_v1";
878
876
  const R2_RANGE_CACHE_MARKER = "sb_r2_transfer_range_v1";
879
877
  const R2_CONDITIONAL_CACHE_MARKER = "sb_r2_transfer_conditional_v1";
880
878
  const R2_STREAMING_CACHE_MARKER = "sb_r2_transfer_streaming_v2";
881
879
  const R2_RANGE_HEADER_CACHE_MARKER = "sb_r2_transfer_range_header_v2";
882
- const R2_DOWNLOAD_DECL_ANCHOR = " void sb_r2_transfer_abort(sb_r2_transfer_ctx*);\n}";
880
+ const R2_DOWNLOAD_DECL_ANCHOR =
881
+ " void sb_r2_transfer_abort(sb_r2_transfer_ctx*);\n}";
883
882
  const R2_DOWNLOAD_DECL_INJECT =
884
883
  " void sb_r2_transfer_abort(sb_r2_transfer_ctx*);\n" +
885
884
  " int sb_r2_transfer_download_open(const char*, const char*, sb_r2_transfer_ctx**);\n" +
@@ -887,7 +886,8 @@ const R2_DOWNLOAD_DECL_INJECT =
887
886
  " const char* sb_r2_transfer_download_etag(sb_r2_transfer_ctx*);\n" +
888
887
  " size_t sb_r2_transfer_download_read(sb_r2_transfer_ctx*, size_t, char*, size_t);\n" +
889
888
  " void sb_r2_transfer_download_close(sb_r2_transfer_ctx*);\n}";
890
- const R2_DOWNLOAD_BRANCH_ANCHOR = ' if (method != "PUT") { respond_with_error(res, "405 Method Not Allowed", "R2 transfer requires PUT", true); return true; }';
889
+ const R2_DOWNLOAD_BRANCH_ANCHOR =
890
+ ' if (method != "PUT") { respond_with_error(res, "405 Method Not Allowed", "R2 transfer requires PUT", true); return true; }';
891
891
  const R2_DOWNLOAD_BRANCH_INJECT = ` // sb_r2_transfer_download_v1
892
892
  // sb_r2_transfer_range_v1
893
893
  // sb_r2_transfer_range_header_v2
@@ -920,7 +920,7 @@ const R2_DOWNLOAD_BRANCH_INJECT = ` // sb_r2_transfer_download_v1
920
920
  struct Download { sb_r2_transfer_ctx* ctx; uintmax_t base; uintmax_t size; uintmax_t sent = 0; bool closed = false; char chunk[65536]; };
921
921
  auto state = std::make_shared<Download>(Download{download, first, last - first + 1});
922
922
  auto pump = std::make_shared<std::function<bool()>>();
923
- *pump = [res, state, pump]() {
923
+ *pump = [res, state]() {
924
924
  if (state->closed) return false;
925
925
  for (;;) {
926
926
  const size_t want = state->size - state->sent < sizeof(state->chunk) ? (size_t)(state->size - state->sent) : sizeof(state->chunk);
@@ -930,11 +930,14 @@ const R2_DOWNLOAD_BRANCH_INJECT = ` // sb_r2_transfer_download_v1
930
930
  if (!result.first) {
931
931
  res->onWritable([res, state](uintmax_t offset) {
932
932
  if (state->closed) return true;
933
- const size_t want = state->size - offset < sizeof(state->chunk) ? (size_t)(state->size - offset) : sizeof(state->chunk);
934
- const size_t bytes = sb_r2_transfer_download_read(state->ctx, (size_t)(state->base + offset), state->chunk, want);
935
- const auto resumed = res->tryEnd(std::string_view(state->chunk, bytes), state->size);
936
- if (resumed.second || bytes == 0) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return true; }
937
- return resumed.first;
933
+ for (;;) {
934
+ const size_t want = state->size - offset < sizeof(state->chunk) ? (size_t)(state->size - offset) : sizeof(state->chunk);
935
+ const size_t bytes = sb_r2_transfer_download_read(state->ctx, (size_t)(state->base + offset), state->chunk, want);
936
+ const auto resumed = res->tryEnd(std::string_view(state->chunk, bytes), state->size);
937
+ if (resumed.second || bytes == 0) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return true; }
938
+ if (!resumed.first) return false;
939
+ offset += bytes;
940
+ }
938
941
  });
939
942
  return false;
940
943
  }
@@ -953,22 +956,67 @@ const done = new Set<string>();
953
956
 
954
957
  /** Edits to Porffor's uWebSockets shim: `[marker, anchor, inject, what]`. */
955
958
  const UWS_EDITS = [
956
- [HTTP10_SHIM_MARKER, HTTP10_SHIM_ANCHOR, HTTP10_SHIM_INJECT, "shim rebuild for HTTP/1.0 (#236)"],
959
+ [
960
+ HTTP10_SHIM_MARKER,
961
+ HTTP10_SHIM_ANCHOR,
962
+ HTTP10_SHIM_INJECT,
963
+ "shim rebuild for HTTP/1.0 (#236)",
964
+ ],
957
965
  [BODY_MARKER, BODY_ANCHOR, BODY_INJECT, "request body limit"],
958
- [STATUS_SIG_MARKER, STATUS_SIG_ANCHOR, STATUS_SIG_INJECT, "status-line return type (#156)"],
959
- [STATUS_303_MARKER, STATUS_303_ANCHOR, STATUS_303_INJECT, "status-line 303 case (#156)"],
966
+ [
967
+ STATUS_SIG_MARKER,
968
+ STATUS_SIG_ANCHOR,
969
+ STATUS_SIG_INJECT,
970
+ "status-line return type (#156)",
971
+ ],
972
+ [
973
+ STATUS_303_MARKER,
974
+ STATUS_303_ANCHOR,
975
+ STATUS_303_INJECT,
976
+ "status-line 303 case (#156)",
977
+ ],
960
978
  [
961
979
  STATUS_DEFAULT_MARKER,
962
980
  STATUS_DEFAULT_ANCHOR,
963
981
  STATUS_DEFAULT_INJECT,
964
982
  "status-line fallback (#156)",
965
983
  ],
966
- [HDR_SIG_MARKER, HDR_SIG_ANCHOR, HDR_SIG_INJECT, "collect_headers res arg (#163)"],
967
- [HDR_CALL_MARKER, HDR_CALL_ANCHOR, HDR_CALL_INJECT, "collect_headers call site (#163)"],
968
- [HDR_CAP_MARKER, HDR_CAP_ANCHOR, HDR_CAP_INJECT, "remote-addr header capacity (#163)"],
969
- [HDR_SKIP_MARKER, HDR_SKIP_ANCHOR, HDR_SKIP_INJECT, "drop client-sent x-sb-remote-addr (#163)"],
970
- [HDR_APPEND_MARKER, HDR_APPEND_ANCHOR, HDR_APPEND_INJECT, "append x-sb-remote-addr (#163)"],
971
- [READ_RAW_DECL_MARKER, READ_RAW_DECL_ANCHOR, READ_RAW_DECL_INJECT, "read_raw_bytes decl (#176)"],
984
+ [
985
+ HDR_SIG_MARKER,
986
+ HDR_SIG_ANCHOR,
987
+ HDR_SIG_INJECT,
988
+ "collect_headers res arg (#163)",
989
+ ],
990
+ [
991
+ HDR_CALL_MARKER,
992
+ HDR_CALL_ANCHOR,
993
+ HDR_CALL_INJECT,
994
+ "collect_headers call site (#163)",
995
+ ],
996
+ [
997
+ HDR_CAP_MARKER,
998
+ HDR_CAP_ANCHOR,
999
+ HDR_CAP_INJECT,
1000
+ "remote-addr header capacity (#163)",
1001
+ ],
1002
+ [
1003
+ HDR_SKIP_MARKER,
1004
+ HDR_SKIP_ANCHOR,
1005
+ HDR_SKIP_INJECT,
1006
+ "drop client-sent x-sb-remote-addr (#163)",
1007
+ ],
1008
+ [
1009
+ HDR_APPEND_MARKER,
1010
+ HDR_APPEND_ANCHOR,
1011
+ HDR_APPEND_INJECT,
1012
+ "append x-sb-remote-addr (#163)",
1013
+ ],
1014
+ [
1015
+ READ_RAW_DECL_MARKER,
1016
+ READ_RAW_DECL_ANCHOR,
1017
+ READ_RAW_DECL_INJECT,
1018
+ "read_raw_bytes decl (#176)",
1019
+ ],
972
1020
  [
973
1021
  FORBIDDEN_HDR_MARKER,
974
1022
  FORBIDDEN_HDR_ANCHOR,
@@ -981,8 +1029,18 @@ const UWS_EDITS = [
981
1029
  WRITE_RESPONSE_INJECT,
982
1030
  "detect x-sb-raw-body, steer the body read (#176)",
983
1031
  ],
984
- [R2_TRANSFER_MARKER, R2_TRANSFER_ANCHOR, R2_TRANSFER_INJECT, "native direct R2 transfer ingress (#195)"],
985
- [R2_TRANSFER_CALL_MARKER, R2_TRANSFER_CALL_ANCHOR, R2_TRANSFER_CALL_INJECT, "direct R2 transfer dispatch (#195)"],
1032
+ [
1033
+ R2_TRANSFER_MARKER,
1034
+ R2_TRANSFER_ANCHOR,
1035
+ R2_TRANSFER_INJECT,
1036
+ "native direct R2 transfer ingress (#195)",
1037
+ ],
1038
+ [
1039
+ R2_TRANSFER_CALL_MARKER,
1040
+ R2_TRANSFER_CALL_ANCHOR,
1041
+ R2_TRANSFER_CALL_INJECT,
1042
+ "direct R2 transfer dispatch (#195)",
1043
+ ],
986
1044
  ] as const;
987
1045
 
988
1046
  /** The uWebSockets shim source: body limit + the #156 status-line fix. Exported for tests. */
@@ -993,15 +1051,27 @@ export async function patchUwebsockets(root: string): Promise<void> {
993
1051
  // Upgrade already-patched caches from the initial upload-only bridge. New
994
1052
  // caches receive the full bridge below; old ones need declarations plus the
995
1053
  // GET dispatch grafted onto their existing transfer handler.
996
- if (src.includes(R2_TRANSFER_MARKER) && !src.includes(R2_DOWNLOAD_CACHE_MARKER)) {
997
- if (!src.includes(R2_DOWNLOAD_DECL_ANCHOR) || !src.includes(R2_DOWNLOAD_BRANCH_ANCHOR))
998
- throw new Error(`could not upgrade Porffor's direct R2 transfer bridge: anchor not found in ${file}.`);
1054
+ if (
1055
+ src.includes(R2_TRANSFER_MARKER) &&
1056
+ !src.includes(R2_DOWNLOAD_CACHE_MARKER)
1057
+ ) {
1058
+ if (
1059
+ !src.includes(R2_DOWNLOAD_DECL_ANCHOR) ||
1060
+ !src.includes(R2_DOWNLOAD_BRANCH_ANCHOR)
1061
+ )
1062
+ throw new Error(
1063
+ `could not upgrade Porffor's direct R2 transfer bridge: anchor not found in ${file}.`,
1064
+ );
999
1065
  src = src.replace(R2_DOWNLOAD_DECL_ANCHOR, R2_DOWNLOAD_DECL_INJECT);
1000
1066
  src = src.replace(R2_DOWNLOAD_BRANCH_ANCHOR, R2_DOWNLOAD_BRANCH_INJECT);
1001
1067
  changed = true;
1002
1068
  }
1003
- if (src.includes(R2_DOWNLOAD_CACHE_MARKER) && !src.includes(R2_RANGE_CACHE_MARKER)) {
1004
- const stateAnchor = " 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)});";
1069
+ if (
1070
+ src.includes(R2_DOWNLOAD_CACHE_MARKER) &&
1071
+ !src.includes(R2_RANGE_CACHE_MARKER)
1072
+ ) {
1073
+ const stateAnchor =
1074
+ " 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)});";
1005
1075
  const range = ` const uintmax_t full_size = sb_r2_transfer_download_size(download);
1006
1076
  uintmax_t first = 0, last = full_size ? full_size - 1 : 0;
1007
1077
  const std::string_view range = req->getHeader("range");
@@ -1017,18 +1087,36 @@ export async function patchUwebsockets(root: string): Promise<void> {
1017
1087
  res->writeStatus("206 Partial Content"); res->writeHeader("Content-Range", content_range);
1018
1088
  }
1019
1089
  `;
1020
- const state = " 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});";
1021
- if (!src.includes(stateAnchor)) throw new Error(`could not upgrade Porffor's direct R2 range bridge: anchor not found in ${file}.`);
1022
- src = src.replace(" // sb_r2_transfer_download_v1\n", " // sb_r2_transfer_download_v1\n // sb_r2_transfer_range_v1\n");
1090
+ const state =
1091
+ " 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});";
1092
+ if (!src.includes(stateAnchor))
1093
+ throw new Error(
1094
+ `could not upgrade Porffor's direct R2 range bridge: anchor not found in ${file}.`,
1095
+ );
1096
+ src = src.replace(
1097
+ " // sb_r2_transfer_download_v1\n",
1098
+ " // sb_r2_transfer_download_v1\n // sb_r2_transfer_range_v1\n",
1099
+ );
1023
1100
  src = src.replace(stateAnchor, range + state);
1024
- src = src.replace("sb_r2_transfer_download_read(state->ctx, (size_t)offset, chunk, sizeof(chunk))", "sb_r2_transfer_download_read(state->ctx, (size_t)(state->base + offset), chunk, sizeof(chunk))");
1101
+ src = src.replace(
1102
+ "sb_r2_transfer_download_read(state->ctx, (size_t)offset, chunk, sizeof(chunk))",
1103
+ "sb_r2_transfer_download_read(state->ctx, (size_t)(state->base + offset), chunk, sizeof(chunk))",
1104
+ );
1025
1105
  changed = true;
1026
1106
  }
1027
- if (src.includes(R2_RANGE_CACHE_MARKER) && !src.includes(R2_CONDITIONAL_CACHE_MARKER)) {
1028
- const declaration = " size_t sb_r2_transfer_download_size(sb_r2_transfer_ctx*);\n";
1029
- const declarationReplacement = declaration + " const char* sb_r2_transfer_download_etag(sb_r2_transfer_ctx*);\n";
1030
- const anchor = " const uintmax_t full_size = sb_r2_transfer_download_size(download);\n";
1031
- const inject = ` // sb_r2_transfer_conditional_v1
1107
+ if (
1108
+ src.includes(R2_RANGE_CACHE_MARKER) &&
1109
+ !src.includes(R2_CONDITIONAL_CACHE_MARKER)
1110
+ ) {
1111
+ const declaration =
1112
+ " size_t sb_r2_transfer_download_size(sb_r2_transfer_ctx*);\n";
1113
+ const declarationReplacement =
1114
+ declaration +
1115
+ " const char* sb_r2_transfer_download_etag(sb_r2_transfer_ctx*);\n";
1116
+ const anchor =
1117
+ " const uintmax_t full_size = sb_r2_transfer_download_size(download);\n";
1118
+ const inject =
1119
+ ` // sb_r2_transfer_conditional_v1
1032
1120
  const std::string etag = sb_r2_transfer_download_etag(download);
1033
1121
  res->writeHeader("ETag", etag); res->writeHeader("Accept-Ranges", "bytes");
1034
1122
  const std::string_view if_none_match = req->getHeader("if-none-match");
@@ -1036,11 +1124,19 @@ export async function patchUwebsockets(root: string): Promise<void> {
1036
1124
  sb_r2_transfer_download_close(download); res->writeStatus("304 Not Modified"); res->end(); return true;
1037
1125
  }
1038
1126
  ` + anchor;
1039
- if (!src.includes(declaration) || !src.includes(anchor)) throw new Error(`could not upgrade Porffor's direct R2 conditional bridge: anchor not found in ${file}.`);
1040
- src = src.replace(declaration, declarationReplacement).replace(anchor, inject);
1127
+ if (!src.includes(declaration) || !src.includes(anchor))
1128
+ throw new Error(
1129
+ `could not upgrade Porffor's direct R2 conditional bridge: anchor not found in ${file}.`,
1130
+ );
1131
+ src = src
1132
+ .replace(declaration, declarationReplacement)
1133
+ .replace(anchor, inject);
1041
1134
  changed = true;
1042
1135
  }
1043
- if (src.includes(R2_CONDITIONAL_CACHE_MARKER) && !src.includes(R2_STREAMING_CACHE_MARKER)) {
1136
+ if (
1137
+ src.includes(R2_CONDITIONAL_CACHE_MARKER) &&
1138
+ !src.includes(R2_STREAMING_CACHE_MARKER)
1139
+ ) {
1044
1140
  const oldPump = ` const uintmax_t offset = res->getWriteOffset();
1045
1141
  char chunk[65536];
1046
1142
  const size_t bytes = sb_r2_transfer_download_read(state->ctx, (size_t)(state->base + offset), chunk, sizeof(chunk));
@@ -1055,15 +1151,28 @@ export async function patchUwebsockets(root: string): Promise<void> {
1055
1151
  if (result.second) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return false; }
1056
1152
  if (!result.first) { res->onWritable([pump](uintmax_t) { return (*pump)(); }); return false; }
1057
1153
  }`;
1058
- if (!src.includes(oldPump)) throw new Error(`could not upgrade Porffor's direct R2 streaming bridge: anchor not found in ${file}.`);
1059
- src = src.replace(" // sb_r2_transfer_conditional_v1\n", " // sb_r2_transfer_conditional_v1\n // sb_r2_transfer_streaming_v2\n").replace(oldPump, newPump);
1154
+ if (!src.includes(oldPump))
1155
+ throw new Error(
1156
+ `could not upgrade Porffor's direct R2 streaming bridge: anchor not found in ${file}.`,
1157
+ );
1158
+ src = src
1159
+ .replace(
1160
+ " // sb_r2_transfer_conditional_v1\n",
1161
+ " // sb_r2_transfer_conditional_v1\n // sb_r2_transfer_streaming_v2\n",
1162
+ )
1163
+ .replace(oldPump, newPump);
1060
1164
  changed = true;
1061
1165
  }
1062
1166
  // A short-lived v2 cache marker was written before its pump fix. Repair
1063
1167
  // those caches as well as applying the current form to future upgrades.
1064
- if (src.includes("state->base + offset") && src.includes("uintmax_t size; bool closed = false;")) {
1065
- const oldState = "struct Download { sb_r2_transfer_ctx* ctx; uintmax_t base; uintmax_t size; bool closed = false; };";
1066
- const newState = "struct Download { sb_r2_transfer_ctx* ctx; uintmax_t base; uintmax_t size; uintmax_t sent = 0; bool closed = false; };";
1168
+ if (
1169
+ src.includes("state->base + offset") &&
1170
+ src.includes("uintmax_t size; bool closed = false;")
1171
+ ) {
1172
+ const oldState =
1173
+ "struct Download { sb_r2_transfer_ctx* ctx; uintmax_t base; uintmax_t size; bool closed = false; };";
1174
+ const newState =
1175
+ "struct Download { sb_r2_transfer_ctx* ctx; uintmax_t base; uintmax_t size; uintmax_t sent = 0; bool closed = false; };";
1067
1176
  const oldPump = ` const uintmax_t offset = res->getWriteOffset(); char chunk[65536];
1068
1177
  const size_t bytes = sb_r2_transfer_download_read(state->ctx, (size_t)(state->base + offset), chunk, sizeof(chunk));
1069
1178
  const auto result = res->tryEnd(std::string_view(chunk, bytes), state->size);
@@ -1077,52 +1186,130 @@ export async function patchUwebsockets(root: string): Promise<void> {
1077
1186
  if (!result.first) { res->onWritable([pump, state](uintmax_t offset) { state->sent = offset; return (*pump)(); }); return false; }
1078
1187
  state->sent += bytes;
1079
1188
  }`;
1080
- if (!src.includes(oldState) || !src.includes(oldPump)) throw new Error(`could not repair Porffor's direct R2 streaming bridge: anchor not found in ${file}.`);
1189
+ if (!src.includes(oldState) || !src.includes(oldPump))
1190
+ throw new Error(
1191
+ `could not repair Porffor's direct R2 streaming bridge: anchor not found in ${file}.`,
1192
+ );
1081
1193
  src = src.replace(oldState, newState).replace(oldPump, newPump);
1082
1194
  changed = true;
1083
1195
  }
1084
- const duplicatePump = " for (;;) {\n char chunk[65536];\n for (;;) {\n";
1196
+ const duplicatePump =
1197
+ " for (;;) {\n char chunk[65536];\n for (;;) {\n";
1085
1198
  if (src.includes(duplicatePump)) {
1086
- src = src.replace(duplicatePump, " for (;;) {\n char chunk[65536];\n");
1199
+ src = src.replace(
1200
+ duplicatePump,
1201
+ " for (;;) {\n char chunk[65536];\n",
1202
+ );
1087
1203
  changed = true;
1088
1204
  }
1089
1205
  const unclosedPump = " state->sent += bytes;\n };\n";
1090
1206
  if (src.includes(unclosedPump)) {
1091
- src = src.replace(unclosedPump, " state->sent += bytes;\n }\n };\n");
1207
+ src = src.replace(
1208
+ unclosedPump,
1209
+ " state->sent += bytes;\n }\n };\n",
1210
+ );
1092
1211
  changed = true;
1093
1212
  }
1094
- if (src.includes(R2_STREAMING_CACHE_MARKER) && !src.includes(R2_RANGE_HEADER_CACHE_MARKER)) {
1095
- const rangeHeader = " const std::string_view range = req->getHeader(\"range\");\n";
1096
- if (!src.includes(rangeHeader)) throw new Error(`could not upgrade Porffor's direct R2 range-header bridge: anchor not found in ${file}.`);
1097
- src = src.replace(" // sb_r2_transfer_range_v1\n", " // sb_r2_transfer_range_v1\n // sb_r2_transfer_range_header_v2\n");
1098
- src = src.replace(rangeHeader, " std::string_view range = req->getHeader(\"range\"); if (range.empty()) range = req->getHeader(\"Range\");\n");
1213
+ if (
1214
+ src.includes(R2_STREAMING_CACHE_MARKER) &&
1215
+ !src.includes(R2_RANGE_HEADER_CACHE_MARKER)
1216
+ ) {
1217
+ const rangeHeader =
1218
+ ' const std::string_view range = req->getHeader("range");\n';
1219
+ if (!src.includes(rangeHeader))
1220
+ throw new Error(
1221
+ `could not upgrade Porffor's direct R2 range-header bridge: anchor not found in ${file}.`,
1222
+ );
1223
+ src = src.replace(
1224
+ " // sb_r2_transfer_range_v1\n",
1225
+ " // sb_r2_transfer_range_v1\n // sb_r2_transfer_range_header_v2\n",
1226
+ );
1227
+ src = src.replace(
1228
+ rangeHeader,
1229
+ ' std::string_view range = req->getHeader("range"); if (range.empty()) range = req->getHeader("Range");\n',
1230
+ );
1099
1231
  changed = true;
1100
1232
  }
1101
- const earlyConditionalHeaders = " res->writeHeader(\"ETag\", etag); res->writeHeader(\"Accept-Ranges\", \"bytes\");\n";
1102
- if (src.includes(earlyConditionalHeaders) && src.indexOf(earlyConditionalHeaders) < src.indexOf(" const uintmax_t full_size = sb_r2_transfer_download_size(download);")) {
1103
- const old304 = " sb_r2_transfer_download_close(download); res->writeStatus(\"304 Not Modified\"); res->end(); return true;";
1104
- const new304 = " sb_r2_transfer_download_close(download); res->writeStatus(\"304 Not Modified\"); res->writeHeader(\"ETag\", etag); res->end(); return true;";
1105
- const rangeEnd = " res->writeStatus(\"206 Partial Content\"); res->writeHeader(\"Content-Range\", content_range);\n }\n";
1106
- if (!src.includes(old304) || !src.includes(rangeEnd)) throw new Error(`could not repair Porffor's direct R2 response ordering: anchor not found in ${file}.`);
1107
- src = src.replace(earlyConditionalHeaders, "").replace(old304, new304).replace(rangeEnd, rangeEnd + " res->writeHeader(\"ETag\", etag); res->writeHeader(\"Accept-Ranges\", \"bytes\");\n");
1233
+ const earlyConditionalHeaders =
1234
+ ' res->writeHeader("ETag", etag); res->writeHeader("Accept-Ranges", "bytes");\n';
1235
+ if (
1236
+ src.includes(earlyConditionalHeaders) &&
1237
+ src.indexOf(earlyConditionalHeaders) <
1238
+ src.indexOf(
1239
+ " const uintmax_t full_size = sb_r2_transfer_download_size(download);",
1240
+ )
1241
+ ) {
1242
+ const old304 =
1243
+ ' sb_r2_transfer_download_close(download); res->writeStatus("304 Not Modified"); res->end(); return true;';
1244
+ const new304 =
1245
+ ' sb_r2_transfer_download_close(download); res->writeStatus("304 Not Modified"); res->writeHeader("ETag", etag); res->end(); return true;';
1246
+ const rangeEnd =
1247
+ ' res->writeStatus("206 Partial Content"); res->writeHeader("Content-Range", content_range);\n }\n';
1248
+ if (!src.includes(old304) || !src.includes(rangeEnd))
1249
+ throw new Error(
1250
+ `could not repair Porffor's direct R2 response ordering: anchor not found in ${file}.`,
1251
+ );
1252
+ src = src
1253
+ .replace(earlyConditionalHeaders, "")
1254
+ .replace(old304, new304)
1255
+ .replace(
1256
+ rangeEnd,
1257
+ rangeEnd +
1258
+ ' res->writeHeader("ETag", etag); res->writeHeader("Accept-Ranges", "bytes");\n',
1259
+ );
1108
1260
  changed = true;
1109
1261
  }
1110
1262
  if (src.includes("state->sent") && src.includes(R2_DOWNLOAD_CACHE_MARKER)) {
1111
- const stateStart = src.indexOf(" struct Download {", src.indexOf(R2_DOWNLOAD_CACHE_MARKER));
1112
- const stateEnd = src.indexOf(" res->writeHeader(\"Content-Type\"", stateStart);
1113
- const freshStart = R2_DOWNLOAD_BRANCH_INJECT.indexOf(" struct Download {");
1114
- const freshEnd = R2_DOWNLOAD_BRANCH_INJECT.indexOf(" res->writeHeader(\"Content-Type\"", freshStart);
1115
- if (stateStart < 0 || stateEnd < 0 || freshStart < 0 || freshEnd < 0) throw new Error(`could not replace Porffor's direct R2 stream pump: anchor not found in ${file}.`);
1116
- src = src.slice(0, stateStart) + R2_DOWNLOAD_BRANCH_INJECT.slice(freshStart, freshEnd) + src.slice(stateEnd);
1263
+ const stateStart = src.indexOf(
1264
+ " struct Download {",
1265
+ src.indexOf(R2_DOWNLOAD_CACHE_MARKER),
1266
+ );
1267
+ const stateEnd = src.indexOf(
1268
+ ' res->writeHeader("Content-Type"',
1269
+ stateStart,
1270
+ );
1271
+ const freshStart = R2_DOWNLOAD_BRANCH_INJECT.indexOf(
1272
+ " struct Download {",
1273
+ );
1274
+ const freshEnd = R2_DOWNLOAD_BRANCH_INJECT.indexOf(
1275
+ ' res->writeHeader("Content-Type"',
1276
+ freshStart,
1277
+ );
1278
+ if (stateStart < 0 || stateEnd < 0 || freshStart < 0 || freshEnd < 0)
1279
+ throw new Error(
1280
+ `could not replace Porffor's direct R2 stream pump: anchor not found in ${file}.`,
1281
+ );
1282
+ src =
1283
+ src.slice(0, stateStart) +
1284
+ R2_DOWNLOAD_BRANCH_INJECT.slice(freshStart, freshEnd) +
1285
+ src.slice(stateEnd);
1117
1286
  changed = true;
1118
1287
  }
1119
- if (src.includes("writable_offset") && src.includes(R2_DOWNLOAD_CACHE_MARKER)) {
1120
- const stateStart = src.indexOf(" struct Download {", src.indexOf(R2_DOWNLOAD_CACHE_MARKER));
1121
- const stateEnd = src.indexOf(" res->writeHeader(\"Content-Type\"", stateStart);
1288
+ if (
1289
+ src.includes("writable_offset") &&
1290
+ src.includes(R2_DOWNLOAD_CACHE_MARKER)
1291
+ ) {
1292
+ const stateStart = src.indexOf(
1293
+ " struct Download {",
1294
+ src.indexOf(R2_DOWNLOAD_CACHE_MARKER),
1295
+ );
1296
+ const stateEnd = src.indexOf(
1297
+ ' res->writeHeader("Content-Type"',
1298
+ stateStart,
1299
+ );
1122
1300
  const freshStart = R2_TRANSFER_INJECT.indexOf(" struct Download {");
1123
- const freshEnd = R2_TRANSFER_INJECT.indexOf(" res->writeHeader(\"Content-Type\"", freshStart);
1124
- if (stateStart < 0 || stateEnd < 0 || freshStart < 0 || freshEnd < 0) throw new Error(`could not restore Porffor's direct R2 stream pump: anchor not found in ${file}.`);
1125
- src = src.slice(0, stateStart) + R2_TRANSFER_INJECT.slice(freshStart, freshEnd) + src.slice(stateEnd);
1301
+ const freshEnd = R2_TRANSFER_INJECT.indexOf(
1302
+ ' res->writeHeader("Content-Type"',
1303
+ freshStart,
1304
+ );
1305
+ if (stateStart < 0 || stateEnd < 0 || freshStart < 0 || freshEnd < 0)
1306
+ throw new Error(
1307
+ `could not restore Porffor's direct R2 stream pump: anchor not found in ${file}.`,
1308
+ );
1309
+ src =
1310
+ src.slice(0, stateStart) +
1311
+ R2_TRANSFER_INJECT.slice(freshStart, freshEnd) +
1312
+ src.slice(stateEnd);
1126
1313
  changed = true;
1127
1314
  }
1128
1315
  for (const [marker, anchor, inject, what] of UWS_EDITS) {
@@ -1140,14 +1327,20 @@ export async function patchUwebsockets(root: string): Promise<void> {
1140
1327
  }
1141
1328
 
1142
1329
  const CLASS_SELF_IMPORT_ANCHOR = "import link from './modules.js';";
1143
- const CLASS_SELF_IMPORT_INJECT = "import link from './modules.js';\nimport sbClassSelf from './sb-class-self.js';";
1144
- const CLASS_SELF_CALL_ANCHOR = " if (ast._ts) globalThis.typedInput = Prefs.optTypes;";
1145
- const CLASS_SELF_CALL_INJECT = " sbClassSelf(ast); // sproutboat #256\n if (ast._ts) globalThis.typedInput = Prefs.optTypes;";
1330
+ const CLASS_SELF_IMPORT_INJECT =
1331
+ "import link from './modules.js';\nimport sbClassSelf from './sb-class-self.js';";
1332
+ const CLASS_SELF_CALL_ANCHOR =
1333
+ " if (ast._ts) globalThis.typedInput = Prefs.optTypes;";
1334
+ const CLASS_SELF_CALL_INJECT =
1335
+ " sbClassSelf(ast); // sproutboat #256\n if (ast._ts) globalThis.typedInput = Prefs.optTypes;";
1146
1336
  const CLASS_SELF_MARKER = "sproutboat #256";
1147
1337
 
1148
1338
  /** #256 — see CLASS_SELF_HELPER. */
1149
1339
  export async function patchClassSelfName(root: string): Promise<void> {
1150
- await writeFile(resolve(root, "compiler/sb-class-self.js"), CLASS_SELF_HELPER);
1340
+ await writeFile(
1341
+ resolve(root, "compiler/sb-class-self.js"),
1342
+ CLASS_SELF_HELPER,
1343
+ );
1151
1344
  const file = resolve(root, "compiler/parse.js");
1152
1345
  let src = await readFile(file, "utf8");
1153
1346
  if (src.includes(CLASS_SELF_MARKER)) return;
@@ -1155,9 +1348,14 @@ export async function patchClassSelfName(root: string): Promise<void> {
1155
1348
  [CLASS_SELF_IMPORT_ANCHOR, "import"],
1156
1349
  [CLASS_SELF_CALL_ANCHOR, "call"],
1157
1350
  ] as const) {
1158
- if (!src.includes(anchor)) throw new Error(`could not patch Porffor's class self-name rewrite (${what}): anchor not found in ${file}`);
1351
+ if (!src.includes(anchor))
1352
+ throw new Error(
1353
+ `could not patch Porffor's class self-name rewrite (${what}): anchor not found in ${file}`,
1354
+ );
1159
1355
  }
1160
- src = src.replace(CLASS_SELF_IMPORT_ANCHOR, CLASS_SELF_IMPORT_INJECT).replace(CLASS_SELF_CALL_ANCHOR, CLASS_SELF_CALL_INJECT);
1356
+ src = src
1357
+ .replace(CLASS_SELF_IMPORT_ANCHOR, CLASS_SELF_IMPORT_INJECT)
1358
+ .replace(CLASS_SELF_CALL_ANCHOR, CLASS_SELF_CALL_INJECT);
1161
1359
  await writeFile(file, src);
1162
1360
  }
1163
1361
 
@@ -1169,9 +1367,24 @@ async function patchCompilerArgs(root: string): Promise<void> {
1169
1367
  for (const [marker, anchor, inject, what] of [
1170
1368
  [LINK_MARKER, LINK_ANCHOR, LINK_INJECT, "extra link args"],
1171
1369
  [CFLAGS_MARKER, CFLAGS_ANCHOR, CFLAGS_INJECT, "extra compiler flags"],
1172
- [FP_CONTRACT_MARKER, FP_CONTRACT_ANCHOR, FP_CONTRACT_INJECT, "floating-point contraction"],
1173
- [HTTP10_IMPORT_MARKER, HTTP10_IMPORT_ANCHOR, HTTP10_IMPORT_INJECT, "HTTP/1.0 helper import (#236)"],
1174
- [HTTP10_CALL_MARKER, HTTP10_CALL_ANCHOR, HTTP10_CALL_INJECT, "HTTP/1.0 parser edit (#236)"],
1370
+ [
1371
+ FP_CONTRACT_MARKER,
1372
+ FP_CONTRACT_ANCHOR,
1373
+ FP_CONTRACT_INJECT,
1374
+ "floating-point contraction",
1375
+ ],
1376
+ [
1377
+ HTTP10_IMPORT_MARKER,
1378
+ HTTP10_IMPORT_ANCHOR,
1379
+ HTTP10_IMPORT_INJECT,
1380
+ "HTTP/1.0 helper import (#236)",
1381
+ ],
1382
+ [
1383
+ HTTP10_CALL_MARKER,
1384
+ HTTP10_CALL_ANCHOR,
1385
+ HTTP10_CALL_INJECT,
1386
+ "HTTP/1.0 parser edit (#236)",
1387
+ ],
1175
1388
  ] as const) {
1176
1389
  if (src.includes(marker)) continue;
1177
1390
  const at = src.indexOf(anchor);
@@ -1185,7 +1398,9 @@ async function patchCompilerArgs(root: string): Promise<void> {
1185
1398
  // Porffor's module-build cache includes them in its compiler command.
1186
1399
  src =
1187
1400
  marker !== LINK_MARKER
1188
- ? src.slice(0, at + anchor.length) + inject + src.slice(at + anchor.length)
1401
+ ? src.slice(0, at + anchor.length) +
1402
+ inject +
1403
+ src.slice(at + anchor.length)
1189
1404
  : src.slice(0, at) + inject + src.slice(at);
1190
1405
  changed = true;
1191
1406
  }
@@ -1197,12 +1412,23 @@ export async function patchDateParser(root: string): Promise<void> {
1197
1412
  const src = await readFile(file, "utf8");
1198
1413
  if (src.includes(DATE_PARSER_MARKER)) return;
1199
1414
  const start = src.indexOf("export const __ecma262_ParseDTSF");
1200
- const end = src.indexOf("// RFC 7231 or Date.prototype.toString() parser", start);
1201
- if (start < 0 || end < 0) throw new Error(`could not find Porffor's ISO date parser in ${file}`);
1415
+ const end = src.indexOf(
1416
+ "// RFC 7231 or Date.prototype.toString() parser",
1417
+ start,
1418
+ );
1419
+ if (start < 0 || end < 0)
1420
+ throw new Error(`could not find Porffor's ISO date parser in ${file}`);
1202
1421
  const parser = src.slice(start, end);
1203
1422
  if (!parser.includes(DATE_PARSER_ANCHOR))
1204
- throw new Error(`could not patch Porffor's ISO date parser: anchor not found in ${file}`);
1205
- await writeFile(file, src.slice(0, start) + parser.replace(DATE_PARSER_ANCHOR, DATE_PARSER_INJECT) + src.slice(end));
1423
+ throw new Error(
1424
+ `could not patch Porffor's ISO date parser: anchor not found in ${file}`,
1425
+ );
1426
+ await writeFile(
1427
+ file,
1428
+ src.slice(0, start) +
1429
+ parser.replace(DATE_PARSER_ANCHOR, DATE_PARSER_INJECT) +
1430
+ src.slice(end),
1431
+ );
1206
1432
  }
1207
1433
 
1208
1434
  /** #168: keep a promise resolving to null out of the `then` probe. */
@@ -1211,7 +1437,9 @@ export async function patchPromiseResolveNull(root: string): Promise<void> {
1211
1437
  const src = await readFile(file, "utf8");
1212
1438
  if (src.includes(PROMISE_NULL_MARKER)) return;
1213
1439
  if (!src.includes(PROMISE_NULL_ANCHOR))
1214
- throw new Error(`could not patch Porffor's promise resolve for null: anchor not found in ${file}`);
1440
+ throw new Error(
1441
+ `could not patch Porffor's promise resolve for null: anchor not found in ${file}`,
1442
+ );
1215
1443
  await writeFile(file, src.replace(PROMISE_NULL_ANCHOR, PROMISE_NULL_INJECT));
1216
1444
  }
1217
1445
 
@@ -1221,51 +1449,55 @@ export async function patchReplaceAll(root: string): Promise<void> {
1221
1449
  let src = await readFile(file, "utf8");
1222
1450
  if (src.includes(REPLACE_ALL_MARKER)) return;
1223
1451
  const start = src.indexOf("export const __Porffor_string_replaceAll = ");
1224
- const end = src.indexOf("export const __String_prototype_replaceAll = ", start);
1225
- if (start < 0 || end < 0) throw new Error(`could not find Porffor's replaceAll in ${file}`);
1452
+ const end = src.indexOf(
1453
+ "export const __String_prototype_replaceAll = ",
1454
+ start,
1455
+ );
1456
+ if (start < 0 || end < 0)
1457
+ throw new Error(`could not find Porffor's replaceAll in ${file}`);
1226
1458
  let body = src.slice(start, end);
1227
1459
  for (const [anchor, inject] of REPLACE_ALL_EDITS) {
1228
- if (!body.includes(anchor)) throw new Error(`could not patch Porffor's replaceAll: anchor not found in ${file}`);
1460
+ if (!body.includes(anchor))
1461
+ throw new Error(
1462
+ `could not patch Porffor's replaceAll: anchor not found in ${file}`,
1463
+ );
1229
1464
  body = body.replace(anchor, inject);
1230
1465
  }
1231
1466
  src = src.slice(0, start) + body + src.slice(end);
1232
1467
  await writeFile(file, src);
1233
1468
  }
1234
1469
 
1235
- /** #238: modular typed-array stores. Before the precompile so builtins get it too. */
1236
- export async function patchTypedArrayStore(root: string): Promise<void> {
1237
- const file = resolve(root, "compiler/codegen.js");
1238
- let src = await readFile(file, "utf8");
1239
- let changed = false;
1240
- for (const [marker, anchor, inject, what] of [
1241
- [TA_GET_MARKER, TA_GET_ANCHOR, TA_GET_INJECT, "typed-array read bounds"],
1242
- [TA_SET_BOUNDS_MARKER, TA_SET_BOUNDS_ANCHOR, TA_SET_BOUNDS_INJECT, "typed-array write bounds"],
1243
- ] as const) {
1244
- if (src.includes(marker)) continue;
1245
- if (!src.includes(anchor)) throw new Error(`could not patch Porffor's ${what}: anchor not found in ${file}`);
1246
- src = src.replace(anchor, inject);
1247
- changed = true;
1248
- }
1249
- if (changed) await writeFile(file, src);
1250
- }
1251
-
1252
1470
  /** Register elided local function expressions so inference uses their lexical binding. */
1253
1471
  export async function patchLocalFunctionTypes(root: string): Promise<void> {
1254
1472
  const file = resolve(root, "compiler/codegen.js");
1255
1473
  let src = await readFile(file, "utf8");
1256
- const marker = "sproutboat: preserve lexical types for direct-only local functions";
1474
+ const marker =
1475
+ "sproutboat: preserve lexical types for direct-only local functions";
1257
1476
  if (src.includes(marker)) return;
1258
- const anchor = "if (!hoistRead && directCallOnlyFunctionBinding(scope, kind, name, pattern, func)) {\n return valUndefined();";
1259
- if (!src.includes(anchor)) throw new Error(`could not patch Porffor local function types: anchor not found in ${file}`);
1260
- src = src.replace(anchor,
1477
+ const anchor =
1478
+ "if (!hoistRead && directCallOnlyFunctionBinding(scope, kind, name, pattern, func)) {\n return valUndefined();";
1479
+ if (!src.includes(anchor))
1480
+ throw new Error(
1481
+ `could not patch Porffor local function types: anchor not found in ${file}`,
1482
+ );
1483
+ src = src.replace(
1484
+ anchor,
1261
1485
  "if (!hoistRead && directCallOnlyFunctionBinding(scope, kind, name, pattern, func)) {\n" +
1262
- ` // ${marker}\n` +
1263
- " bindNamedFunction(scope, name, func);\n return valUndefined();");
1486
+ ` // ${marker}\n` +
1487
+ " bindNamedFunction(scope, name, func);\n return valUndefined();",
1488
+ );
1264
1489
  // resolveNamedFunction already handles unambiguous global fallbacks. Falling
1265
1490
  // back again can infer a type from an unrelated same-named function.
1266
- const fallback = "const func = resolveNamedFunction(scope, name) ?? funcByName(name);";
1267
- if (!src.includes(fallback)) throw new Error(`could not patch Porffor local function type fallback: anchor not found in ${file}`);
1268
- src = src.replace(fallback, "const func = resolveNamedFunction(scope, name);");
1491
+ const fallback =
1492
+ "const func = resolveNamedFunction(scope, name) ?? funcByName(name);";
1493
+ if (!src.includes(fallback))
1494
+ throw new Error(
1495
+ `could not patch Porffor local function type fallback: anchor not found in ${file}`,
1496
+ );
1497
+ src = src.replace(
1498
+ fallback,
1499
+ "const func = resolveNamedFunction(scope, name);",
1500
+ );
1269
1501
  await writeFile(file, src);
1270
1502
  }
1271
1503
 
@@ -1278,10 +1510,16 @@ export async function patchUint8ArrayBase64(root: string): Promise<void> {
1278
1510
  const edits = [
1279
1511
  ["let alphabet: string =", "let alphabet: any ="],
1280
1512
  ["let lastChunkHandling: string =", "let lastChunkHandling: any ="],
1281
- ["const fullChunks: i32 = (len / 3) * 3;", "const fullChunks: i32 = ((len / 3) | 0) * 3;"],
1513
+ [
1514
+ "const fullChunks: i32 = (len / 3) * 3;",
1515
+ "const fullChunks: i32 = ((len / 3) | 0) * 3;",
1516
+ ],
1282
1517
  ] as const;
1283
1518
  for (const [anchor, replacement] of edits) {
1284
- if (!src.includes(anchor)) throw new Error(`could not patch Porffor base64: anchor not found in ${file}`);
1519
+ if (!src.includes(anchor))
1520
+ throw new Error(
1521
+ `could not patch Porffor base64: anchor not found in ${file}`,
1522
+ );
1285
1523
  src = src.replaceAll(anchor, replacement);
1286
1524
  }
1287
1525
  await writeFile(file, `// ${marker}\n${src}`);
@@ -1302,8 +1540,12 @@ export async function precompileBuiltins(root: string): Promise<void> {
1302
1540
  new Response(child.stdout).text(),
1303
1541
  new Response(child.stderr).text(),
1304
1542
  ]);
1305
- if (code !== 0) throw new Error(`could not precompile Porffor's patched builtins: ${stderr || stdout}`);
1306
- if ((await readFile(table)).length === 0) throw new Error(`Porffor precompile produced an empty ${table}`);
1543
+ if (code !== 0)
1544
+ throw new Error(
1545
+ `could not precompile Porffor's patched builtins: ${stderr || stdout}`,
1546
+ );
1547
+ if ((await readFile(table)).length === 0)
1548
+ throw new Error(`Porffor precompile produced an empty ${table}`);
1307
1549
  }
1308
1550
 
1309
1551
  /** The native-fetch server source: $PORT support + the #165 console sink. Exported for tests. */
@@ -1320,7 +1562,8 @@ export async function patchRenderJs(root: string): Promise<void> {
1320
1562
  "Porffor's native-fetch renderer changed. See https://github.com/baronunread/sproutboat/tree/main/patches/upstream.",
1321
1563
  );
1322
1564
  }
1323
- src = src.slice(0, at + anchor.length) + inject + src.slice(at + anchor.length);
1565
+ src =
1566
+ src.slice(0, at + anchor.length) + inject + src.slice(at + anchor.length);
1324
1567
  changed = true;
1325
1568
  }
1326
1569
 
@@ -1341,24 +1584,678 @@ export async function patchRenderJs(root: string): Promise<void> {
1341
1584
  if (changed) await writeFile(file, src);
1342
1585
  }
1343
1586
 
1587
+ /** Reclaim a burst's dead heap and oversized GC workspace after a quiet period. */
1588
+ export async function patchQuietCollection(root: string): Promise<void> {
1589
+ const renderFile = resolve(root, "compiler/render.js");
1590
+ const shimFile = resolve(root, "compiler/uwebsockets.js");
1591
+ let render = await readFile(renderFile, "utf8");
1592
+ let shim = await readFile(shimFile, "utf8");
1593
+ const marker = "// sproutboat: reclaim large idle heaps";
1594
+ if (!render.includes(marker)) {
1595
+ const start = render.indexOf(
1596
+ "void porf_native_fetch_collect_normal_from(void* stack_top) {",
1597
+ );
1598
+ const end = render.indexOf(
1599
+ "\nint porf_native_fetch_should_collect(void)",
1600
+ start,
1601
+ );
1602
+ if (start < 0 || end < 0)
1603
+ throw new Error("Porffor quiet-GC renderer anchor missing");
1604
+ const normal = render.slice(start, end);
1605
+ if (
1606
+ !normal.includes("porf_gc_collect_idle();") ||
1607
+ !normal.includes(" porf_c_stack_top = prev_stack_top;")
1608
+ )
1609
+ throw new Error("Porffor quiet-GC collector changed");
1610
+ const quiet = normal
1611
+ .replace("collect_normal_from", "collect_quiet_from")
1612
+ .replace(
1613
+ "if (porf_heap_base == 0) return;",
1614
+ "if (!porf_native_fetch_needs_quiet_collect()) return;",
1615
+ )
1616
+ .replace("porf_gc_collect_idle();", "porf_gc_collect(0);")
1617
+ .replace(
1618
+ " porf_c_stack_top = prev_stack_top;",
1619
+ ` // The mark queue is empty after collection; its capacity is scratch, not live roots.
1620
+ if (porf_gc_mark_queue_len == 0 && porf_gc_mark_queue_cap > 4096) {
1621
+ free(porf_gc_mark_queue);
1622
+ porf_gc_mark_queue = NULL;
1623
+ porf_gc_mark_queue_cap = 0;
1624
+ }
1625
+ porf_c_stack_top = prev_stack_top;`,
1626
+ );
1627
+ const needs = `int porf_native_fetch_needs_quiet_collect(void) {
1628
+ \${gcEnabled ? ' return porf_heap_base != 0 && ((u64)porf_heap_top - (u64)porf_heap_base > 8ull * 1024ull * 1024ull || porf_gc_mark_queue_cap > 65536);' : ' return 0;'}
1629
+ }
1630
+ `;
1631
+ render =
1632
+ render.slice(0, end) +
1633
+ "\n" +
1634
+ marker +
1635
+ "\n" +
1636
+ needs +
1637
+ quiet +
1638
+ render.slice(end);
1639
+ }
1640
+ if (!shim.includes(marker)) {
1641
+ const declaration =
1642
+ "void porf_native_fetch_collect_normal_from(void* stack_top);";
1643
+ const start = shim.indexOf("static void collect_after_request(void) {");
1644
+ const end = shim.indexOf("\n}\n", start);
1645
+ if (!shim.includes(declaration) || start < 0 || end < 0)
1646
+ throw new Error("Porffor quiet-GC native shim anchor missing");
1647
+ const original = shim.slice(start, end);
1648
+ if (
1649
+ !original.includes(
1650
+ "porf_native_fetch_collect_normal_from((void*)&porf_stack_anchor);",
1651
+ )
1652
+ )
1653
+ throw new Error("Porffor normal request collection changed");
1654
+ const timer = `${marker}
1655
+ static us_timer_t* sb_quiet_gc_timer = nullptr;
1656
+ static void sb_collect_quiet(struct us_timer_t*) {
1657
+ volatile int stack_anchor = 0;
1658
+ porf_native_fetch_collect_quiet_from((void*)&stack_anchor);
1659
+ }
1660
+ `;
1661
+ // Preserve normal request collection unconditionally; only the extra timer is guarded.
1662
+ const schedule = `
1663
+ if (porf_native_fetch_needs_quiet_collect()) {
1664
+ if (!sb_quiet_gc_timer) sb_quiet_gc_timer = us_create_timer((struct us_loop_t*)uWS::Loop::get(), 0, 0);
1665
+ if (sb_quiet_gc_timer) us_timer_set(sb_quiet_gc_timer, sb_collect_quiet, 500, 0);
1666
+ }`;
1667
+ shim = shim.slice(0, start) + timer + original + schedule + shim.slice(end);
1668
+ shim = shim.replace(
1669
+ declaration,
1670
+ declaration +
1671
+ "\nvoid porf_native_fetch_collect_quiet_from(void* stack_top);\nint porf_native_fetch_needs_quiet_collect(void);",
1672
+ );
1673
+ }
1674
+ await Promise.all([writeFile(renderFile, render), writeFile(shimFile, shim)]);
1675
+ }
1676
+
1344
1677
  /**
1345
1678
  * Apply every patch to a Porffor checkout at `root` (the directory that holds
1346
1679
  * `compiler/` and `runtime/`). Idempotent per process and per marker, so it is
1347
1680
  * safe to call on every build. Callers pass an explicit root — `ensurePorffor`
1348
1681
  * right after extraction, the compile path against its resolved checkout.
1349
1682
  */
1683
+ const RESPONSE_DEFAULTS_MARKER =
1684
+ "// sproutboat: Fetch default content type for text bodies";
1685
+ const RESPONSE_DEFAULTS_ANCHOR =
1686
+ "class Response {\n constructor(body = null, init = undefined) {\n this.body = body;\n this.status = Number(init?.status ?? 200);\n this.headers = new Headers(init?.headers);\n";
1687
+
1688
+ /**
1689
+ * Fetch's extract-a-body step gives a string body `text/plain;charset=UTF-8` unless the
1690
+ * caller set a content type (workerd does). Porffor's Response sent none, so
1691
+ * `new Response("hi")` reached clients untyped.
1692
+ */
1693
+ export async function patchResponseDefaults(root: string): Promise<void> {
1694
+ const file = resolve(root, "runtime/fetch-globals.js");
1695
+ const src = await readFile(file, "utf8");
1696
+ if (src.includes(RESPONSE_DEFAULTS_MARKER) || src.includes("sproutboat: Fetch-spec Request and Response")) return;
1697
+ if (!src.includes(RESPONSE_DEFAULTS_ANCHOR))
1698
+ throw new Error(
1699
+ `could not patch Porffor's Response defaults: anchor not found in ${file}`,
1700
+ );
1701
+ await writeFile(
1702
+ file,
1703
+ src.replace(
1704
+ RESPONSE_DEFAULTS_ANCHOR,
1705
+ RESPONSE_DEFAULTS_ANCHOR +
1706
+ ` ${RESPONSE_DEFAULTS_MARKER}\n` +
1707
+ " if (typeof body === 'string' && !this.headers.has('content-type')) this.headers.set('content-type', 'text/plain;charset=UTF-8');\n",
1708
+ ),
1709
+ );
1710
+ }
1711
+
1712
+ const URL_CLASS_START = "class URL {\n constructor(input) {\n";
1713
+ const URL_CLASS_END = "\nclass Request {";
1714
+ const URL_CLASS_MARKER = "sproutboat: WHATWG-style URL";
1715
+
1716
+ /**
1717
+ * Replaces Porffor's URL (href, origin, pathname and search only; no base argument, no
1718
+ * setters but pathname, no hash) with the WHATWG-style class in url-class.ts.
1719
+ */
1720
+ export async function patchURLClass(root: string): Promise<void> {
1721
+ const file = resolve(root, "runtime/fetch-globals.js");
1722
+ const src = await readFile(file, "utf8");
1723
+ if (src.includes(URL_CLASS_MARKER)) return;
1724
+ const start = src.indexOf(URL_CLASS_START);
1725
+ const end = start === -1 ? -1 : src.indexOf(URL_CLASS_END, start);
1726
+ if (end === -1)
1727
+ throw new Error(`could not patch Porffor's URL: anchor not found in ${file}`);
1728
+ await writeFile(file, src.slice(0, start) + URL_CLASS_SOURCE + src.slice(end + 1));
1729
+ }
1730
+
1731
+ const HEADERS_ARRAY_ANCHOR =
1732
+ " if (Porffor.type(init.entries) == Porffor.TYPES.function) {\n this._entries = Porffor.array.new(8);\n for (const entry of init.entries()) {";
1733
+ const HEADERS_ARRAY_INJECT =
1734
+ " if (!Array.isArray(init) && Porffor.type(init.entries) == Porffor.TYPES.function) {\n this._entries = Porffor.array.new(8);\n for (const entry of init.entries()) {";
1735
+
1736
+ /**
1737
+ * `new Headers([["a", "1"]])`: arrays also have entries() (yielding [index, pair]), and
1738
+ * Porffor checked that first, so a list of pairs became headers named "0", "1"...
1739
+ * Arrays now reach the sequence branch below it.
1740
+ */
1741
+ export async function patchHeadersArrayInit(root: string): Promise<void> {
1742
+ const file = resolve(root, "runtime/fetch-globals.js");
1743
+ const src = await readFile(file, "utf8");
1744
+ if (src.includes(HEADERS_ARRAY_INJECT) || src.includes("sproutboat: Fetch-spec Headers")) return;
1745
+ if (!src.includes(HEADERS_ARRAY_ANCHOR))
1746
+ throw new Error(
1747
+ `could not patch Porffor's Headers array init: anchor not found in ${file}`,
1748
+ );
1749
+ await writeFile(
1750
+ file,
1751
+ src.replace(HEADERS_ARRAY_ANCHOR, HEADERS_ARRAY_INJECT),
1752
+ );
1753
+ }
1754
+
1755
+ // baronunread/sproutboat#286: Map.groupBy is missing (Object.groupBy exists). Same
1756
+ // shape as __Object_groupBy, keyed by SameValueZero through the Map itself.
1757
+ const MAP_GROUPBY_MARKER = "sproutboat #286: Map.groupBy";
1758
+ const MAP_GROUPBY_SOURCE =
1759
+ "\n// sproutboat #286: Map.groupBy\n" +
1760
+ "export const __Map_groupBy = (items: any, callbackFn: any): Map => {\n" +
1761
+ " const out: Map = __Porffor_hashtableNew(true);\n" +
1762
+ " Porffor.IR.gcBarrier(out, Porffor.TYPES.map);\n" +
1763
+ " let i: i32 = 0;\n" +
1764
+ " for (const x of items) {\n" +
1765
+ " const k: any = callbackFn(x, i++);\n" +
1766
+ " if (!Porffor.callThis(__Map_prototype_has, out, k)) Porffor.callThis(__Map_prototype_set, out, k, Porffor.array.new(4));\n" +
1767
+ " Porffor.array.fastPush(Porffor.callThis(__Map_prototype_get, out, k), x);\n" +
1768
+ " }\n" +
1769
+ " return out;\n" +
1770
+ "};\n";
1771
+
1772
+ /** #286: add Map.groupBy. Before the precompile, which compiles it with the other builtins. */
1773
+ export async function patchMapGroupBy(root: string): Promise<void> {
1774
+ const file = resolve(root, "compiler/builtins/map.ts");
1775
+ const src = await readFile(file, "utf8");
1776
+ if (src.includes(MAP_GROUPBY_MARKER)) return;
1777
+ if (!src.includes("export const Map = function (iterable: any): Map {"))
1778
+ throw new Error(`could not patch Porffor's Map.groupBy: anchor not found in ${file}`);
1779
+ if (src.includes("export const __Map_groupBy"))
1780
+ throw new Error(`Porffor now defines Map.groupBy itself: drop patchMapGroupBy (${file})`);
1781
+ await writeFile(file, src.trimEnd() + "\n" + MAP_GROUPBY_SOURCE);
1782
+ }
1783
+
1784
+ // baronunread/sproutboat#286: a private field (#x) is stored as an ordinary property
1785
+ // named "__#x", installed like a public field, so Object.keys, for-in and
1786
+ // JSON.stringify listed it. Install private fields the way methods are installed:
1787
+ // writable and configurable but not enumerable. Object.getOwnPropertyNames still
1788
+ // sees the internal name.
1789
+ const PRIVATE_FIELD_MARKER = "sproutboat #286: private fields";
1790
+ const PRIVATE_FIELD_EDITS: [string, string][] = [
1791
+ [
1792
+ " builtinCall(func, '__Porffor_object_class_value', [\n",
1793
+ " // sproutboat #286: private fields are not enumerable\n" +
1794
+ " builtinCall(func, x.key?.type === 'PrivateIdentifier' ? '__Porffor_object_class_method' : '__Porffor_object_class_value', [\n",
1795
+ ],
1796
+ [
1797
+ " if (kind === 'get' || kind === 'set') initKind = kind;\n",
1798
+ " if (kind === 'get' || kind === 'set') initKind = kind;\n" +
1799
+ " if (initKind === 'value' && x.key?.type === 'PrivateIdentifier') initKind = 'method'; // sproutboat #286: private fields\n",
1800
+ ],
1801
+ ];
1802
+
1803
+ export async function patchPrivateFieldsHidden(root: string): Promise<void> {
1804
+ const file = resolve(root, "compiler/codegen.js");
1805
+ let src = await readFile(file, "utf8");
1806
+ if (src.includes(PRIVATE_FIELD_MARKER)) return;
1807
+ for (const [anchor, inject] of PRIVATE_FIELD_EDITS) {
1808
+ if (src.split(anchor).length !== 2)
1809
+ throw new Error(`could not patch Porffor's private fields: anchor not found in ${file}`);
1810
+ src = src.replace(anchor, inject);
1811
+ }
1812
+ await writeFile(file, src);
1813
+ }
1814
+
1815
+ // baronunread/sproutboat#287 (sproutd B9/B13): native-fetch mode runs frequent full
1816
+ // collections, and each returned its free pages to the OS (porf_gc_discard_free_runs),
1817
+ // so the next large allocation faulted them back in: allocation-heavy handlers ran up
1818
+ // to 4x slower than plain Porffor. Full collections under load now keep their free
1819
+ // pages, and the idle quiet collection returns them. The idle gate also opens when
1820
+ // anything was claimed or promoted since the last full collection, so a small sprout
1821
+ // that never reaches the 16 MiB / 8 MiB full-collection triggers still gives back its
1822
+ // promoted garbage. Same edits as sproutd's deferred discard and idle return.
1823
+ const DEFERRED_DISCARD_MARKER = "static i32 porf_gc_discard_pending = 0;";
1824
+ const DEFERRED_DISCARD_EDITS: [string, string][] = [
1825
+ [
1826
+ "static i32 porf_gc_mark_queue_cap = 0;",
1827
+ "static i32 porf_gc_mark_queue_cap = 0;\n// sproutboat #287: discard free runs only in the idle collection\nstatic i32 porf_gc_discard_now = 0;\n" +
1828
+ DEFERRED_DISCARD_MARKER,
1829
+ ],
1830
+ [
1831
+ " porf_gc_discard_free_runs();\n",
1832
+ " if (porf_gc_discard_now) porf_gc_discard_free_runs(); else porf_gc_discard_pending = 1;\n",
1833
+ ],
1834
+ [
1835
+ "porf_gc_mark_queue_cap > 65536);' : ' return 0;'",
1836
+ "porf_gc_mark_queue_cap > 65536 || porf_gc_discard_pending || porf_gc_claimed_since_full > 0 || porf_gc_promoted_since_full > 0);' : ' return 0;'",
1837
+ ],
1838
+ [
1839
+ " porf_c_stack_top = stack_top;\n porf_gc_collect(0);\n // The mark queue is empty",
1840
+ " porf_c_stack_top = stack_top;\n porf_gc_discard_now = 1;\n porf_gc_collect(0);\n porf_gc_discard_now = 0;\n porf_gc_discard_pending = 0;\n // The mark queue is empty",
1841
+ ],
1842
+ ];
1843
+
1844
+ /** #287: deferred discard and idle return. After patchQuietCollection, which adds the quiet collector. */
1845
+ export async function patchDeferredDiscard(root: string): Promise<void> {
1846
+ const file = resolve(root, "compiler/render.js");
1847
+ let src = await readFile(file, "utf8");
1848
+ if (src.includes(DEFERRED_DISCARD_MARKER)) return;
1849
+ for (const [anchor, inject] of DEFERRED_DISCARD_EDITS) {
1850
+ if (src.split(anchor).length !== 2)
1851
+ throw new Error(`could not patch Porffor's deferred discard: anchor not found in ${file}`);
1852
+ src = src.replace(anchor, inject);
1853
+ }
1854
+ await writeFile(file, src);
1855
+ }
1856
+
1857
+ // sproutd B16: String.prototype.repeat copied the source once per repetition, so
1858
+ // "ab".repeat(300000) made 300,000 two-byte copies (about 60 MB/s; a proof fixture
1859
+ // spent 64% of its load time here). Copy once, then double the filled prefix onto
1860
+ // itself: log2(count) copies. Same for the UTF-16 and Latin-1 variants, which
1861
+ // share the loop text.
1862
+ const REPEAT_LOOP =
1863
+ " for (let i: i32 = 0; i < count; i++) {\n" +
1864
+ " Porffor.IR.copy(Porffor.IR.ptr(out) + 4 + i * thisLen, Porffor.IR.ptr(this) + 4, thisLen);\n" +
1865
+ " }\n";
1866
+ const REPEAT_MARKER = "sproutboat B16: doubling repeat";
1867
+ const REPEAT_DOUBLING =
1868
+ " // sproutboat B16: doubling repeat\n" +
1869
+ " const total: i32 = thisLen * count;\n" +
1870
+ " if (total > 0) {\n" +
1871
+ " Porffor.IR.copy(Porffor.IR.ptr(out) + 4, Porffor.IR.ptr(this) + 4, thisLen);\n" +
1872
+ " let filled: i32 = thisLen;\n" +
1873
+ " while (filled * 2 <= total) {\n" +
1874
+ " Porffor.IR.copy(Porffor.IR.ptr(out) + 4 + filled, Porffor.IR.ptr(out) + 4, filled);\n" +
1875
+ " filled *= 2;\n" +
1876
+ " }\n" +
1877
+ " if (filled < total) Porffor.IR.copy(Porffor.IR.ptr(out) + 4 + filled, Porffor.IR.ptr(out) + 4, total - filled);\n" +
1878
+ " }\n";
1879
+
1880
+ /** B16: doubling String.prototype.repeat. Before the precompile, which compiles it. */
1881
+ export async function patchRepeatDoubling(root: string): Promise<void> {
1882
+ const file = resolve(root, "compiler/builtins/string.ts");
1883
+ const src = await readFile(file, "utf8");
1884
+ if (src.includes(REPEAT_MARKER)) return;
1885
+ if (src.split(REPEAT_LOOP).length !== 3)
1886
+ throw new Error(`could not patch Porffor's String.prototype.repeat: anchor not found twice in ${file}`);
1887
+ await writeFile(file, src.replaceAll(REPEAT_LOOP, REPEAT_DOUBLING));
1888
+ }
1889
+
1890
+ // JSON.stringify of a cyclic object recursed until the native stack overflowed: the
1891
+ // process died (SIGBUS/SIGSEGV) instead of throwing, so one request could take a
1892
+ // worker down (found by the W2 conformance suite). The replacer path already checks
1893
+ // cycles; the plain serializer now keeps its own ancestor stack and throws the same
1894
+ // TypeError. Arrays and objects push on entry and pop on exit; JSON.stringify clears
1895
+ // the stack first, so a throw mid-serialization leaves nothing behind.
1896
+ const JSON_CYCLE_MARKER = "sproutboat: JSON cycle check";
1897
+ const JSON_CYCLE_EDITS: [string, string, number][] = [
1898
+ [
1899
+ "let __Porffor_json_bufLimit: i32 = 0;\n",
1900
+ "let __Porffor_json_bufLimit: i32 = 0;\n" +
1901
+ "// sproutboat: JSON cycle check\n" +
1902
+ "let __Porffor_json_ancestors: any = 0;\n" +
1903
+ "export const __Porffor_json_enter = (value: any): void => {\n" +
1904
+ " if (__Porffor_json_ancestors === 0) __Porffor_json_ancestors = Porffor.array.new(8);\n" +
1905
+ " const stack: any[] = __Porffor_json_ancestors;\n" +
1906
+ " if (stack.includes(value)) {\n" +
1907
+ " stack.length = 0;\n" +
1908
+ " throw new TypeError('Converting circular structure to JSON');\n" +
1909
+ " }\n" +
1910
+ " Porffor.array.fastPush(stack, value);\n" +
1911
+ "};\n" +
1912
+ "export const __Porffor_json_leave = (): void => {\n" +
1913
+ " const stack: any[] = __Porffor_json_ancestors;\n" +
1914
+ " stack.length = stack.length - 1;\n" +
1915
+ "};\n",
1916
+ 1,
1917
+ ],
1918
+ [" depth += 1;\n", " depth += 1;\n __Porffor_json_enter(value);\n", 2],
1919
+ [" depth -= 1;\n", " __Porffor_json_leave();\n depth -= 1;\n", 2],
1920
+ [
1921
+ " let out: i32 = __Porffor_json_serialize(buffer, value, 0, space);\n",
1922
+ " if (__Porffor_json_ancestors !== 0) (__Porffor_json_ancestors as any[]).length = 0;\n" +
1923
+ " let out: i32 = __Porffor_json_serialize(buffer, value, 0, space);\n",
1924
+ 1,
1925
+ ],
1926
+ ];
1927
+
1928
+ /** JSON.stringify throws on cycles instead of overflowing the stack. Before the precompile. */
1929
+ export async function patchJsonCycles(root: string): Promise<void> {
1930
+ const file = resolve(root, "compiler/builtins/json.ts");
1931
+ let src = await readFile(file, "utf8");
1932
+ if (src.includes(JSON_CYCLE_MARKER)) return;
1933
+ for (const [anchor, inject, count] of JSON_CYCLE_EDITS) {
1934
+ if (src.split(anchor).length !== count + 1)
1935
+ throw new Error(`could not patch Porffor's JSON cycle check: anchor not found ${count}x in ${file}`);
1936
+ src = src.replaceAll(anchor, inject);
1937
+ }
1938
+ await writeFile(file, src);
1939
+ }
1940
+
1941
+ // Object.prototype.toString gave "[object Object]" for typed arrays: their lazily
1942
+ // built prototypes have no toStringTag, and the fallback list of builtin tags left
1943
+ // them out (W2 conformance; V8 gives "[object Uint8Array]").
1944
+ const TA_TAG_MARKER = "sproutboat: typed array toStringTag";
1945
+ const TA_TAG_ANCHOR = " return '[object ' + builtinTag + ']';\n};\n";
1946
+ const TA_TAG_INJECT =
1947
+ " // sproutboat: typed array toStringTag\n" +
1948
+ " if (Porffor.fastAnd(tag === undefined, t >= Porffor.TYPES.uint8clampedarray, t <= Porffor.TYPES.float64array)) {\n" +
1949
+ " if (t == Porffor.TYPES.uint8clampedarray) builtinTag = 'Uint8ClampedArray';\n else if (t == Porffor.TYPES.uint8array) builtinTag = 'Uint8Array';\n else if (t == Porffor.TYPES.int8array) builtinTag = 'Int8Array';\n else if (t == Porffor.TYPES.uint16array) builtinTag = 'Uint16Array';\n else if (t == Porffor.TYPES.int16array) builtinTag = 'Int16Array';\n else if (t == Porffor.TYPES.uint32array) builtinTag = 'Uint32Array';\n else if (t == Porffor.TYPES.int32array) builtinTag = 'Int32Array';\n else if (t == Porffor.TYPES.biguint64array) builtinTag = 'BigUint64Array';\n else if (t == Porffor.TYPES.bigint64array) builtinTag = 'BigInt64Array';\n else if (t == Porffor.TYPES.float32array) builtinTag = 'Float32Array';\n else if (t == Porffor.TYPES.float64array) builtinTag = 'Float64Array';\n" +
1950
+ " }\n" +
1951
+ TA_TAG_ANCHOR;
1952
+
1953
+ /** Typed arrays get their own toString tag. Before the precompile. */
1954
+ export async function patchTypedArrayTag(root: string): Promise<void> {
1955
+ const file = resolve(root, "compiler/builtins/object.ts");
1956
+ const src = await readFile(file, "utf8");
1957
+ if (src.includes(TA_TAG_MARKER)) return;
1958
+ if (src.split(TA_TAG_ANCHOR).length !== 2)
1959
+ throw new Error(`could not patch Porffor's typed array toStringTag: anchor not found in ${file}`);
1960
+ await writeFile(file, src.replace(TA_TAG_ANCHOR, TA_TAG_INJECT));
1961
+ }
1962
+
1963
+ // console.log coloured numbers, booleans and objects with ANSI escapes even when
1964
+ // stdout was a file or pipe, so sproutd's per-app logs (B21) and piped sprout output
1965
+ // were full of \x1b[33m. Colour only when stdout is a terminal, as Node and Bun do.
1966
+ const CONSOLE_COLOR_MARKER = "sproutboat: console colours only on a terminal";
1967
+ const CONSOLE_COLOR_ANCHOR = "export const __Porffor_consolePrint = (arg: any): void => {\n";
1968
+ const CONSOLE_COLOR_PRINT = " __Porffor_print(arg);\n};\n";
1969
+ const CONSOLE_COLOR_HELPER =
1970
+ "// sproutboat: console colours only on a terminal\n" +
1971
+ "let __Porffor_console_tty: i32 = -1;\n" +
1972
+ "export const __Porffor_consoleColors = (): boolean => {\n" +
1973
+ " if (__Porffor_console_tty < 0) {\n" +
1974
+ " let tty: i32 = 0;\n" +
1975
+ " Porffor.c`tty = isatty(1) ? 1 : 0;`;\n" +
1976
+ " __Porffor_console_tty = tty;\n" +
1977
+ " }\n" +
1978
+ " return __Porffor_console_tty == 1;\n" +
1979
+ "};\n\n";
1980
+
1981
+ /** console colours only when stdout is a terminal. Before the precompile. */
1982
+ export async function patchConsoleColors(root: string): Promise<void> {
1983
+ const file = resolve(root, "compiler/builtins/console.ts");
1984
+ let src = await readFile(file, "utf8");
1985
+ if (src.includes(CONSOLE_COLOR_MARKER)) return;
1986
+ const at = src.indexOf(CONSOLE_COLOR_ANCHOR);
1987
+ const print = at < 0 ? -1 : src.indexOf(CONSOLE_COLOR_PRINT, at);
1988
+ if (at < 0 || print < 0 || src.split(CONSOLE_COLOR_ANCHOR).length !== 2)
1989
+ throw new Error(`could not patch Porffor's console colours: anchor not found in ${file}`);
1990
+ src =
1991
+ src.slice(0, at) + CONSOLE_COLOR_HELPER + src.slice(at, print) +
1992
+ " __Porffor_print(arg, __Porffor_consoleColors());\n};\n" + src.slice(print + CONSOLE_COLOR_PRINT.length);
1993
+ await writeFile(file, src);
1994
+ }
1995
+
1996
+ const TEXT_DECODER_MARKER = "sproutboat: WHATWG UTF-8 decoder";
1997
+ /** Replaces Porffor's TextDecoder (text-decoder.ts). After patchURLClass, whose class ends it. */
1998
+ export async function patchTextDecoder(root: string): Promise<void> {
1999
+ const file = resolve(root, "runtime/fetch-globals.js");
2000
+ const src = await readFile(file, "utf8");
2001
+ if (src.includes(TEXT_DECODER_MARKER)) return;
2002
+ const start = src.indexOf("class TextDecoder {");
2003
+ const end = start < 0 ? -1 : src.indexOf("\nclass URL {", start);
2004
+ if (start < 0 || end < 0 || src.indexOf("class TextDecoder {", start + 1) >= 0)
2005
+ throw new Error(`could not patch Porffor's TextDecoder: anchor not found in ${file}`);
2006
+ await writeFile(file, src.slice(0, start) + TEXT_DECODER_SOURCE + src.slice(end));
2007
+ }
2008
+
2009
+ // String.prototype.toLowerCase/toUpperCase mapped ASCII only ("todo: unicode" upstream), so
2010
+ // "ÄÖÜ", Greek and Cyrillic came back unchanged, and URL hosts lowercased wrongly (W2).
2011
+ // Simple one-to-one BMP mappings from case-map.ts, looked up by binary search for units
2012
+ // >= 128. Latin-1 strings keep their own range (À-Þ, à-þ); one-to-many special casing
2013
+ // (ß to SS) and Latin-1 µ/ÿ uppercase (which leave Latin-1) are not handled.
2014
+ const CASE_MARKER = "sproutboat: Unicode case mapping";
2015
+ const caseLiteral = (runs: string) =>
2016
+ "'" + Array.from(runs, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0")).join("") + "'";
2017
+ const CASE_HELPERS =
2018
+ "// sproutboat: Unicode case mapping\n" +
2019
+ "export const __Porffor_caseFind = (chr: i32, runs: string): i32 => {\n" +
2020
+ " let lo: i32 = 0, hi: i32 = runs.length / 4 - 1, found: i32 = -1;\n" +
2021
+ " while (lo <= hi) {\n" +
2022
+ " const mid: i32 = (lo + hi) >> 1;\n" +
2023
+ " if (runs.charCodeAt(mid * 4) <= chr) { found = mid; lo = mid + 1; } else hi = mid - 1;\n" +
2024
+ " }\n" +
2025
+ " if (found < 0) return chr;\n" +
2026
+ " const start: i32 = runs.charCodeAt(found * 4);\n" +
2027
+ " if (chr > runs.charCodeAt(found * 4 + 1)) return chr;\n" +
2028
+ " if ((chr - start) % runs.charCodeAt(found * 4 + 3) != 0) return chr;\n" +
2029
+ " return runs.charCodeAt(found * 4 + 2) + (chr - start);\n" +
2030
+ "};\n" +
2031
+ "export const __Porffor_caseLower = (chr: i32): i32 => __Porffor_caseFind(chr, " + caseLiteral(CASE_LOWER_RUNS) + ");\n" +
2032
+ "export const __Porffor_caseUpper = (chr: i32): i32 => __Porffor_caseFind(chr, " + caseLiteral(CASE_UPPER_RUNS) + ");\n\n";
2033
+ const CASE_EDITS: [string, string][] = [
2034
+ [" if (chr >= 97) if (chr <= 122) chr -= 32;\n\n Porffor.IR.storeU16(j, 4, chr);",
2035
+ " if (chr < 128) { if (chr >= 97) if (chr <= 122) chr -= 32; } else chr = __Porffor_caseUpper(chr);\n\n Porffor.IR.storeU16(j, 4, chr);"],
2036
+ [" if (chr >= 65) if (chr <= 90) chr += 32;\n\n Porffor.IR.storeU16(j, 4, chr);",
2037
+ " if (chr < 128) { if (chr >= 65) if (chr <= 90) chr += 32; } else chr = __Porffor_caseLower(chr);\n\n Porffor.IR.storeU16(j, 4, chr);"],
2038
+ [" if (chr >= 97) if (chr <= 122) chr -= 32;\n\n Porffor.IR.storeU8(j++, 4, chr);",
2039
+ " if (chr >= 97) { if (chr <= 122) chr -= 32; else if (chr >= 224) if (chr <= 254) if (chr != 247) chr -= 32; }\n\n Porffor.IR.storeU8(j++, 4, chr);"],
2040
+ [" if (chr >= 65) if (chr <= 90) chr += 32;\n\n Porffor.IR.storeU8(j++, 4, chr);",
2041
+ " if (chr >= 65) { if (chr <= 90) chr += 32; else if (chr >= 192) if (chr <= 222) if (chr != 215) chr += 32; }\n\n Porffor.IR.storeU8(j++, 4, chr);"],
2042
+ ];
2043
+
2044
+ /** Unicode simple case mapping for toLowerCase/toUpperCase. Before the precompile. */
2045
+ export async function patchCaseMapping(root: string): Promise<void> {
2046
+ const file = resolve(root, "compiler/builtins/string.ts");
2047
+ let src = await readFile(file, "utf8");
2048
+ if (src.includes(CASE_MARKER)) return;
2049
+ const at = src.indexOf("export const __String_prototype_toUpperCase = function (this: string) {");
2050
+ if (at < 0) throw new Error(`could not patch Porffor's case mapping: anchor not found in ${file}`);
2051
+ for (const [anchor, inject] of CASE_EDITS) {
2052
+ if (src.split(anchor).length !== 2) throw new Error(`could not patch Porffor's case mapping: anchor not found in ${file}`);
2053
+ src = src.replace(anchor, inject);
2054
+ }
2055
+ await writeFile(file, src.slice(0, at) + CASE_HELPERS + src.slice(at));
2056
+ }
2057
+
2058
+ const HEADERS_CLASS_MARKER = "sproutboat: Fetch-spec Headers";
2059
+ /** Replaces Porffor's Headers (headers-class.ts), which also covers patchHeadersArrayInit's fix. */
2060
+ export async function patchHeadersClass(root: string): Promise<void> {
2061
+ const file = resolve(root, "runtime/fetch-globals.js");
2062
+ const src = await readFile(file, "utf8");
2063
+ if (src.includes(HEADERS_CLASS_MARKER)) return;
2064
+ const start = src.indexOf("class Headers {");
2065
+ const end = start < 0 ? -1 : src.indexOf("\nfunction setTimeout(", start);
2066
+ if (start < 0 || end < 0) throw new Error(`could not patch Porffor's Headers: anchor not found in ${file}`);
2067
+ await writeFile(file, src.slice(0, start) + HEADERS_CLASS_SOURCE + src.slice(end));
2068
+ }
2069
+
2070
+ // setTimeout(fn, 0) never fired on Linux: uSockets arms timers with timerfd_settime, where
2071
+ // a zero first expiry disarms the timer (kqueue on macOS fires it at once). The same held
2072
+ // for setInterval(fn, 0), which also lost its repeat. Arm at least 1 ms.
2073
+ const ZERO_TIMER_ANCHOR =
2074
+ " porf_native_fetch_timer_start(id, (i32)delay_ms, timer->repeat ? (i32)delay_ms : 0);";
2075
+ const ZERO_TIMER_INJECT =
2076
+ " porf_native_fetch_timer_start(id, delay_ms < 1 ? 1 : (i32)delay_ms, timer->repeat ? (delay_ms < 1 ? 1 : (i32)delay_ms) : 0); /* sproutboat: zero-delay timers fire on Linux */";
2077
+
2078
+ /** Zero-delay timers are armed at 1 ms, since a zero timerfd expiry disarms them. */
2079
+ export async function patchZeroDelayTimers(root: string): Promise<void> {
2080
+ const file = resolve(root, "compiler/render.js");
2081
+ const src = await readFile(file, "utf8");
2082
+ if (src.includes("sproutboat: zero-delay timers fire on Linux")) return;
2083
+ if (src.split(ZERO_TIMER_ANCHOR).length !== 2)
2084
+ throw new Error(`could not patch Porffor's zero-delay timers: anchor not found in ${file}`);
2085
+ await writeFile(file, src.replace(ZERO_TIMER_ANCHOR, ZERO_TIMER_INJECT));
2086
+ }
2087
+
2088
+ // sproutd links the shared runtime and each app's module as separate shared objects, so
2089
+ // Porffor's split output declares module globals without `static`, under their JS names.
2090
+ // A global named like a libc symbol then binds to libc: the structuredClone prelude's
2091
+ // `clone` made the runtime write a JS value into libc's clone() and every worker crashed
2092
+ // on start (SEGV_ACCERR). An app's `const hash` or `let search` would do the same.
2093
+ // Porffor's reserved-name list cannot keep up with libc, so split globals get a prefix;
2094
+ // Internal __ globals (Porffor's, the prelude's, sproutd's) keep their names: C refers to them.
2095
+ const SPLIT_GLOBALS_MARKER = "sproutboat: split globals are prefixed";
2096
+ const SPLIT_GLOBALS_EDITS: [string, string][] = [
2097
+ [
2098
+ " const st = split ? '' : 'static ';\n",
2099
+ " const st = split ? '' : 'static ';\n" +
2100
+ " // " + SPLIT_GLOBALS_MARKER + ", so none binds to a libc symbol of the same name\n" +
2101
+ " const gname = n => sanitize(split && !n.startsWith('__') ? 'pg_' + n : n);\n",
2102
+ ],
2103
+ [
2104
+ " case K.Global:\n cur.globals[node[N_A]] = true;\n case K.Local:\n return [sanitize(node[N_A]), P_PRIM];",
2105
+ " case K.Global:\n cur.globals[node[N_A]] = true;\n return [gname(node[N_A]), P_PRIM];\n case K.Local:\n return [sanitize(node[N_A]), P_PRIM];",
2106
+ ],
2107
+ [
2108
+ " emit(`${ind()}${sanitize(node[N_A][N_A])} = ${rx(node[N_B], P_COMMA)};\\n`);",
2109
+ " emit(`${ind()}${node[N_A][N_KIND] === K.Global ? gname(node[N_A][N_A]) : sanitize(node[N_A][N_A])} = ${rx(node[N_B], P_COMMA)};\\n`);",
2110
+ ],
2111
+ [
2112
+ " ? `${sanitize(update[N_A][N_A])} = ${rx(update[N_B], P_COMMA)}`",
2113
+ " ? `${update[N_A][N_KIND] === K.Global ? gname(update[N_A][N_A]) : sanitize(update[N_A][N_A])} = ${rx(update[N_B], P_COMMA)}`",
2114
+ ],
2115
+ [" const t = sanitize(target[N_A]);", " const t = target[N_KIND] === K.Global ? gname(target[N_A]) : sanitize(target[N_A]);"],
2116
+ ["${st}${CT[g.type]} ${sanitize(g.name)}", "${st}${CT[g.type]} ${gname(g.name)}"],
2117
+ [" const name = sanitize(g.name);", " const name = gname(g.name);"],
2118
+ ["const buckets = sanitize(g.name.replace(", "const buckets = gname(g.name.replace("],
2119
+ ["const bucketsCap = sanitize(g.name.replace(", "const bucketsCap = gname(g.name.replace("],
2120
+ ["text.push(`extern ${CT[globalTypes[x]]} ${sanitize(x)};\\n`);", "text.push(`extern ${CT[globalTypes[x]]} ${gname(x)};\\n`);"],
2121
+ ];
2122
+
2123
+ /** Split (shared-object) output prefixes module globals, so none binds to libc. */
2124
+ export async function patchSplitGlobals(root: string): Promise<void> {
2125
+ const file = resolve(root, "compiler/render.js");
2126
+ let src = await readFile(file, "utf8");
2127
+ if (src.includes(SPLIT_GLOBALS_MARKER)) return;
2128
+ for (const [anchor, inject] of SPLIT_GLOBALS_EDITS) {
2129
+ if (src.split(anchor).length !== 2)
2130
+ throw new Error(`could not patch Porffor's split globals: anchor not found in ${file}`);
2131
+ src = src.replace(anchor, inject);
2132
+ }
2133
+ await writeFile(file, src);
2134
+ }
2135
+
2136
+ const FETCH_CLASSES_MARKER = "sproutboat: Fetch-spec Request and Response";
2137
+ /** Replaces Porffor's Request and Response (fetch-classes.ts); covers patchResponseDefaults. */
2138
+ export async function patchFetchClasses(root: string): Promise<void> {
2139
+ const file = resolve(root, "runtime/fetch-globals.js");
2140
+ const src = await readFile(file, "utf8");
2141
+ if (src.includes(FETCH_CLASSES_MARKER)) return;
2142
+ const request = src.indexOf("class Request {");
2143
+ const response = src.indexOf("class Response {");
2144
+ if (request < 0 || response < request || src.indexOf("\nclass ", response + 1) >= 0)
2145
+ throw new Error(`could not patch Porffor's Request and Response: anchor not found in ${file}`);
2146
+ await writeFile(file, src.slice(0, request) + REQUEST_CLASS_SOURCE + RESPONSE_CLASS_SOURCE);
2147
+ }
2148
+
2149
+ /** The native response finalizer sends byte bodies as bytes (fetch-classes.ts). */
2150
+ export async function patchResponseBytes(root: string): Promise<void> {
2151
+ const file = resolve(root, "runtime/native-fetch.js");
2152
+ const src = await readFile(file, "utf8");
2153
+ if (src.includes("sproutboat: byte bodies go out as their bytes")) return;
2154
+ if (src.split(RESPONSE_BYTES_ANCHOR).length !== 2)
2155
+ throw new Error(`could not patch Porffor's response finalizer: anchor not found in ${file}`);
2156
+ await writeFile(file, src.replace(RESPONSE_BYTES_ANCHOR, RESPONSE_BYTES_INJECT));
2157
+ }
2158
+
2159
+ /** Replaces Porffor's Blob with a byte-backed Blob and File (fetch-classes.ts). */
2160
+ export async function patchBlobClass(root: string): Promise<void> {
2161
+ const file = resolve(root, "runtime/fetch-globals.js");
2162
+ const src = await readFile(file, "utf8");
2163
+ if (src.includes("sproutboat: byte-backed Blob and File")) return;
2164
+ const start = src.indexOf("class Blob {");
2165
+ const end = start < 0 ? -1 : src.indexOf("\nclass TextEncoder {", start);
2166
+ if (start < 0 || end < 0) throw new Error(`could not patch Porffor's Blob: anchor not found in ${file}`);
2167
+ await writeFile(file, src.slice(0, start) + BLOB_CLASS_SOURCE + src.slice(end));
2168
+ }
2169
+
2170
+ /** Streamed responses in the uWebSockets shim (streaming.ts). After patchUwebsockets. */
2171
+ export async function patchStreamingResponses(root: string): Promise<void> {
2172
+ const renderFile = resolve(root, "compiler/render.js");
2173
+ let render = await readFile(renderFile, "utf8");
2174
+ if (!render.includes("sproutboat B19: streaming fallbacks")) {
2175
+ if (render.split(streaming.STREAM_STUBS_ANCHOR).length !== 2)
2176
+ throw new Error(`could not patch Porffor's streaming fallbacks: anchor not found in ${renderFile}`);
2177
+ render = render.replace(streaming.STREAM_STUBS_ANCHOR, streaming.STREAM_STUBS + streaming.STREAM_STUBS_ANCHOR);
2178
+ await writeFile(renderFile, render);
2179
+ }
2180
+ const file = resolve(root, "compiler/uwebsockets.js");
2181
+ let src = await readFile(file, "utf8");
2182
+ if (src.includes("sproutboat B19: streamed responses")) return;
2183
+ const edits: [string, string][] = [
2184
+ [streaming.STREAM_FORBIDDEN_ANCHOR, streaming.STREAM_FORBIDDEN_INJECT],
2185
+ [streaming.STREAM_FLAG_ANCHOR, streaming.STREAM_FLAG_INJECT],
2186
+ [streaming.STREAM_SCAN_ANCHOR, streaming.STREAM_SCAN_INJECT],
2187
+ [streaming.STREAM_CORK_ANCHOR, streaming.STREAM_CORK_INJECT],
2188
+ [streaming.STREAM_END_ANCHOR, streaming.STREAM_END_INJECT],
2189
+ [streaming.STREAM_REGISTRY_ANCHOR, streaming.STREAM_REGISTRY + streaming.STREAM_REGISTRY_ANCHOR],
2190
+ ];
2191
+ for (const [anchor, inject] of edits) {
2192
+ if (src.split(anchor).length !== 2)
2193
+ throw new Error(`could not patch Porffor's streamed responses: anchor not found in ${file}`);
2194
+ src = src.replace(anchor, inject);
2195
+ }
2196
+ await writeFile(file, src);
2197
+ }
2198
+
2199
+ /**
2200
+ * True when root is a finished cache entry that this toolchain version and patch
2201
+ * chain patched (ensurePorffor patches before it writes the manifest). The CLI calls
2202
+ * ensurePorfforPatched on every compile: re-running the patch chain there cost
2203
+ * ~1.3 s per build, and its precompile rewrote builtins_precompiled.js in place,
2204
+ * so parallel builds sharing the cache could import a half-written table.
2205
+ */
2206
+ async function patchedByThisVersion(root: string): Promise<boolean> {
2207
+ try {
2208
+ // SAFETY: private manifest written by ensurePorffor; only three fields are read.
2209
+ const manifest = JSON.parse(await readFile(resolve(root, ".sproutboat-complete"), "utf8")) as {
2210
+ commit?: string;
2211
+ toolchainVersion?: string;
2212
+ patchFingerprint?: string;
2213
+ };
2214
+ return (
2215
+ manifest.commit === PORFFOR_COMMIT_FULL &&
2216
+ manifest.toolchainVersion === TOOLCHAIN_VERSION &&
2217
+ manifest.patchFingerprint === PATCH_FINGERPRINT
2218
+ );
2219
+ } catch {
2220
+ return false;
2221
+ }
2222
+ }
2223
+
1350
2224
  export async function ensurePorfforPatched(root: string): Promise<void> {
1351
2225
  if (done.has(root)) return;
2226
+ if (await patchedByThisVersion(root)) {
2227
+ done.add(root);
2228
+ return;
2229
+ }
1352
2230
  await patchCompilerArgs(root);
1353
2231
  await patchClassSelfName(root);
1354
2232
  await patchUwebsockets(root);
1355
2233
  await patchRenderJs(root);
2234
+ await patchStreamingResponses(root);
2235
+ await patchQuietCollection(root);
2236
+ await patchDeferredDiscard(root);
1356
2237
  await patchDateParser(root);
1357
2238
  await patchPromiseResolveNull(root);
1358
2239
  await patchReplaceAll(root);
1359
- await patchTypedArrayStore(root);
1360
2240
  await patchLocalFunctionTypes(root);
1361
2241
  await patchUint8ArrayBase64(root);
2242
+ await patchResponseDefaults(root);
2243
+ await patchHeadersArrayInit(root);
2244
+ await patchHeadersClass(root);
2245
+ await patchFetchClasses(root);
2246
+ await patchBlobClass(root);
2247
+ await patchResponseBytes(root);
2248
+ await patchURLClass(root);
2249
+ await patchTextDecoder(root);
2250
+ await patchMapGroupBy(root);
2251
+ await patchPrivateFieldsHidden(root);
2252
+ await patchRepeatDoubling(root);
2253
+ await patchJsonCycles(root);
2254
+ await patchTypedArrayTag(root);
2255
+ await patchConsoleColors(root);
2256
+ await patchCaseMapping(root);
2257
+ await patchZeroDelayTimers(root);
2258
+ await patchSplitGlobals(root);
1362
2259
  // Last of the builtin edits: its precompile picks up the date, promise and
1363
2260
  // replaceAll patches above too.
1364
2261
  await precompileBuiltins(root);