@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/CHANGELOG.md +67 -0
- package/PORFFOR_ALPHA17.md +7 -0
- package/PORFFOR_ALPHA18.md +64 -0
- package/PORFFOR_SCORECARD.md +5 -5
- package/package.json +1 -1
- package/scorecards/alpha-17-731f1b2.json +72 -0
- package/src/acquire.test.ts +84 -10
- package/src/acquire.ts +11 -6
- package/src/alpha18-native.test.ts +43 -0
- package/src/case-map.ts +9 -0
- package/src/fetch-classes.ts +242 -0
- package/src/fingerprint.test.ts +28 -0
- package/src/fingerprint.ts +44 -0
- package/src/gen-case-map.ts +35 -0
- package/src/groupby-private-native.test.ts +37 -0
- package/src/headers-class.ts +120 -0
- package/src/idle-gc.test.ts +38 -0
- package/src/json-cycles-native.test.ts +39 -0
- package/src/patch.test.ts +487 -16
- package/src/patch.ts +1092 -195
- package/src/pin.ts +3 -3
- package/src/repeat-native.test.ts +34 -0
- package/src/streaming.ts +125 -0
- package/src/text-decoder.ts +92 -0
- package/src/url-class.ts +342 -0
- package/vendor/porffor-deda494.tar.gz +0 -0
- package/vendor/porffor-731f1b2.tar.gz +0 -0
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
|
-
[
|
|
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
|
-
[
|
|
142
|
-
|
|
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 =
|
|
270
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
711
|
-
"
|
|
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 =
|
|
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
|
|
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
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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 =
|
|
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" +
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
[
|
|
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
|
-
[
|
|
959
|
-
|
|
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
|
-
[
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
-
[
|
|
985
|
-
|
|
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 (
|
|
997
|
-
|
|
998
|
-
|
|
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 (
|
|
1004
|
-
|
|
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 =
|
|
1021
|
-
|
|
1022
|
-
|
|
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(
|
|
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 (
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
const
|
|
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))
|
|
1040
|
-
|
|
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 (
|
|
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))
|
|
1059
|
-
|
|
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 (
|
|
1065
|
-
|
|
1066
|
-
|
|
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))
|
|
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 =
|
|
1196
|
+
const duplicatePump =
|
|
1197
|
+
" for (;;) {\n char chunk[65536];\n for (;;) {\n";
|
|
1085
1198
|
if (src.includes(duplicatePump)) {
|
|
1086
|
-
src = src.replace(
|
|
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(
|
|
1207
|
+
src = src.replace(
|
|
1208
|
+
unclosedPump,
|
|
1209
|
+
" state->sent += bytes;\n }\n };\n",
|
|
1210
|
+
);
|
|
1092
1211
|
changed = true;
|
|
1093
1212
|
}
|
|
1094
|
-
if (
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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 =
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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(
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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 (
|
|
1120
|
-
|
|
1121
|
-
|
|
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(
|
|
1124
|
-
|
|
1125
|
-
|
|
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 =
|
|
1144
|
-
|
|
1145
|
-
const
|
|
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(
|
|
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))
|
|
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
|
|
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
|
-
[
|
|
1173
|
-
|
|
1174
|
-
|
|
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) +
|
|
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(
|
|
1201
|
-
|
|
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(
|
|
1205
|
-
|
|
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(
|
|
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(
|
|
1225
|
-
|
|
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))
|
|
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 =
|
|
1474
|
+
const marker =
|
|
1475
|
+
"sproutboat: preserve lexical types for direct-only local functions";
|
|
1257
1476
|
if (src.includes(marker)) return;
|
|
1258
|
-
const anchor =
|
|
1259
|
-
|
|
1260
|
-
|
|
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
|
-
|
|
1263
|
-
|
|
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 =
|
|
1267
|
-
|
|
1268
|
-
|
|
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
|
-
[
|
|
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))
|
|
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)
|
|
1306
|
-
|
|
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 =
|
|
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);
|