@sproutboat/runtime 0.17.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @sproutboat/runtime
2
2
 
3
+ ## 0.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4274bef: `Response.body` and `Request.body` are `ReadableStream`s for every body type, as in the Fetch spec and workerd. Before, they held the value given (a string, bytes or a `Blob`). `res.body.getReader()`, `res.body.pipeThrough(...)` and `new Response(other.body)` now work on any response, including the incoming request. The value as given stays internal, so responses are still written with a fixed length unless the body really is a stream. A body whose stream has been locked can no longer be read with `text()` and the other readers, which throw a `TypeError`.
8
+
9
+ ## 0.18.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 145e3dc: `HTMLRewriter`, matching Cloudflare's API and lol-html's output.
14
+
15
+ - **Handlers:** `on(selector, { element, text, comments })` and `onDocument({ doctype, text, comments, end })`.
16
+ - **Element methods:** attributes, `tagName`, `before`/`after`/`prepend`/`append`/`replace`/`setInnerContent` with `{ html }`, `remove`, `removeAndKeepContent` and `onEndTag`.
17
+ - **Selectors:** type, `*`, `#id`, `.class`, the attribute operators, descendant and child combinators, and lists.
18
+ - **Async:** handlers may be async.
19
+
20
+ It reads the whole body before rewriting rather than streaming it. 37 cases are checked against Bun's lol-html-based HTMLRewriter, including where lol-html keeps attributes byte for byte and does not imply end tags.
21
+ - 2541458: More of the Web platform, from sproutd's W2 conformance suite against workerd:
22
+
23
+ - `CompressionStream` and `DecompressionStream` for `gzip`, `deflate` and `deflate-raw`, in JavaScript, because the native runtime links no zlib. Output decompresses with zlib, and zlib's output (stored, fixed and dynamic blocks) decompresses here. A corrupt or truncated input errors with a `TypeError`.
24
+ - A `Response` or `Request` whose body is a `ReadableStream` is read to the end by `text()`, `json()`, `arrayBuffer()` and `blob()`. Before, these returned `"[object ReadableStream]"`. Chunks must be bytes, as in workerd.
25
+ - `JSON.parse` applies its reviver: children first, `undefined` deletes the property, and `this` is the holder.
26
+
27
+ ### Patch Changes
28
+
29
+ - 116ee47: sproutd builds the runtime again. In split builds Porffor cannot type a prelude function's parameters, and the compression checksums' `let c = ~crc` compiled to C that assigned a boxed value to a number. The checksums now start from the parameter itself.
30
+
3
31
  ## 0.17.1
4
32
 
5
33
  ### Patch Changes
package/WEB_STREAMS.md CHANGED
@@ -14,8 +14,14 @@ the status and headers go out at once and each chunk is written as it is produce
14
14
  (chunked encoding), so Server-Sent Events and long responses work. Chunks must be bytes.
15
15
  Hosts that do not stream yet (sproutd's workers) read the stream to the end and send it
16
16
  buffered, at most 32 MiB. Request bodies and outbound fetch remain buffered.
17
- TextEncoderStream and TextDecoderStream are included; CompressionStream and
18
- DecompressionStream are not.
17
+ TextEncoderStream and TextDecoderStream are included, and CompressionStream and
18
+ DecompressionStream ("gzip", "deflate", "deflate-raw") in JavaScript, since the native
19
+ runtime links no zlib. Compression writes one fixed-Huffman block per chunk as it arrives
20
+ (close to zlib on text: 252 bytes against 221 for 22.5 KB); decompression inflates all
21
+ block types once the input ends and checks the CRC-32 or Adler-32. Response and Request
22
+ bodies that are ReadableStreams are read by text(), json(), arrayBuffer() and blob(), and
23
+ their chunks must be bytes, as in workerd. `body` is a ReadableStream for every body type,
24
+ as in Fetch: a stream body itself, otherwise a stream made once over the bytes.
19
25
  BYOB buffer detachment requires transfer support from the underlying engine;
20
26
  the polyfill cannot supply that engine primitive. Full Web Platform Tests
21
27
  conformance has not been established for Porffor.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/runtime",
3
- "version": "0.17.1",
3
+ "version": "0.19.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -6,9 +6,10 @@ const ingress = src.slice(
6
6
  src.indexOf("function __sbDecodeIngress("),
7
7
  src.indexOf("globalThis.__sbEntry ="),
8
8
  );
9
- if (!decoder || !ingress) throw new Error("Ingress decoder source not found");
9
+ const rawBody = src.slice(src.indexOf("function __sbRawBody("), src.indexOf("function __sbHasStreamBody("));
10
+ if (!decoder || !ingress || !rawBody) throw new Error("Ingress decoder source not found");
10
11
  // oxlint-disable-next-line no-function-constructor -- Exercise the text-only runtime prelude.
11
- const load = new Function(`${decoder}\n${ingress}\nreturn __sbDecodeIngress;`);
12
+ const load = new Function(`${decoder}\n${rawBody}\n${ingress}\nreturn __sbDecodeIngress;`);
12
13
  const decode = load();
13
14
  const raw = (text) =>
14
15
  Array.from(new TextEncoder().encode(text), (byte) => String.fromCharCode(byte)).join("");
@@ -2305,6 +2305,706 @@
2305
2305
  /* oxlint-enable */
2306
2306
  // END WEB STREAMS
2307
2307
 
2308
+ // COMPRESSION STREAMS: CompressionStream and DecompressionStream ("gzip", "deflate",
2309
+ // "deflate-raw"), in JavaScript: the native runtime links no zlib. Compression is LZ77 with
2310
+ // fixed Huffman codes, one block per written chunk, so output follows input as it arrives;
2311
+ // it is smaller than the input for text, not as small as zlib's. Decompression inflates
2312
+ // stored, fixed and dynamic blocks once the input ends, and checks the CRC-32 or Adler-32.
2313
+ // A malformed or truncated stream errors with a TypeError, as in workerd.
2314
+
2315
+ const __sbCrcTable = (() => {
2316
+ const table = new Int32Array(256);
2317
+ for (let n = 0; n < 256; n++) {
2318
+ let c = n;
2319
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
2320
+ table[n] = c;
2321
+ }
2322
+ return table;
2323
+ })();
2324
+ // c starts as the parameter itself. In split builds (sproutd) a prelude function may be
2325
+ // called from app code, so Porffor cannot type its parameters; it typed c as a number
2326
+ // from the loop but boxed ~crc (crc might be a BigInt), and the C did not compile.
2327
+ function __sbCrc32(crc, bytes) {
2328
+ let c = crc;
2329
+ c = ~c;
2330
+ for (let i = 0; i < bytes.length; i++) c = __sbCrcTable[(c ^ bytes[i]) & 255] ^ (c >>> 8);
2331
+ return ~c >>> 0;
2332
+ }
2333
+ function __sbAdler32(adler, bytes) {
2334
+ let a = adler, b = adler;
2335
+ a = a & 0xffff;
2336
+ b = b >>> 16;
2337
+ for (let i = 0; i < bytes.length; ) {
2338
+ const end = Math.min(i + 3800, bytes.length);
2339
+ for (; i < end; i++) { a += bytes[i]; b += a; }
2340
+ a %= 65521; b %= 65521;
2341
+ }
2342
+ return ((b << 16) | a) >>> 0;
2343
+ }
2344
+
2345
+ // Length and distance codes (RFC 1951 3.2.5).
2346
+ const __sbLenBase = [3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258];
2347
+ const __sbLenExtra = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0];
2348
+ const __sbDistBase = [1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577];
2349
+ const __sbDistExtra = [0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13];
2350
+
2351
+ class __sbBitWriter {
2352
+ constructor() { this.out = new Uint8Array(1024); this.length = 0; this.bits = 0; this.count = 0; }
2353
+ byte(b) {
2354
+ if (this.length == this.out.length) { const grown = new Uint8Array(this.out.length * 2); grown.set(this.out); this.out = grown; }
2355
+ this.out[this.length++] = b;
2356
+ }
2357
+ // Values go out least significant bit first.
2358
+ write(value, n) {
2359
+ this.bits |= value << this.count;
2360
+ this.count += n;
2361
+ while (this.count >= 8) { this.byte(this.bits & 255); this.bits >>>= 8; this.count -= 8; }
2362
+ }
2363
+ // Huffman codes go out most significant bit first.
2364
+ code(code, n) {
2365
+ let reversed = 0;
2366
+ for (let i = 0; i < n; i++) { reversed = (reversed << 1) | (code & 1); code >>= 1; }
2367
+ this.write(reversed, n);
2368
+ }
2369
+ align() { if (this.count > 0) { this.byte(this.bits & 255); this.bits = 0; this.count = 0; } }
2370
+ take() { const out = this.out.slice(0, this.length); this.length = 0; return out; }
2371
+ }
2372
+ function __sbFixedLiteral(w, v) {
2373
+ if (v < 144) w.code(0x30 + v, 8);
2374
+ else if (v < 256) w.code(0x190 + v - 144, 9);
2375
+ else if (v < 280) w.code(v - 256, 7);
2376
+ else w.code(0xc0 + v - 280, 8);
2377
+ }
2378
+ // One fixed-Huffman block for data, matching within it (a hash chain of 3-byte prefixes).
2379
+ function __sbDeflateBlock(w, data, last) {
2380
+ w.write(last ? 1 : 0, 1);
2381
+ w.write(1, 2);
2382
+ const head = new Int32Array(1 << 15).fill(-1);
2383
+ const prev = new Int32Array(Math.max(data.length, 1));
2384
+ const hash = (i) => ((data[i] << 10) ^ (data[i + 1] << 5) ^ data[i + 2]) & 0x7fff;
2385
+ let i = 0;
2386
+ while (i < data.length) {
2387
+ let bestLen = 0, bestDist = 0;
2388
+ if (i + 2 < data.length) {
2389
+ const h = hash(i);
2390
+ let candidate = head[h], chain = 32;
2391
+ while (candidate >= 0 && i - candidate <= 32768 && chain-- > 0) {
2392
+ let len = 0;
2393
+ const max = Math.min(258, data.length - i);
2394
+ while (len < max && data[candidate + len] == data[i + len]) len++;
2395
+ if (len > bestLen) { bestLen = len; bestDist = i - candidate; if (len == max) break; }
2396
+ candidate = prev[candidate];
2397
+ }
2398
+ prev[i] = head[h];
2399
+ head[h] = i;
2400
+ }
2401
+ if (bestLen >= 3) {
2402
+ let code = 0;
2403
+ while (code < 28 && __sbLenBase[code + 1] <= bestLen) code++;
2404
+ __sbFixedLiteral(w, 257 + code);
2405
+ if (__sbLenExtra[code]) w.write(bestLen - __sbLenBase[code], __sbLenExtra[code]);
2406
+ let dcode = 0;
2407
+ while (dcode < 29 && __sbDistBase[dcode + 1] <= bestDist) dcode++;
2408
+ w.code(dcode, 5);
2409
+ if (__sbDistExtra[dcode]) w.write(bestDist - __sbDistBase[dcode], __sbDistExtra[dcode]);
2410
+ // Index the skipped positions so later matches can start inside this one.
2411
+ for (let k = 1; k < bestLen && i + k + 2 < data.length; k++) { const h = hash(i + k); prev[i + k] = head[h]; head[h] = i + k; }
2412
+ i += bestLen;
2413
+ } else {
2414
+ __sbFixedLiteral(w, data[i]);
2415
+ i++;
2416
+ }
2417
+ }
2418
+ __sbFixedLiteral(w, 256);
2419
+ }
2420
+
2421
+ class __sbHuffman {
2422
+ // Canonical code from code lengths, decoded bit by bit (RFC 1951 3.2.2).
2423
+ constructor(lengths) {
2424
+ this.counts = new Int32Array(16);
2425
+ this.symbols = new Int32Array(lengths.length);
2426
+ for (let i = 0; i < lengths.length; i++) this.counts[lengths[i]]++;
2427
+ this.counts[0] = 0;
2428
+ const offsets = new Int32Array(16);
2429
+ for (let i = 1; i < 16; i++) offsets[i] = offsets[i - 1] + this.counts[i - 1];
2430
+ for (let i = 0; i < lengths.length; i++) if (lengths[i]) this.symbols[offsets[lengths[i]]++] = i;
2431
+ }
2432
+ }
2433
+ class __sbBitReader {
2434
+ constructor(data, at) { this.data = data; this.at = at; this.bits = 0; this.count = 0; }
2435
+ need(n) {
2436
+ while (this.count < n) {
2437
+ if (this.at >= this.data.length) throw new TypeError("Decompression failed: unexpected end of data");
2438
+ this.bits |= this.data[this.at++] << this.count;
2439
+ this.count += 8;
2440
+ }
2441
+ }
2442
+ read(n) {
2443
+ if (n == 0) return 0;
2444
+ this.need(n);
2445
+ const v = this.bits & ((1 << n) - 1);
2446
+ this.bits >>>= n;
2447
+ this.count -= n;
2448
+ return v;
2449
+ }
2450
+ decode(h) {
2451
+ let code = 0, first = 0, index = 0;
2452
+ for (let len = 1; len < 16; len++) {
2453
+ code |= this.read(1);
2454
+ const count = h.counts[len];
2455
+ if (code - count < first) return h.symbols[index + (code - first)];
2456
+ index += count;
2457
+ first += count;
2458
+ first <<= 1;
2459
+ code <<= 1;
2460
+ }
2461
+ throw new TypeError("Decompression failed: invalid Huffman code");
2462
+ }
2463
+ // The byte after the last bit read; stored blocks and trailers start on a byte.
2464
+ byteAt() { return this.at - (this.count >> 3); }
2465
+ align() { this.bits = 0; this.count = 0; }
2466
+ }
2467
+ const __sbFixedLit = (() => {
2468
+ const l = new Uint8Array(288);
2469
+ for (let i = 0; i < 288; i++) l[i] = i < 144 ? 8 : i < 256 ? 9 : i < 280 ? 7 : 8;
2470
+ return new __sbHuffman(l);
2471
+ })();
2472
+ const __sbFixedDist = new __sbHuffman(new Uint8Array(30).fill(5));
2473
+ const __sbClOrder = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15];
2474
+ function __sbGrow(out, used, n) {
2475
+ let size = out.length * 2;
2476
+ while (size < used + n) size *= 2;
2477
+ const grown = new Uint8Array(size);
2478
+ grown.set(out.subarray(0, used));
2479
+ return grown;
2480
+ }
2481
+ // Inflates raw deflate data from at; answers the output and the byte after the last block.
2482
+ function __sbInflate(data, at) {
2483
+ const r = new __sbBitReader(data, at);
2484
+ // Not named length: Porffor loses writes to a local of that name (Alpha 18).
2485
+ let out = new Uint8Array(Math.max(1024, data.length * 4));
2486
+ let used = 0;
2487
+ let last = 0;
2488
+ while (!last) {
2489
+ last = r.read(1);
2490
+ const type = r.read(2);
2491
+ if (type == 0) {
2492
+ r.align();
2493
+ const p = r.at;
2494
+ if (p + 4 > data.length) throw new TypeError("Decompression failed: unexpected end of data");
2495
+ const len = data[p] | (data[p + 1] << 8), nlen = data[p + 2] | (data[p + 3] << 8);
2496
+ if ((len ^ 0xffff) != nlen) throw new TypeError("Decompression failed: invalid stored block");
2497
+ if (p + 4 + len > data.length) throw new TypeError("Decompression failed: unexpected end of data");
2498
+ if (used + len > out.length) out = __sbGrow(out, used, len);
2499
+ out.set(data.subarray(p + 4, p + 4 + len), used); used += len;
2500
+ r.at = p + 4 + len;
2501
+ continue;
2502
+ }
2503
+ let lit = __sbFixedLit, dist = __sbFixedDist;
2504
+ if (type == 2) {
2505
+ const hlit = r.read(5) + 257, hdist = r.read(5) + 1, hclen = r.read(4) + 4;
2506
+ const cl = new Uint8Array(19);
2507
+ for (let i = 0; i < hclen; i++) cl[__sbClOrder[i]] = r.read(3);
2508
+ const clh = new __sbHuffman(cl);
2509
+ const lengths = new Uint8Array(hlit + hdist);
2510
+ for (let i = 0; i < hlit + hdist; ) {
2511
+ const sym = r.decode(clh);
2512
+ if (sym < 16) lengths[i++] = sym;
2513
+ else {
2514
+ let repeat = 0, value = 0;
2515
+ if (sym == 16) { if (i == 0) throw new TypeError("Decompression failed: invalid code lengths"); value = lengths[i - 1]; repeat = 3 + r.read(2); }
2516
+ else if (sym == 17) repeat = 3 + r.read(3);
2517
+ else repeat = 11 + r.read(7);
2518
+ if (i + repeat > hlit + hdist) throw new TypeError("Decompression failed: invalid code lengths");
2519
+ while (repeat--) lengths[i++] = value;
2520
+ }
2521
+ }
2522
+ lit = new __sbHuffman(lengths.subarray(0, hlit));
2523
+ dist = new __sbHuffman(lengths.subarray(hlit));
2524
+ } else if (type != 1) throw new TypeError("Decompression failed: invalid block type");
2525
+ for (;;) {
2526
+ const sym = r.decode(lit);
2527
+ if (sym < 256) { if (used + 1 > out.length) out = __sbGrow(out, used, 1); out[used++] = sym; continue; }
2528
+ if (sym == 256) break;
2529
+ const code = sym - 257;
2530
+ if (code >= 29) throw new TypeError("Decompression failed: invalid used code");
2531
+ const len = __sbLenBase[code] + r.read(__sbLenExtra[code]);
2532
+ const dcode = r.decode(dist);
2533
+ if (dcode >= 30) throw new TypeError("Decompression failed: invalid distance code");
2534
+ const d = __sbDistBase[dcode] + r.read(__sbDistExtra[dcode]);
2535
+ if (d > used) throw new TypeError("Decompression failed: distance too far back");
2536
+ if (used + len > out.length) out = __sbGrow(out, used, len);
2537
+ for (let k = 0; k < len; k++) { out[used] = out[used - d]; used++; }
2538
+ }
2539
+ }
2540
+ return { out: out.slice(0, used), end: r.byteAt() };
2541
+ }
2542
+
2543
+ function __sbFormat(format, what) {
2544
+ const f = String(format);
2545
+ if (f != "gzip" && f != "deflate" && f != "deflate-raw") throw new TypeError("Unsupported " + what + " format: " + f);
2546
+ return f;
2547
+ }
2548
+ function __sbChunkBytes(chunk) {
2549
+ if (chunk instanceof Uint8Array) return chunk;
2550
+ if (chunk instanceof ArrayBuffer) return new Uint8Array(chunk);
2551
+ if (ArrayBuffer.isView(chunk)) return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
2552
+ throw new TypeError("The provided value is not of type '(ArrayBuffer or ArrayBufferView)'");
2553
+ }
2554
+
2555
+ class CompressionStream {
2556
+ constructor(format) {
2557
+ const kind = __sbFormat(format, "compression");
2558
+ const w = new __sbBitWriter();
2559
+ let check = kind == "deflate" ? 1 : 0, size = 0;
2560
+ if (kind == "gzip") for (const b of [0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 0xff]) w.byte(b);
2561
+ else if (kind == "deflate") { w.byte(0x78); w.byte(0x9c); }
2562
+ const t = new TransformStream({
2563
+ transform(chunk, controller) {
2564
+ const bytes = __sbChunkBytes(chunk);
2565
+ if (!bytes.length) return;
2566
+ if (kind == "gzip") check = __sbCrc32(check, bytes);
2567
+ else if (kind == "deflate") check = __sbAdler32(check, bytes);
2568
+ size = (size + bytes.length) >>> 0;
2569
+ __sbDeflateBlock(w, bytes, false);
2570
+ const out = w.take();
2571
+ if (out.length) controller.enqueue(out);
2572
+ },
2573
+ flush(controller) {
2574
+ __sbDeflateBlock(w, new Uint8Array(0), true);
2575
+ w.align();
2576
+ if (kind == "gzip") for (const v of [check, size]) for (let k = 0; k < 4; k++) w.byte((v >>> (8 * k)) & 255);
2577
+ else if (kind == "deflate") for (let k = 3; k >= 0; k--) w.byte((check >>> (8 * k)) & 255);
2578
+ controller.enqueue(w.take());
2579
+ },
2580
+ });
2581
+ this._t = t;
2582
+ }
2583
+ get readable() { return this._t.readable; }
2584
+ get writable() { return this._t.writable; }
2585
+ }
2586
+
2587
+ class DecompressionStream {
2588
+ constructor(format) {
2589
+ const kind = __sbFormat(format, "decompression");
2590
+ const parts = [];
2591
+ let total = 0;
2592
+ const t = new TransformStream({
2593
+ transform(chunk) { const bytes = __sbChunkBytes(chunk); parts.push(bytes.slice()); total += bytes.length; },
2594
+ flush(controller) {
2595
+ const data = new Uint8Array(total);
2596
+ let at = 0;
2597
+ for (const p of parts) { data.set(p, at); at += p.length; }
2598
+ let start = 0;
2599
+ if (kind == "gzip") {
2600
+ if (data.length < 18 || data[0] != 0x1f || data[1] != 0x8b || data[2] != 8) throw new TypeError("Decompression failed: not gzip data");
2601
+ const flags = data[3];
2602
+ start = 10;
2603
+ if (flags & 4) start += 2 + (data[start] | (data[start + 1] << 8));
2604
+ if (flags & 8) { while (start < data.length && data[start]) start++; start++; }
2605
+ if (flags & 16) { while (start < data.length && data[start]) start++; start++; }
2606
+ if (flags & 2) start += 2;
2607
+ } else if (kind == "deflate") {
2608
+ if (data.length < 6 || (data[0] & 15) != 8 || ((data[0] << 8) | data[1]) % 31 != 0 || data[1] & 32) throw new TypeError("Decompression failed: not zlib data");
2609
+ start = 2;
2610
+ }
2611
+ const { out, end } = __sbInflate(data, start);
2612
+ if (kind == "gzip") {
2613
+ if (end + 8 > data.length) throw new TypeError("Decompression failed: unexpected end of data");
2614
+ const crc = (data[end] | (data[end + 1] << 8) | (data[end + 2] << 16) | (data[end + 3] << 24)) >>> 0;
2615
+ const isize = (data[end + 4] | (data[end + 5] << 8) | (data[end + 6] << 16) | (data[end + 7] << 24)) >>> 0;
2616
+ if (crc != __sbCrc32(0, out) || isize != out.length >>> 0) throw new TypeError("Decompression failed: gzip checksum mismatch");
2617
+ } else if (kind == "deflate") {
2618
+ if (end + 4 > data.length) throw new TypeError("Decompression failed: unexpected end of data");
2619
+ const adler = ((data[end] << 24) | (data[end + 1] << 16) | (data[end + 2] << 8) | data[end + 3]) >>> 0;
2620
+ if (adler != __sbAdler32(1, out)) throw new TypeError("Decompression failed: deflate checksum mismatch");
2621
+ }
2622
+ if (out.length) controller.enqueue(out);
2623
+ },
2624
+ });
2625
+ this._t = t;
2626
+ }
2627
+ get readable() { return this._t.readable; }
2628
+ get writable() { return this._t.writable; }
2629
+ }
2630
+ // Static installs, so app code sees the names when it is compiled apart (see streams-entry.js).
2631
+ if (globalThis.CompressionStream == null) globalThis.CompressionStream = CompressionStream;
2632
+ if (globalThis.DecompressionStream == null) globalThis.DecompressionStream = DecompressionStream;
2633
+ // END COMPRESSION STREAMS
2634
+
2635
+ // HTML REWRITER: Cloudflare's HTMLRewriter in JavaScript (workerd uses lol-html). It reads
2636
+ // the whole body, then tokenizes it once: elements, text and comments are matched against
2637
+ // the selectors given to on() and onDocument(), handlers run in document order (and may be
2638
+ // async), and the changes they ask for are written as the document is re-emitted.
2639
+ // Selectors: type, *, #id, .class, [attr], [attr=v], [attr~=v], [attr^=v], [attr$=v],
2640
+ // [attr*=v], [attr|=v], descendant and child combinators, and lists. Not :nth-child and
2641
+ // other pseudo-classes, which throw a TypeError on on(), as an unsupported selector does.
2642
+
2643
+ const __sbVoidTags = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
2644
+ const __sbRawTextTags = new Set(["script", "style", "textarea", "title", "xmp", "iframe", "noembed", "noframes", "plaintext"]);
2645
+ function __sbEscapeText(s) { return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }
2646
+ function __sbEscapeAttr(s) { return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;"); }
2647
+ function __sbContent(content, options) { return options && options.html ? String(content) : __sbEscapeText(content); }
2648
+
2649
+ // One compound selector: [{ tag, id, classes, attrs }] joined by combinators.
2650
+ function __sbParseSelector(text) {
2651
+ const list = [];
2652
+ for (const part of String(text).split(",")) {
2653
+ const steps = [];
2654
+ let s = part.trim(), combinator = " ";
2655
+ if (!s) throw new TypeError("Invalid selector: " + text);
2656
+ while (s.length) {
2657
+ const step = { tag: null, id: null, classes: [], attrs: [], combinator };
2658
+ let m;
2659
+ let star = false;
2660
+ if ((m = /^(\*|[a-zA-Z][\w-]*)/.exec(s))) { if (m[1] != "*") step.tag = m[1].toLowerCase(); else star = true; s = s.slice(m[0].length); }
2661
+ for (;;) {
2662
+ if ((m = /^#([\w-]+)/.exec(s))) { step.id = m[1]; s = s.slice(m[0].length); continue; }
2663
+ if ((m = /^\.([\w-]+)/.exec(s))) { step.classes.push(m[1]); s = s.slice(m[0].length); continue; }
2664
+ if ((m = /^\[\s*([\w:-]+)\s*(?:([~^$*|]?=)\s*(?:"([^"]*)"|'([^']*)'|([^\]\s]+))\s*)?(i)?\s*\]/.exec(s))) {
2665
+ step.attrs.push({ name: m[1].toLowerCase(), op: m[2] || null, value: m[3] ?? m[4] ?? m[5] ?? "", ci: !!m[6] });
2666
+ s = s.slice(m[0].length);
2667
+ continue;
2668
+ }
2669
+ break;
2670
+ }
2671
+ if (s.length && s[0] == ":") throw new TypeError("Unsupported pseudo-class selector: " + text);
2672
+ if (step.tag == null && step.id == null && !step.classes.length && !step.attrs.length && !star)
2673
+ throw new TypeError("Invalid selector: " + text);
2674
+ steps.push(step);
2675
+ const ws = /^\s*/.exec(s)[0];
2676
+ s = s.slice(ws.length);
2677
+ if (!s.length) break;
2678
+ if (s[0] == ">") { combinator = ">"; s = s.slice(1).trimStart(); }
2679
+ else if (ws.length) combinator = " ";
2680
+ else throw new TypeError("Invalid selector: " + text);
2681
+ }
2682
+ list.push(steps);
2683
+ }
2684
+ return list;
2685
+ }
2686
+ function __sbStepMatches(step, el) {
2687
+ if (step.tag != null && step.tag != el.tag) return false;
2688
+ if (step.id != null && el.attr("id") !== step.id) return false;
2689
+ if (step.classes.length) {
2690
+ const have = (el.attr("class") || "").split(/\s+/);
2691
+ for (const c of step.classes) if (!have.includes(c)) return false;
2692
+ }
2693
+ for (const a of step.attrs) {
2694
+ let v = el.attr(a.name);
2695
+ if (v == null) return false;
2696
+ if (!a.op) continue;
2697
+ let want = a.value;
2698
+ if (a.ci) { v = v.toLowerCase(); want = want.toLowerCase(); }
2699
+ if (a.op == "=" && v !== want) return false;
2700
+ if (a.op == "~=" && !v.split(/\s+/).includes(want)) return false;
2701
+ if (a.op == "^=" && (!want || !v.startsWith(want))) return false;
2702
+ if (a.op == "$=" && (!want || !v.endsWith(want))) return false;
2703
+ if (a.op == "*=" && (!want || !v.includes(want))) return false;
2704
+ if (a.op == "|=" && v !== want && !v.startsWith(want + "-")) return false;
2705
+ }
2706
+ return true;
2707
+ }
2708
+ // stack: the open elements, outermost first; the last is the candidate.
2709
+ function __sbSelectorMatches(steps, stack) {
2710
+ const match = (si, ei) => {
2711
+ if (!__sbStepMatches(steps[si], stack[ei])) return false;
2712
+ if (si == 0) return true;
2713
+ if (steps[si].combinator == ">") return ei > 0 && match(si - 1, ei - 1);
2714
+ for (let k = ei - 1; k >= 0; k--) if (match(si - 1, k)) return true;
2715
+ return false;
2716
+ };
2717
+ return match(steps.length - 1, stack.length - 1);
2718
+ }
2719
+
2720
+ // Content to write around a node, from before/after/prepend/append/replace calls.
2721
+ class __sbEdits {
2722
+ constructor() { this.before = ""; this.after = ""; this.replaced = null; this.removed = false; }
2723
+ }
2724
+ class __sbElement {
2725
+ constructor(state) { this._s = state; }
2726
+ get tagName() { return this._s.tag; }
2727
+ set tagName(name) { this._s.tag = String(name).toLowerCase(); this._s.renamed = true; }
2728
+ get namespaceURI() { return "http://www.w3.org/1999/xhtml"; }
2729
+ get attributes() { return this._s.attrs.map((a) => [a[0], a[1]])[Symbol.iterator](); }
2730
+ get removed() { return this._s.edits.removed; }
2731
+ get selfClosing() { return this._s.selfClosing; }
2732
+ get canHaveContent() { return !__sbVoidTags.has(this._s.tag); }
2733
+ getAttribute(name) { return this._s.attr(String(name).toLowerCase()); }
2734
+ hasAttribute(name) { return this._s.attr(String(name).toLowerCase()) != null; }
2735
+ setAttribute(name, value) {
2736
+ const n = String(name).toLowerCase();
2737
+ const at = this._s.attrs.findIndex((a) => a[0] == n);
2738
+ if (at >= 0) this._s.attrs[at] = [n, String(value), null]; else this._s.attrs.push([n, String(value), null]);
2739
+ this._s.attrsChanged = true;
2740
+ return this;
2741
+ }
2742
+ removeAttribute(name) {
2743
+ const n = String(name).toLowerCase();
2744
+ this._s.attrs = this._s.attrs.filter((a) => a[0] != n);
2745
+ this._s.attrsChanged = true;
2746
+ return this;
2747
+ }
2748
+ before(content, options) { this._s.edits.before += __sbContent(content, options); return this; }
2749
+ after(content, options) { this._s.edits.after = __sbContent(content, options) + this._s.edits.after; return this; }
2750
+ prepend(content, options) { this._s.prepend = __sbContent(content, options) + this._s.prepend; return this; }
2751
+ append(content, options) { this._s.append += __sbContent(content, options); return this; }
2752
+ setInnerContent(content, options) { this._s.inner = __sbContent(content, options); this._s.prepend = ""; this._s.append = ""; return this; }
2753
+ replace(content, options) { this._s.edits.replaced = __sbContent(content, options); this._s.edits.removed = true; return this; }
2754
+ remove() { this._s.edits.removed = true; this._s.edits.replaced = ""; return this; }
2755
+ removeAndKeepContent() { this._s.keepContent = true; this._s.edits.removed = true; return this; }
2756
+ onEndTag(handler) { this._s.endHandlers.push(handler); }
2757
+ }
2758
+ class __sbEndTag {
2759
+ constructor(state) { this._s = state; }
2760
+ get name() { return this._s.tag; }
2761
+ set name(n) { this._s.tag = String(n).toLowerCase(); }
2762
+ before(content, options) { this._s.endBefore += __sbContent(content, options); return this; }
2763
+ after(content, options) { this._s.endAfter = __sbContent(content, options) + this._s.endAfter; return this; }
2764
+ remove() { this._s.endRemoved = true; return this; }
2765
+ }
2766
+ class __sbTextChunk {
2767
+ constructor(text, last) { this.text = text; this.lastInTextNode = last; this._e = new __sbEdits(); }
2768
+ get removed() { return this._e.removed; }
2769
+ before(content, options) { this._e.before += __sbContent(content, options); return this; }
2770
+ after(content, options) { this._e.after = __sbContent(content, options) + this._e.after; return this; }
2771
+ replace(content, options) { this._e.replaced = __sbContent(content, options); this._e.removed = true; return this; }
2772
+ remove() { this._e.removed = true; this._e.replaced = ""; return this; }
2773
+ }
2774
+ class __sbComment {
2775
+ constructor(text) { this.text = text; this._e = new __sbEdits(); }
2776
+ get removed() { return this._e.removed; }
2777
+ before(content, options) { this._e.before += __sbContent(content, options); return this; }
2778
+ after(content, options) { this._e.after = __sbContent(content, options) + this._e.after; return this; }
2779
+ replace(content, options) { this._e.replaced = __sbContent(content, options); this._e.removed = true; return this; }
2780
+ remove() { this._e.removed = true; this._e.replaced = ""; return this; }
2781
+ }
2782
+ class __sbDocEnd {
2783
+ constructor(out) { this._out = out; }
2784
+ append(content, options) { this._out.push(__sbContent(content, options)); return this; }
2785
+ }
2786
+
2787
+ class __sbRewriteRun {
2788
+ constructor(handlers, documentHandlers, html) {
2789
+ this.handlers = handlers;
2790
+ this.documentHandlers = documentHandlers;
2791
+ this.html = html;
2792
+ this.out = [];
2793
+ this.stack = [];
2794
+ // Inner content of an element replaced by setInnerContent, remove or replace is skipped:
2795
+ // the depth in the stack below which output is suppressed, or -1.
2796
+ this.skipBelow = -1;
2797
+ }
2798
+ emit(s) { if (this.skipBelow < 0) this.out.push(s); }
2799
+ async call(handler, method, arg) {
2800
+ const f = handler && handler[method];
2801
+ if (__sbIsFn(f)) await f.call(handler, arg);
2802
+ }
2803
+ // Text and comment handlers run for content anywhere inside a matched element.
2804
+ inside(steps) {
2805
+ for (let i = 0; i < this.stack.length; i++) if (steps.some((s) => __sbSelectorMatches(s, this.stack.slice(0, i + 1)))) return true;
2806
+ return false;
2807
+ }
2808
+ // A text node arrives as its text, then an empty chunk with lastInTextNode, as lol-html does.
2809
+ async text(raw) {
2810
+ if (!raw.length || this.skipBelow >= 0) return;
2811
+ for (const [text, last] of [[raw, false], ["", true]]) {
2812
+ const chunk = new __sbTextChunk(text, last);
2813
+ for (const h of this.documentHandlers) await this.call(h, "text", chunk);
2814
+ for (const [steps, h] of this.handlers) if (this.inside(steps)) await this.call(h, "text", chunk);
2815
+ this.emit(chunk._e.removed ? chunk._e.before + (chunk._e.replaced ?? "") + chunk._e.after : chunk._e.before + text + chunk._e.after);
2816
+ }
2817
+ }
2818
+ async comment(raw) {
2819
+ if (this.skipBelow >= 0) return;
2820
+ const c = new __sbComment(raw);
2821
+ for (const h of this.documentHandlers) await this.call(h, "comments", c);
2822
+ for (const [steps, h] of this.handlers) if (this.inside(steps)) await this.call(h, "comments", c);
2823
+ this.emit(c._e.removed ? c._e.before + (c._e.replaced ?? "") + c._e.after : c._e.before + "<!--" + c.text + "-->" + c._e.after);
2824
+ }
2825
+ async doctype(raw) {
2826
+ const m = /^<!doctype\s+([^\s>]+)?/i.exec(raw);
2827
+ const d = { name: m && m[1] ? m[1].toLowerCase() : null, publicId: null, systemId: null };
2828
+ for (const h of this.documentHandlers) await this.call(h, "doctype", d);
2829
+ this.emit(raw);
2830
+ }
2831
+ startTag(state) {
2832
+ let s = "<" + (state.renamed ? state.tag : state.rawName);
2833
+ for (const [n, v, raw] of state.attrs) s += raw != null ? " " + raw : " " + n + '="' + __sbEscapeAttr(v) + '"';
2834
+ return s + (state.selfClosing ? " />" : ">");
2835
+ }
2836
+ async element(tag, rawName, attrs, selfClosing, rawStart) {
2837
+ const state = {
2838
+ tag, rawName, attrs, selfClosing, rawStart,
2839
+ attr(name) { const a = this.attrs.find((x) => x[0] == name); return a ? a[1] : null; },
2840
+ edits: new __sbEdits(), prepend: "", append: "", inner: null, keepContent: false, renamed: false, attrsChanged: false,
2841
+ endHandlers: [], endBefore: "", endAfter: "", endRemoved: false,
2842
+ };
2843
+ const skipped = this.skipBelow >= 0;
2844
+ this.stack.push(state);
2845
+ if (!skipped) {
2846
+ const el = new __sbElement(state);
2847
+ for (const [steps, h] of this.handlers) if (steps.some((s) => __sbSelectorMatches(s, this.stack))) await this.call(h, "element", el);
2848
+ }
2849
+ const e = state.edits;
2850
+ this.emit(e.before);
2851
+ const unchanged = !state.renamed && !state.attrsChanged;
2852
+ if (!e.removed || state.keepContent) {
2853
+ if (!e.removed) this.emit(unchanged ? rawStart : this.startTag(state));
2854
+ } else this.emit(e.replaced ?? "");
2855
+ const isVoid = __sbVoidTags.has(tag) || selfClosing;
2856
+ if (isVoid) {
2857
+ this.stack.pop();
2858
+ this.emit(e.after);
2859
+ return null;
2860
+ }
2861
+ if (!e.removed || state.keepContent) this.emit(state.prepend);
2862
+ if (state.inner != null) { this.emit(state.inner); if (this.skipBelow < 0) this.skipBelow = this.stack.length; }
2863
+ else if (e.removed && !state.keepContent && this.skipBelow < 0) this.skipBelow = this.stack.length;
2864
+ return state;
2865
+ }
2866
+ async endTag(state, rawEnd) {
2867
+ if (this.skipBelow == this.stack.length) this.skipBelow = -1;
2868
+ this.stack.pop();
2869
+ const e = state.edits;
2870
+ if (!e.removed || state.keepContent) this.emit(state.append);
2871
+ if (!e.removed) {
2872
+ const end = new __sbEndTag(state);
2873
+ for (const handler of state.endHandlers) await handler(end);
2874
+ this.emit(state.endBefore);
2875
+ if (!state.endRemoved) this.emit(state.renamed || end.name != state.tag ? "</" + state.tag + ">" : rawEnd);
2876
+ this.emit(state.endAfter);
2877
+ }
2878
+ this.emit(e.after);
2879
+ }
2880
+ async run() {
2881
+ const html = this.html;
2882
+ let i = 0, textStart = 0;
2883
+ const flushText = async (to) => { if (to > textStart) await this.text(html.slice(textStart, to)); };
2884
+ const open = [];
2885
+ while (i < html.length) {
2886
+ const lt = html.indexOf("<", i);
2887
+ if (lt < 0) break;
2888
+ const next = html[lt + 1];
2889
+ if (html.startsWith("<!--", lt)) {
2890
+ await flushText(lt);
2891
+ const end = html.indexOf("-->", lt + 4);
2892
+ const stop = end < 0 ? html.length : end;
2893
+ await this.comment(html.slice(lt + 4, stop));
2894
+ i = textStart = end < 0 ? html.length : end + 3;
2895
+ continue;
2896
+ }
2897
+ if (next == "!" || next == "?") {
2898
+ await flushText(lt);
2899
+ const end = html.indexOf(">", lt);
2900
+ const raw = html.slice(lt, end < 0 ? html.length : end + 1);
2901
+ if (/^<!doctype/i.test(raw)) await this.doctype(raw); else this.emit(raw);
2902
+ i = textStart = lt + raw.length;
2903
+ continue;
2904
+ }
2905
+ if (next == "/" && /[a-zA-Z]/.test(html[lt + 2] || "")) {
2906
+ await flushText(lt);
2907
+ const end = html.indexOf(">", lt);
2908
+ const raw = html.slice(lt, end < 0 ? html.length : end + 1);
2909
+ const name = /^<\/([^\s/>]+)/.exec(raw)[1].toLowerCase();
2910
+ // Close up to the matching open element; unmatched end tags pass through.
2911
+ let at = -1;
2912
+ for (let k = open.length - 1; k >= 0; k--) if (open[k].rawName.toLowerCase() == name) { at = k; break; }
2913
+ if (at < 0) this.emit(raw);
2914
+ else {
2915
+ while (open.length > at + 1) { const inner = open.pop(); await this.endTag(inner, ""); }
2916
+ await this.endTag(open.pop(), raw);
2917
+ }
2918
+ i = textStart = lt + raw.length;
2919
+ continue;
2920
+ }
2921
+ if (!/[a-zA-Z]/.test(next || "")) { i = lt + 1; continue; }
2922
+ await flushText(lt);
2923
+ // A start tag: name, then attributes (quoted, unquoted or bare) up to > or />.
2924
+ let p = lt + 1;
2925
+ while (p < html.length && !/[\s/>]/.test(html[p])) p++;
2926
+ const rawName = html.slice(lt + 1, p);
2927
+ const tag = rawName.toLowerCase();
2928
+ const attrs = [];
2929
+ let selfClosing = false;
2930
+ for (;;) {
2931
+ while (p < html.length && /\s/.test(html[p])) p++;
2932
+ if (p >= html.length) break;
2933
+ if (html[p] == ">") { p++; break; }
2934
+ if (html[p] == "/" && html[p + 1] == ">") { selfClosing = true; p += 2; break; }
2935
+ if (html[p] == "/") { p++; continue; }
2936
+ const start = p;
2937
+ let q = p;
2938
+ while (q < html.length && !/[\s=/>]/.test(html[q])) q++;
2939
+ const name = html.slice(p, q).toLowerCase();
2940
+ p = q;
2941
+ while (p < html.length && /\s/.test(html[p])) p++;
2942
+ if (html[p] == "=") {
2943
+ p++;
2944
+ while (p < html.length && /\s/.test(html[p])) p++;
2945
+ let value;
2946
+ if (html[p] == '"' || html[p] == "'") {
2947
+ const quote = html[p], close = html.indexOf(quote, p + 1);
2948
+ value = html.slice(p + 1, close < 0 ? html.length : close);
2949
+ p = close < 0 ? html.length : close + 1;
2950
+ } else {
2951
+ let r = p;
2952
+ while (r < html.length && !/[\s>]/.test(html[r])) r++;
2953
+ value = html.slice(p, r);
2954
+ p = r;
2955
+ }
2956
+ attrs.push([name, value.replace(/&amp;/g, "&").replace(/&quot;/g, '"'), html.slice(start, p)]);
2957
+ } else attrs.push([name, "", html.slice(start, p)]);
2958
+ }
2959
+ const rawStart = html.slice(lt, p);
2960
+ const state = await this.element(tag, rawName, attrs, selfClosing && !__sbVoidTags.has(tag) ? false : selfClosing, rawStart);
2961
+ if (state) open.push(state);
2962
+ i = textStart = p;
2963
+ // Raw text elements hold their content verbatim up to their own end tag.
2964
+ if (state && __sbRawTextTags.has(tag)) {
2965
+ const close = html.toLowerCase().indexOf("</" + tag, p);
2966
+ const stop = close < 0 ? html.length : close;
2967
+ await this.text(html.slice(p, stop));
2968
+ i = textStart = stop;
2969
+ }
2970
+ }
2971
+ await flushText(html.length);
2972
+ // An element whose end tag never came has not ended: nothing appended to it is written
2973
+ // (lol-html does not imply end tags).
2974
+ open.length = 0;
2975
+ const tail = [];
2976
+ for (const h of this.documentHandlers) if (__sbIsFn(h.end)) await h.end(new __sbDocEnd(tail));
2977
+ return this.out.join("") + tail.join("");
2978
+ }
2979
+ }
2980
+
2981
+ class HTMLRewriter {
2982
+ constructor() { this._handlers = []; this._documentHandlers = []; }
2983
+ on(selector, handlers) { this._handlers.push([__sbParseSelector(selector), handlers]); return this; }
2984
+ onDocument(handlers) { this._documentHandlers.push(handlers); return this; }
2985
+ transform(response) {
2986
+ const handlers = this._handlers, documentHandlers = this._documentHandlers;
2987
+ const headers = new Headers(response.headers);
2988
+ headers.delete("content-length");
2989
+ const body = new ReadableStream({
2990
+ async start(controller) {
2991
+ try {
2992
+ const html = await response.text();
2993
+ const out = await new __sbRewriteRun(handlers, documentHandlers, html).run();
2994
+ controller.enqueue(new TextEncoder().encode(out));
2995
+ controller.close();
2996
+ } catch (error) {
2997
+ controller.error(error);
2998
+ }
2999
+ },
3000
+ });
3001
+ return new Response(body, { status: response.status, statusText: response.statusText, headers });
3002
+ }
3003
+ }
3004
+ // A static install, so app code sees the name when it is compiled apart (see streams-entry.js).
3005
+ if (globalThis.HTMLRewriter == null) globalThis.HTMLRewriter = HTMLRewriter;
3006
+ // END HTML REWRITER
3007
+
2308
3008
  // Porffor exposes native timers lexically. Libraries also access them through
2309
3009
  // globalThis, including noble hashes 2.4's host-task yields.
2310
3010
  if (globalThis.setTimeout == null) globalThis.setTimeout = setTimeout;
@@ -2392,7 +3092,7 @@ function __sbCpuMs() {
2392
3092
  function __sbTagCpu(res, t0) {
2393
3093
  const cpu = __sbCpuMs() - t0;
2394
3094
  try {
2395
- const body = res && res.body;
3095
+ const body = __sbRawBody(res);
2396
3096
  if (__sbIsStr(body) && !res.headers.has("set-cookie")) {
2397
3097
  const headers = {};
2398
3098
  res.headers.forEach(function (value, name) {
@@ -4489,7 +5189,7 @@ globalThis.__sbInstallBindings = function (target, bindings) {
4489
5189
  url,
4490
5190
  method: opts.method || "GET",
4491
5191
  headers,
4492
- ...__sbRequestBody(opts.body),
5192
+ ...__sbRequestBody(opts instanceof Request ? __sbRawBody(opts) : opts.body),
4493
5193
  });
4494
5194
  const respHeaders = new Headers();
4495
5195
  for (let j = 0; j < (r.headers || []).length; j++) respHeaders.set(r.headers[j][0], r.headers[j][1]);
@@ -4521,7 +5221,7 @@ globalThis.__sbInstallFetch = function () {
4521
5221
  if (__sbIsFn(source.forEach)) source.forEach((v, k) => headers.push([k, v]));
4522
5222
  else for (const k in source) headers.push([k, source[k]]);
4523
5223
  }
4524
- const body = opts.body !== undefined ? opts.body : req && req.body !== "" ? req.body : null;
5224
+ const body = opts.body !== undefined && !(opts instanceof Request) ? opts.body : req && __sbRawBody(req) !== "" ? __sbRawBody(req) : null;
4525
5225
  const method = opts.method || (req && req.method) || "GET";
4526
5226
  const msg = { url, method, headers, ...__sbRequestBody(body) };
4527
5227
  const respond = (r) => {
@@ -4947,8 +5647,14 @@ async function __sbCollectBody(stream) {
4947
5647
  }
4948
5648
  return out;
4949
5649
  }
5650
+ // The body as given (a string, bytes or a ReadableStream), not the stream the Fetch-spec
5651
+ // body getter makes over it; plain objects keep their body as is.
5652
+ function __sbRawBody(message) {
5653
+ if (message == null) return null;
5654
+ return message._bodyInit !== undefined ? message._bodyInit : message.body;
5655
+ }
4950
5656
  function __sbHasStreamBody(res) {
4951
- return res != null && res.body instanceof ReadableStream;
5657
+ return res != null && __sbRawBody(res) instanceof ReadableStream;
4952
5658
  }
4953
5659
 
4954
5660
  // B19: on a host that streams (a standalone sprout's native server defines sb_stream_*), a
@@ -4997,7 +5703,7 @@ function __sbStreamingSupported() {
4997
5703
  return __sbStreaming === 1;
4998
5704
  }
4999
5705
  function __sbStreamResponse(response) {
5000
- const stream = response.body;
5706
+ const stream = __sbRawBody(response);
5001
5707
  const id = __sbStreamOp("o", "0", "");
5002
5708
  response.headers.set("x-sb-stream", id);
5003
5709
  response.body = "";
@@ -5033,7 +5739,7 @@ async function __sbFetchWithDrain(res, tasks) {
5033
5739
  r = res && __sbIsFn(res.then) ? await res : res;
5034
5740
  if (__sbHasStreamBody(r)) {
5035
5741
  if (__sbStreamingSupported()) __sbStreamResponse(r);
5036
- else r.body = await __sbCollectBody(r.body);
5742
+ else r.body = await __sbCollectBody(__sbRawBody(r));
5037
5743
  }
5038
5744
  } catch (error) {
5039
5745
  // #179 — a rejected handler promise must not take the process down with it.
@@ -5094,7 +5800,7 @@ async function __sbQueueWithDrain(result, tasks) {
5094
5800
  // handing the request to user code. Request.text() in Porffor otherwise treats
5095
5801
  // the raw bytes as already-decoded characters and corrupts Unicode/HMAC input.
5096
5802
  function __sbDecodeIngress(request) {
5097
- const raw = request.body;
5803
+ const raw = __sbRawBody(request);
5098
5804
  if (raw == null) return;
5099
5805
  let decoded = null;
5100
5806
  request.text = function () {
@@ -5286,7 +5992,8 @@ async function __sbRunQueueBatch(handlers, body, ctx) {
5286
5992
 
5287
5993
  function __sbReadJson(request) {
5288
5994
  try {
5289
- return JSON.parse(request.body == null ? "{}" : String(request.body));
5995
+ const body = __sbRawBody(request);
5996
+ return JSON.parse(body == null ? "{}" : String(body));
5290
5997
  } catch {
5291
5998
  return {};
5292
5999
  }
@@ -18,6 +18,38 @@ const handler = `export default {async fetch(request) {
18
18
  if (path === "/stream-strings") return new Response(new ReadableStream({ start(c) { c.enqueue("text"); c.close(); } }));
19
19
  if (path === "/form") { const f = new FormData(); f.append("a", "1"); f.append("f", new Blob(["xy"]), "n.txt"); return new Response(f); }
20
20
  const cases = [
21
+ // body is a ReadableStream for every body type (Fetch), and readers still work.
22
+ () => [new Response("hi").body instanceof ReadableStream, new Response(new Uint8Array([1])).body instanceof ReadableStream, new Response(null).body, new Request("http://a.test", { method: "POST", body: "q" }).body instanceof ReadableStream].join(","),
23
+ async () => new Response(new Response("abc").body).text(),
24
+ async () => { const r = new Response("x"); const b = r.body; return (b === r.body) + ":" + (await r.text()); },
25
+ async () => { const r = new Response("x"); r.body.getReader(); try { await r.text(); return "no throw"; } catch (e) { return e.name; } },
26
+ async () => { const reader = new Response("chunk").body.getReader(); const { value, done } = await reader.read(); return new TextDecoder().decode(value) + ":" + done + ":" + (await reader.read()).done; },
27
+ async () => { const r = new Response("cloned"); return (await r.clone().text()) + "|" + (await r.text()); },
28
+ // HTMLRewriter, against Bun's (lol-html, as in workerd): the W2 cases and more.
29
+ () => typeof HTMLRewriter,
30
+ () => new HTMLRewriter().on("h1", { element(e) { e.setInnerContent("x"); } }).transform(new Response("<h1>a</h1><p>b</p>")).text(),
31
+ () => new HTMLRewriter().on("a", { element(e) { e.setAttribute("href", "/n"); } }).transform(new Response('<a href="/o" data-x=1>l</a>')).text(),
32
+ () => new HTMLRewriter().on("script", { element(e) { e.remove(); } }).transform(new Response("<p>a</p><script>if (a < b) x()</script>")).text(),
33
+ async () => { let t = ""; await new HTMLRewriter().on("p", { text(c) { t += c.text + (c.lastInTextNode ? "|" : ""); } }).transform(new Response("<p>hi <b>there</b></p>")).text(); return t; },
34
+ () => new HTMLRewriter().on("body", { element(e) { e.append("<i>z</i>", { html: true }).prepend("<&>"); } }).transform(new Response("<body>a</body>")).text(),
35
+ () => new HTMLRewriter().on("div > span.k, #m", { element(e) { e.replace("R"); } }).on("p", { comments(c) { c.remove(); } }).transform(new Response('<div><span class="k">1</span><p><!--c-->2<span class="k">3</span></p></div><i id="m">4</i>')).text(),
36
+ () => new HTMLRewriter().on("li", { async element(e) { await null; e.removeAndKeepContent(); } }).onDocument({ end(d) { d.append("<!--e-->", { html: true }); } }).transform(new Response("<ul><li>a</li><li>b</li></ul>")).text(),
37
+ // CompressionStream and DecompressionStream: our output round-trips, and zlib's decodes.
38
+ async () => { const out = []; for (const f of ["gzip", "deflate", "deflate-raw"]) out.push(await new Response(new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("hello ".repeat(200))); c.close(); } }).pipeThrough(new CompressionStream(f)).pipeThrough(new DecompressionStream(f))).text() === "hello ".repeat(200)); return out.join(","); },
39
+ async () => new Response(new ReadableStream({ start(c) { c.enqueue(Uint8Array.from(atob("H4sIAAAAAAACEysuKMovLUnKTyxRKB5uTACdCbks3AAAAA=="), (ch) => ch.charCodeAt(0))); c.close(); } }).pipeThrough(new DecompressionStream("gzip"))).text(),
40
+ async () => new Response(new ReadableStream({ start(c) { c.enqueue(Uint8Array.from(atob("eJxLqcxLzM1MVsgoTUvLTcxTSBnlj4ZHJv3SQ2peCgCxCfLI"), (ch) => ch.charCodeAt(0))); c.close(); } }).pipeThrough(new DecompressionStream("deflate"))).text(),
41
+ async () => { const s = new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("hello hello")); c.close(); } }).pipeThrough(new CompressionStream("gzip")).pipeThrough(new DecompressionStream("gzip")).pipeThrough(new TextDecoderStream()); let t = ""; for (const r = s.getReader(); ;) { const { value, done } = await r.read(); if (done) break; t += value; } return t; },
42
+ // workerd knows gzip, deflate and deflate-raw; Bun (the reference) also brotli.
43
+ () => { try { new CompressionStream("brotli"); return "workerd:no throw"; } catch (e) { return "workerd:" + e.name; } },
44
+ // A ReadableStream body is read to its end by text(), arrayBuffer(), json() and blob().
45
+ async () => new Response(new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("hé")); c.enqueue(new Uint8Array([33])); c.close(); } })).text(),
46
+ async () => Array.from(new Uint8Array(await new Response(new ReadableStream({ start(c) { c.enqueue(new Uint8Array([1, 2])); c.close(); } })).arrayBuffer())).join(","),
47
+ async () => JSON.stringify(await new Response(new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode('{"a":[1]}')); c.close(); } })).json()),
48
+ async () => (await new Response(new ReadableStream({ start(c) { c.enqueue(new Uint8Array([120])); c.close(); } })).blob()).size,
49
+ async () => new Request("http://a.test", { method: "POST", body: new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("req")); c.close(); } }), duplex: "half" }).text(),
50
+ // Chunks must be bytes: workerd throws a TypeError, where Bun (the reference below) reads a
51
+ // string chunk as text. "workerd:" marks the answer to expect from workerd instead.
52
+ async () => { try { await new Response(new ReadableStream({ start(c) { c.enqueue("s"); c.close(); } })).text(); return "workerd:no throw"; } catch (e) { return "workerd:" + e.name; } },
21
53
  () => new Request("http://a.test/a/../b?x=1#f").url,
22
54
  async () => { const r = new Request("http://a.test", { method: "POST", body: "x" }); await r.text(); try { await r.text(); return "no throw"; } catch (e) { return r.bodyUsed + ":" + e.name; } },
23
55
  () => new Request("http://a.test").redirect,
@@ -125,7 +157,7 @@ try {
125
157
  for (const run of reference) {
126
158
  try { want.push(String(await run())); } catch (e) { want.push("THROWS " + (e instanceof Error ? e.name : String(e))); }
127
159
  }
128
- assert.deepEqual(got, want);
160
+ assert.deepEqual(got, want.map((w) => (w.startsWith("workerd:") ? "workerd:TypeError" : w)));
129
161
  console.log(`Native bodies are sent as bytes and ${want.length} Fetch class cases match Bun`);
130
162
  } finally {
131
163
  if (server) {
@@ -17,6 +17,11 @@ const CASES = [
17
17
  `JSON.stringify({ n: { toJSON() { return 42; } } })`,
18
18
  `JSON.stringify({ nested: { at: new Date(0), list: [new Date(0)] } })`,
19
19
  `JSON.stringify({ plain: { x: 1 } })`,
20
+ // The reviver: children first, undefined deletes, `this` is the holder, arrays by index.
21
+ `JSON.stringify(JSON.parse('{"a":1,"b":{"c":2}}', (k, v) => (typeof v === "number" ? v * 10 : v)))`,
22
+ `JSON.stringify(JSON.parse('{"a":1,"drop":2,"l":[1,2,3]}', (k, v) => (k === "drop" ? undefined : v)))`,
23
+ `JSON.stringify(JSON.parse('[5,6]', function (k, v) { return k === "" ? v : k + ":" + v + ":" + Array.isArray(this); }))`,
24
+ `JSON.stringify(JSON.parse('{"x":1}', (k, v) => (k === "" ? "root:" + typeof v : v)))`,
20
25
  ];
21
26
  const handler = `export default {fetch() {
22
27
  const U = URL;
@@ -18,7 +18,9 @@ const CASES = [
18
18
  `new Request(new URL("http://a.test/u")).url`,
19
19
  `new Request("http://a.test/x", { method: "post" }).method`,
20
20
  ];
21
- const handler = `export default {async fetch() {
21
+ const handler = `export default {async fetch(request) {
22
+ // The incoming request: its body is a ReadableStream, and text() still reads it.
23
+ if (request.method === "POST") return new Response((request.body instanceof ReadableStream) + ":" + (await request.text()));
22
24
  const cases = [
23
25
  ${CASES.map((c) => ` () => (${c}),`).join("\n")}
24
26
  ];
@@ -91,7 +93,9 @@ try {
91
93
  const got = JSON.parse(await (await fetch(`http://127.0.0.1:${port}/`)).text()) as string[];
92
94
  const diffs = CASES.flatMap((c, i) => (got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`]));
93
95
  assert.deepEqual(diffs, [], `Request copies differ from Bun in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`);
94
- console.log(`Native Request copies match Bun in all ${CASES.length} cases`);
96
+ const posted = await (await fetch(`http://127.0.0.1:${port}/`, { method: "POST", body: "posted é" })).text();
97
+ assert.equal(posted, "true:posted é", "the incoming request body is a stream and text() reads it");
98
+ console.log(`Native Request copies match Bun in all ${CASES.length} cases; the incoming body is a stream`);
95
99
  } finally {
96
100
  if (server) {
97
101
  server.kill(9);