@sproutboat/runtime 0.16.0 → 0.17.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.
@@ -1,37 +1,6 @@
1
1
  // WEB STREAMS: generated by gen-streams.ts, do not edit.
2
2
  /* oxlint-disable -- generated third-party implementation */
3
-
4
- var __defProp = Object.defineProperty;
5
- var __returnValue = (v) => v;
6
- function __exportSetter(name, newValue) {
7
- this[name] = __returnValue.bind(null, newValue);
8
- }
9
- var __export = (target, all) => {
10
- for (var name in all)
11
- __defProp(target, name, {
12
- get: all[name],
13
- enumerable: true,
14
- configurable: true,
15
- set: __exportSetter.bind(all, name)
16
- });
17
- };
18
3
  // web-streams-polyfill 4.3.0, MIT, copyright Mattias Buelens (see WEB_STREAMS_LICENSE)
19
- var exports_ponyfill = {};
20
- __export(exports_ponyfill, {
21
- ByteLengthQueuingStrategy: () => ByteLengthQueuingStrategy,
22
- CountQueuingStrategy: () => CountQueuingStrategy,
23
- ReadableByteStreamController: () => ReadableByteStreamController,
24
- ReadableStream: () => ReadableStream,
25
- ReadableStreamBYOBReader: () => ReadableStreamBYOBReader,
26
- ReadableStreamBYOBRequest: () => ReadableStreamBYOBRequest,
27
- ReadableStreamDefaultController: () => ReadableStreamDefaultController,
28
- ReadableStreamDefaultReader: () => ReadableStreamDefaultReader,
29
- TransformStream: () => TransformStream,
30
- TransformStreamDefaultController: () => TransformStreamDefaultController,
31
- WritableStream: () => WritableStream,
32
- WritableStreamDefaultController: () => WritableStreamDefaultController,
33
- WritableStreamDefaultWriter: () => WritableStreamDefaultWriter
34
- });
35
4
  function e() {}
36
5
  function t(e) {
37
6
  return typeof e == "object" && e !== null || typeof e == "function";
@@ -332,8 +301,7 @@
332
301
  return ue(n, 0, e, t, o), n;
333
302
  }
334
303
  function he(e, t) {
335
- let r = e[t];
336
- if (r == null && t === Symbol.iterator && Array.isArray(e)) r = () => e.values();
304
+ const r = e[t];
337
305
  if (r != null) {
338
306
  if (typeof r != "function")
339
307
  throw new TypeError(`${String(t)} is not a function`);
@@ -2307,10 +2275,32 @@
2307
2275
  }
2308
2276
  Object.defineProperties(TransformStreamDefaultController.prototype, { enqueue: { enumerable: true }, error: { enumerable: true }, terminate: { enumerable: true }, desiredSize: { enumerable: true } }), o(TransformStreamDefaultController.prototype.enqueue, "enqueue"), o(TransformStreamDefaultController.prototype.error, "error"), o(TransformStreamDefaultController.prototype.terminate, "terminate"), typeof Symbol.toStringTag == "symbol" && Object.defineProperty(TransformStreamDefaultController.prototype, Symbol.toStringTag, { value: "TransformStreamDefaultController", configurable: true });
2309
2277
  // streams-entry.js
2310
- for (const name of Object.keys(exports_ponyfill)) {
2311
- if (globalThis[name] == null)
2312
- globalThis[name] = exports_ponyfill[name];
2313
- }
2278
+ if (globalThis.ByteLengthQueuingStrategy == null)
2279
+ globalThis.ByteLengthQueuingStrategy = ByteLengthQueuingStrategy;
2280
+ if (globalThis.CountQueuingStrategy == null)
2281
+ globalThis.CountQueuingStrategy = CountQueuingStrategy;
2282
+ if (globalThis.ReadableByteStreamController == null)
2283
+ globalThis.ReadableByteStreamController = ReadableByteStreamController;
2284
+ if (globalThis.ReadableStream == null)
2285
+ globalThis.ReadableStream = ReadableStream;
2286
+ if (globalThis.ReadableStreamBYOBReader == null)
2287
+ globalThis.ReadableStreamBYOBReader = ReadableStreamBYOBReader;
2288
+ if (globalThis.ReadableStreamBYOBRequest == null)
2289
+ globalThis.ReadableStreamBYOBRequest = ReadableStreamBYOBRequest;
2290
+ if (globalThis.ReadableStreamDefaultController == null)
2291
+ globalThis.ReadableStreamDefaultController = ReadableStreamDefaultController;
2292
+ if (globalThis.ReadableStreamDefaultReader == null)
2293
+ globalThis.ReadableStreamDefaultReader = ReadableStreamDefaultReader;
2294
+ if (globalThis.TransformStream == null)
2295
+ globalThis.TransformStream = TransformStream;
2296
+ if (globalThis.TransformStreamDefaultController == null)
2297
+ globalThis.TransformStreamDefaultController = TransformStreamDefaultController;
2298
+ if (globalThis.WritableStream == null)
2299
+ globalThis.WritableStream = WritableStream;
2300
+ if (globalThis.WritableStreamDefaultController == null)
2301
+ globalThis.WritableStreamDefaultController = WritableStreamDefaultController;
2302
+ if (globalThis.WritableStreamDefaultWriter == null)
2303
+ globalThis.WritableStreamDefaultWriter = WritableStreamDefaultWriter;
2314
2304
 
2315
2305
  /* oxlint-enable */
2316
2306
  // END WEB STREAMS
@@ -2435,8 +2425,38 @@ function __sbCollectionIterator(owner, kind, formData) {
2435
2425
 
2436
2426
  class __SproutboatURLSearchParams {
2437
2427
  constructor(init) {
2428
+ this._url = null;
2429
+ this._reset(init);
2430
+ }
2431
+ // A URL's searchParams writes back into it (_url), and is re-read when the URL's
2432
+ // search is assigned (_reset).
2433
+ _reset(init) {
2438
2434
  this._keys = [];
2439
2435
  this._vals = [];
2436
+ // The URLSearchParams init forms: another URLSearchParams, a sequence of [name, value]
2437
+ // pairs, a record of own string keys, or a query string. Only strings were read, so
2438
+ // new URLSearchParams({ a: "1" }) serialized as "%5Bobject+Object%5D=" (W2).
2439
+ if (init instanceof __SproutboatURLSearchParams) {
2440
+ this._keys = init._keys.slice();
2441
+ this._vals = init._vals.slice();
2442
+ return;
2443
+ }
2444
+ if (init != null && Object(init) === init) {
2445
+ if (__sbIsFn(init[Symbol.iterator])) {
2446
+ for (const pair of init) {
2447
+ const items = Array.from(pair);
2448
+ if (items.length !== 2) throw new TypeError("URLSearchParams: each pair must have exactly two items");
2449
+ this._keys.push(String(items[0]));
2450
+ this._vals.push(String(items[1]));
2451
+ }
2452
+ return;
2453
+ }
2454
+ for (const key of Object.keys(init)) {
2455
+ this._keys.push(key);
2456
+ this._vals.push(String(init[key]));
2457
+ }
2458
+ return;
2459
+ }
2440
2460
  let raw = init == null ? "" : String(init);
2441
2461
  if (raw.charCodeAt(0) === 63) raw = raw.slice(1); // strip a leading '?'
2442
2462
  if (raw.length === 0) return;
@@ -2451,6 +2471,11 @@ class __SproutboatURLSearchParams {
2451
2471
  this._vals.push(decodeURIComponent(v.split("+").join(" ")));
2452
2472
  }
2453
2473
  }
2474
+ _update() {
2475
+ if (this._url == null) return;
2476
+ const s = this.toString();
2477
+ this._url._query = s === "" ? null : s;
2478
+ }
2454
2479
  get(name) {
2455
2480
  for (let i = 0; i < this._keys.length; i++) if (this._keys[i] === name) return this._vals[i];
2456
2481
  return null;
@@ -2467,12 +2492,10 @@ class __SproutboatURLSearchParams {
2467
2492
  forEach(cb) {
2468
2493
  for (let i = 0; i < this._keys.length; i++) cb(this._vals[i], this._keys[i], this);
2469
2494
  }
2470
- // Mutators: standalone `new URLSearchParams()` building works. They do NOT
2471
- // write back into a URL's `search` (Porffor's URL has no setter) — build the
2472
- // string with toString() and assign it yourself.
2473
2495
  append(name, value) {
2474
2496
  this._keys.push(String(name));
2475
2497
  this._vals.push(String(value));
2498
+ this._update();
2476
2499
  }
2477
2500
  set(name, value) {
2478
2501
  let found = false;
@@ -2487,7 +2510,11 @@ class __SproutboatURLSearchParams {
2487
2510
  found = true;
2488
2511
  }
2489
2512
  }
2490
- if (!found) this.append(name, value);
2513
+ if (!found) {
2514
+ this._keys.push(String(name));
2515
+ this._vals.push(String(value));
2516
+ }
2517
+ this._update();
2491
2518
  }
2492
2519
  delete(name) {
2493
2520
  for (let i = 0; i < this._keys.length; i++)
@@ -2496,13 +2523,15 @@ class __SproutboatURLSearchParams {
2496
2523
  this._vals.splice(i, 1);
2497
2524
  i--;
2498
2525
  }
2526
+ this._update();
2499
2527
  }
2500
2528
  sort() {
2501
2529
  const idx = this._keys
2502
2530
  .map((_, i) => i)
2503
- .sort((a, b) => (this._keys[a] < this._keys[b] ? -1 : this._keys[a] > this._keys[b] ? 1 : 0));
2531
+ .sort((a, b) => (this._keys[a] < this._keys[b] ? -1 : this._keys[a] > this._keys[b] ? 1 : a - b));
2504
2532
  this._keys = idx.map((i) => this._keys[i]);
2505
2533
  this._vals = idx.map((i) => this._vals[i]);
2534
+ this._update();
2506
2535
  }
2507
2536
  keys() {
2508
2537
  return __sbCollectionIterator(this, 0, false);
@@ -2517,15 +2546,34 @@ class __SproutboatURLSearchParams {
2517
2546
  get size() {
2518
2547
  return this._keys.length;
2519
2548
  }
2549
+ // application/x-www-form-urlencoded: space is "+", and ! ' ( ) ~ are escaped too.
2520
2550
  toString() {
2521
2551
  let out = "";
2522
2552
  for (let i = 0; i < this._keys.length; i++) {
2523
2553
  if (i > 0) out += "&";
2524
- out += encodeURIComponent(this._keys[i]) + "=" + encodeURIComponent(this._vals[i]);
2554
+ out += __sbFormEncode(this._keys[i]) + "=" + __sbFormEncode(this._vals[i]);
2525
2555
  }
2526
2556
  return out;
2527
2557
  }
2528
2558
  }
2559
+ // One pass over encodeURIComponent's output (chained split/join doubled compile time).
2560
+ function __sbFormEncode(s) {
2561
+ const e = encodeURIComponent(s);
2562
+ let out = "";
2563
+ for (let i = 0; i < e.length; i++) {
2564
+ const c = e.charCodeAt(i);
2565
+ if (c === 33) out += "%21";
2566
+ else if (c === 39) out += "%27";
2567
+ else if (c === 40) out += "%28";
2568
+ else if (c === 41) out += "%29";
2569
+ else if (c === 126) out += "%7E";
2570
+ else if (c === 37 && e.charCodeAt(i + 1) === 50 && e.charCodeAt(i + 2) === 48) {
2571
+ out += "+";
2572
+ i += 2;
2573
+ } else out += e[i];
2574
+ }
2575
+ return out;
2576
+ }
2529
2577
 
2530
2578
  // URL and Response are always defined by Porffor's fetch-globals.js banner.
2531
2579
  if (globalThis.URLSearchParams == null) globalThis.URLSearchParams = __SproutboatURLSearchParams;
@@ -2542,19 +2590,39 @@ if (!("searchParams" in URL.prototype)) {
2542
2590
 
2543
2591
  if (Response.json == null) {
2544
2592
  Response.json = function (data, init) {
2545
- const response = new Response(JSON.stringify(data), init);
2546
- if (!response.headers.has("content-type")) response.headers.set("content-type", "application/json;charset=utf-8");
2547
- return response;
2593
+ // Typed before construction: a string body would otherwise default to text/plain.
2594
+ const headers = new Headers(init?.headers);
2595
+ if (!headers.has("content-type")) headers.set("content-type", "application/json");
2596
+ return new Response(JSON.stringify(data), { status: init?.status ?? 200, statusText: init?.statusText, headers });
2548
2597
  };
2549
2598
  }
2550
2599
 
2600
+ // Error.stack: Porffor keeps no frames, so give V8's first line, "Name: message", rather
2601
+ // than undefined (code logs it or splits it on newlines).
2602
+ for (const E of [Error, TypeError, RangeError, ReferenceError, SyntaxError, EvalError, URIError]) {
2603
+ if (!("stack" in E.prototype)) {
2604
+ Object.defineProperty(E.prototype, "stack", {
2605
+ configurable: true,
2606
+ get() {
2607
+ const message = String(this.message);
2608
+ return message === "" ? String(this.name) : String(this.name) + ": " + message;
2609
+ },
2610
+ });
2611
+ }
2612
+ }
2613
+
2614
+ // Fetch: Headers iterates like entries(), so new Map(headers), [...headers] and
2615
+ // Object.fromEntries(headers) work. Porffor's Headers has entries() but no iterator.
2616
+ if (Headers.prototype[Symbol.iterator] == null) Headers.prototype[Symbol.iterator] = Headers.prototype.entries;
2617
+
2551
2618
  // Porffor alpha-10 has no static Response.redirect().
2552
2619
  if (Response.redirect == null) {
2553
2620
  Response.redirect = function (url, status = 302) {
2554
2621
  if (status !== 301 && status !== 302 && status !== 303 && status !== 307 && status !== 308) {
2555
2622
  throw new RangeError("invalid redirect status");
2556
2623
  }
2557
- return new Response("", { status, headers: { location: String(url) } });
2624
+ // Serialized like the URL parser does (a bare origin gains its "/"); invalid URLs throw.
2625
+ return new Response(null, { status, headers: { location: new URL(String(url)).href } });
2558
2626
  };
2559
2627
  }
2560
2628
 
@@ -2626,11 +2694,21 @@ if (globalThis.FormData == null) {
2626
2694
  constructor() {
2627
2695
  this._entries = [];
2628
2696
  }
2697
+ // A Blob value is stored as a File named by filename ("blob" by default); anything
2698
+ // else as a string, as the FormData spec's entry list does.
2699
+ static _entryValue(value, filename) {
2700
+ if (value instanceof Blob) {
2701
+ if (filename === undefined && value instanceof File) return value;
2702
+ return new File([value], filename === undefined ? "blob" : String(filename), { type: value.type });
2703
+ }
2704
+ return String(value);
2705
+ }
2629
2706
  append(name, value, filename) {
2630
- this._entries.push([String(name), value, filename]);
2707
+ this._entries.push([String(name), __SproutboatFormData._entryValue(value, filename), filename]);
2631
2708
  }
2632
2709
  set(name, value, filename) {
2633
2710
  name = String(name);
2711
+ value = __SproutboatFormData._entryValue(value, filename);
2634
2712
  let replaced = false;
2635
2713
  this._entries = this._entries.filter((e) => {
2636
2714
  if (e[0] !== name) return true;
@@ -2786,7 +2864,12 @@ if (globalThis.crypto.getRandomValues == null) {
2786
2864
  globalThis.crypto.getRandomValues = function (view) {
2787
2865
  const n = view.length >>> 0;
2788
2866
  // WebCrypto caps a single call at 65536 bytes.
2789
- if (n > 65536) throw new RangeError("crypto.getRandomValues: byte length exceeds 65536");
2867
+ if (n > 65536) {
2868
+ // WebCrypto's error for an oversized request (a DOMException named QuotaExceededError).
2869
+ const e = new Error("crypto.getRandomValues: byte length exceeds 65536");
2870
+ e.name = "QuotaExceededError";
2871
+ throw e;
2872
+ }
2790
2873
  if (n === 0) return view;
2791
2874
  // One CSPRNG byte per element. Correct for Uint8Array (and randomUUID); a
2792
2875
  // wider view gets its low byte filled, matching the previous polyfill's shape.
@@ -2796,17 +2879,282 @@ if (globalThis.crypto.getRandomValues == null) {
2796
2879
  return view;
2797
2880
  };
2798
2881
  }
2799
- // structuredClone: JSON round-trip. Lossy (no Map/Set/Date/typed arrays), but
2800
- // covers the common "deep-copy a plain object" case Worker code relies on.
2882
+ // queueMicrotask, and atob/btoa input checks as the HTML spec has them: atob drops ASCII
2883
+ // whitespace and rejects anything that is not base64, btoa rejects characters above
2884
+ // U+00FF. Both threw nothing and returned garbage before (W2 conformance).
2885
+ {
2886
+ const invalidCharacter = (message) => {
2887
+ const e = new Error(message);
2888
+ e.name = "InvalidCharacterError";
2889
+ return e;
2890
+ };
2891
+ if (globalThis.queueMicrotask == null) {
2892
+ globalThis.queueMicrotask = function (callback) {
2893
+ if (!__sbIsFn(callback)) throw new TypeError("queueMicrotask needs a function");
2894
+ Promise.resolve().then(() => callback());
2895
+ };
2896
+ }
2897
+ const nativeAtob = globalThis.atob, nativeBtoa = globalThis.btoa;
2898
+ globalThis.atob = function (data) {
2899
+ let text = String(data).replace(/[\t\n\f\r ]/g, "");
2900
+ if (text.length % 4 === 0) text = text.replace(/==?$/, "");
2901
+ if (text.length % 4 === 1 || !/^[A-Za-z0-9+/]*$/.test(text)) throw invalidCharacter("atob: the string is not valid base64");
2902
+ while (text.length % 4 !== 0) text += "=";
2903
+ return nativeAtob(text);
2904
+ };
2905
+ globalThis.btoa = function (data) {
2906
+ const text = String(data);
2907
+ for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) > 0xff) throw invalidCharacter("btoa: the string has characters outside Latin-1");
2908
+ return nativeBtoa(text);
2909
+ };
2910
+ }
2911
+
2912
+ /* oxlint-disable anti-slop/no-runtime-typeof -- these Web API polyfills branch on JavaScript types
2913
+ exactly where their specs do (listener: function or { handleEvent }; structured clone: by value type). */
2914
+ // Event, CustomEvent, EventTarget, AbortSignal and AbortController, as workerd has
2915
+ // them (no DOM tree, so no bubbling or capture phases). Found missing by the W2
2916
+ // conformance suite; libraries use AbortController for fetch timeouts and EventTarget
2917
+ // for their own events.
2918
+ if (typeof globalThis.EventTarget !== "function") {
2919
+ const namedError = (name, message) => {
2920
+ const e = new Error(message);
2921
+ e.name = name;
2922
+ return e;
2923
+ };
2924
+ class Event {
2925
+ constructor(type, init) {
2926
+ if (arguments.length === 0) throw new TypeError("Event requires a type");
2927
+ const options = init || {};
2928
+ this.type = String(type);
2929
+ this.bubbles = !!options.bubbles;
2930
+ this.cancelable = !!options.cancelable;
2931
+ this.composed = !!options.composed;
2932
+ this.defaultPrevented = false;
2933
+ this.target = null;
2934
+ this.currentTarget = null;
2935
+ this.eventPhase = 0;
2936
+ this.isTrusted = false;
2937
+ this.timeStamp = Date.now();
2938
+ this.__sbStopped = false;
2939
+ }
2940
+ preventDefault() { if (this.cancelable) this.defaultPrevented = true; }
2941
+ stopPropagation() {}
2942
+ stopImmediatePropagation() { this.__sbStopped = true; }
2943
+ composedPath() { return this.currentTarget ? [this.currentTarget] : []; }
2944
+ }
2945
+ class CustomEvent extends Event {
2946
+ constructor(type, init) {
2947
+ super(type, init);
2948
+ this.detail = init && init.detail !== undefined ? init.detail : null;
2949
+ }
2950
+ }
2951
+ class EventTarget {
2952
+ constructor() { this.__sbListeners = {}; }
2953
+ addEventListener(type, listener, options) {
2954
+ if (listener == null) return;
2955
+ const capture = typeof options === "boolean" ? options : !!(options && options.capture);
2956
+ const once = !!(options && typeof options === "object" && options.once);
2957
+ const signal = options && typeof options === "object" ? options.signal : undefined;
2958
+ if (signal && signal.aborted) return;
2959
+ const list = this.__sbListeners[type] || (this.__sbListeners[type] = []);
2960
+ for (const entry of list) if (entry.listener === listener && entry.capture === capture) return;
2961
+ const entry = { listener, capture, once, removed: false };
2962
+ list.push(entry);
2963
+ if (signal) signal.addEventListener("abort", () => this.removeEventListener(type, listener, capture));
2964
+ }
2965
+ removeEventListener(type, listener, options) {
2966
+ const capture = typeof options === "boolean" ? options : !!(options && options.capture);
2967
+ const list = this.__sbListeners[type];
2968
+ if (!list) return;
2969
+ for (let i = 0; i < list.length; i++) {
2970
+ if (list[i].listener === listener && list[i].capture === capture) {
2971
+ list[i].removed = true;
2972
+ list.splice(i, 1);
2973
+ return;
2974
+ }
2975
+ }
2976
+ }
2977
+ dispatchEvent(event) {
2978
+ if (!(event instanceof Event)) throw new TypeError("dispatchEvent needs an Event");
2979
+ event.target = this;
2980
+ event.currentTarget = this;
2981
+ event.eventPhase = 2;
2982
+ const list = (this.__sbListeners[event.type] || []).slice();
2983
+ for (const entry of list) {
2984
+ if (entry.removed) continue;
2985
+ if (entry.once) this.removeEventListener(event.type, entry.listener, entry.capture);
2986
+ const fn = entry.listener;
2987
+ if (typeof fn === "function") fn.call(this, event);
2988
+ else if (fn && typeof fn.handleEvent === "function") fn.handleEvent(event);
2989
+ if (event.__sbStopped) break;
2990
+ }
2991
+ event.currentTarget = null;
2992
+ event.eventPhase = 0;
2993
+ return !event.defaultPrevented;
2994
+ }
2995
+ }
2996
+ class AbortSignal extends EventTarget {
2997
+ constructor() {
2998
+ super();
2999
+ this.aborted = false;
3000
+ this.reason = undefined;
3001
+ this.onabort = null;
3002
+ }
3003
+ throwIfAborted() { if (this.aborted) throw this.reason; }
3004
+ __sbAbort(reason) {
3005
+ if (this.aborted) return;
3006
+ this.aborted = true;
3007
+ this.reason = reason === undefined ? namedError("AbortError", "This operation was aborted") : reason;
3008
+ const event = new Event("abort");
3009
+ if (typeof this.onabort === "function") this.onabort.call(this, event);
3010
+ this.dispatchEvent(event);
3011
+ }
3012
+ static abort(reason) {
3013
+ const signal = new AbortSignal();
3014
+ signal.__sbAbort(reason);
3015
+ return signal;
3016
+ }
3017
+ static timeout(ms) {
3018
+ const signal = new AbortSignal();
3019
+ setTimeout(() => signal.__sbAbort(namedError("TimeoutError", "The operation timed out")), ms);
3020
+ return signal;
3021
+ }
3022
+ static any(signals) {
3023
+ const signal = new AbortSignal();
3024
+ for (const s of signals) {
3025
+ if (s.aborted) {
3026
+ signal.__sbAbort(s.reason);
3027
+ return signal;
3028
+ }
3029
+ }
3030
+ for (const s of signals) s.addEventListener("abort", () => signal.__sbAbort(s.reason));
3031
+ return signal;
3032
+ }
3033
+ }
3034
+ class AbortController {
3035
+ constructor() { this.signal = new AbortSignal(); }
3036
+ abort(reason) { this.signal.__sbAbort(reason); }
3037
+ }
3038
+ globalThis.Event = Event;
3039
+ globalThis.CustomEvent = CustomEvent;
3040
+ globalThis.EventTarget = EventTarget;
3041
+ globalThis.AbortSignal = AbortSignal;
3042
+ globalThis.AbortController = AbortController;
3043
+ }
3044
+
3045
+ // TextEncoderStream and TextDecoderStream over the Web Streams ponyfill (W2 conformance).
3046
+ // The encoder holds back a lone high surrogate at the end of a chunk until the next one;
3047
+ // the decoder is TextDecoder in streaming mode, so a character split across chunks decodes.
3048
+ if (globalThis.TextEncoderStream == null) {
3049
+ class TextEncoderStream {
3050
+ constructor() {
3051
+ const encoder = new TextEncoder();
3052
+ let pending = "";
3053
+ const transform = new TransformStream({
3054
+ transform(chunk, controller) {
3055
+ let text = pending + String(chunk);
3056
+ pending = "";
3057
+ const last = text.charCodeAt(text.length - 1);
3058
+ if (last >= 0xd800 && last <= 0xdbff) {
3059
+ pending = text.slice(-1);
3060
+ text = text.slice(0, -1);
3061
+ }
3062
+ if (text.length > 0) controller.enqueue(encoder.encode(text));
3063
+ },
3064
+ flush(controller) {
3065
+ if (pending.length > 0) controller.enqueue(encoder.encode(pending));
3066
+ },
3067
+ });
3068
+ this.readable = transform.readable;
3069
+ this.writable = transform.writable;
3070
+ }
3071
+ get encoding() {
3072
+ return "utf-8";
3073
+ }
3074
+ }
3075
+ class TextDecoderStream {
3076
+ constructor(label = "utf-8", options = undefined) {
3077
+ const decoder = new TextDecoder(label, options);
3078
+ this.encoding = decoder.encoding;
3079
+ this.fatal = decoder.fatal;
3080
+ this.ignoreBOM = decoder.ignoreBOM;
3081
+ const transform = new TransformStream({
3082
+ transform(chunk, controller) {
3083
+ const text = decoder.decode(chunk, { stream: true });
3084
+ if (text.length > 0) controller.enqueue(text);
3085
+ },
3086
+ flush(controller) {
3087
+ const text = decoder.decode();
3088
+ if (text.length > 0) controller.enqueue(text);
3089
+ },
3090
+ });
3091
+ this.readable = transform.readable;
3092
+ this.writable = transform.writable;
3093
+ }
3094
+ }
3095
+ globalThis.TextEncoderStream = TextEncoderStream;
3096
+ globalThis.TextDecoderStream = TextDecoderStream;
3097
+ }
3098
+
3099
+ // structuredClone: the HTML structured-clone algorithm for the types Worker code
3100
+ // passes around. Keeps cycles and shared references (a memo per call), Date, RegExp,
3101
+ // Map, Set, ArrayBuffer, typed arrays, DataView, errors, arrays and plain objects;
3102
+ // functions and symbols throw DataCloneError. It replaced a JSON round-trip that lost
3103
+ // every non-JSON type and crashed on cycles (W2 conformance).
2801
3104
  if (globalThis.structuredClone == null) {
2802
- // The suggested fix (use structuredClone) is circular — this IS the polyfill,
2803
- // and Porffor exposes no other deep-clone primitive.
2804
- // react-doctor-disable-next-line react-doctor/no-json-parse-stringify-clone
3105
+ const dataCloneError = (what) => {
3106
+ const e = new Error(what + " could not be cloned");
3107
+ e.name = "DataCloneError";
3108
+ return e;
3109
+ };
3110
+ const errorTypes = { Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError };
3111
+ const clone = (value, memo) => {
3112
+ const type = typeof value;
3113
+ if (type === "function") throw dataCloneError("function");
3114
+ if (type === "symbol") throw dataCloneError("Symbol");
3115
+ if (value === null || type !== "object") return value;
3116
+ if (memo.has(value)) return memo.get(value);
3117
+ let out;
3118
+ if (value instanceof Date) out = new Date(value.getTime());
3119
+ else if (value instanceof RegExp) out = new RegExp(value.source, value.flags);
3120
+ else if (value instanceof ArrayBuffer) out = value.slice(0);
3121
+ else if (ArrayBuffer.isView(value)) {
3122
+ const buffer = clone(value.buffer, memo);
3123
+ out = value instanceof DataView ? new DataView(buffer, value.byteOffset, value.byteLength) : new value.constructor(buffer, value.byteOffset, value.length);
3124
+ } else if (value instanceof Map) {
3125
+ out = new Map();
3126
+ memo.set(value, out);
3127
+ for (const [k, v] of value) out.set(clone(k, memo), clone(v, memo));
3128
+ return out;
3129
+ } else if (value instanceof Set) {
3130
+ out = new Set();
3131
+ memo.set(value, out);
3132
+ for (const v of value) out.add(clone(v, memo));
3133
+ return out;
3134
+ } else if (value instanceof Error) {
3135
+ const Ctor = errorTypes[value.name] || Error;
3136
+ out = new Ctor(value.message);
3137
+ } else if (Array.isArray(value)) {
3138
+ out = new Array(value.length);
3139
+ memo.set(value, out);
3140
+ for (const k of Object.keys(value)) out[k] = clone(value[k], memo);
3141
+ return out;
3142
+ } else {
3143
+ out = {};
3144
+ memo.set(value, out);
3145
+ for (const k of Object.keys(value)) out[k] = clone(value[k], memo);
3146
+ return out;
3147
+ }
3148
+ memo.set(value, out);
3149
+ return out;
3150
+ };
2805
3151
  globalThis.structuredClone = function (value) {
2806
- return JSON.parse(JSON.stringify(value));
3152
+ return clone(value, new Map());
2807
3153
  };
2808
3154
  }
2809
3155
 
3156
+ /* oxlint-enable anti-slop/no-runtime-typeof */
3157
+
2810
3158
  if (globalThis.crypto.randomUUID == null) {
2811
3159
  globalThis.crypto.randomUUID = function () {
2812
3160
  const b = new Uint8Array(16);
@@ -2827,6 +3175,8 @@ if (globalThis.crypto.randomUUID == null) {
2827
3175
  // dependency, so it works on the broker transport too.
2828
3176
  function __sbHashBits(algo) {
2829
3177
  const n = String((algo && algo.name) || algo || "").toUpperCase();
3178
+ if (n === "SHA-1" || n === "SHA1") return "1";
3179
+ if (n === "MD5") return "5";
2830
3180
  if (n === "SHA-256" || n === "SHA256") return "256";
2831
3181
  if (n === "SHA-384" || n === "SHA384") return "384";
2832
3182
  if (n === "SHA-512" || n === "SHA512") return "512";
@@ -3053,42 +3403,77 @@ function __sbHexOrBytes(value) {
3053
3403
  return __sbToBytes(value);
3054
3404
  }
3055
3405
 
3406
+ // Errors as WebCrypto names them (DOMException names; plain Errors here).
3407
+ function __sbCryptoError(name, message) {
3408
+ const e = new Error(message);
3409
+ e.name = name;
3410
+ return e;
3411
+ }
3412
+ // An HMAC key as importKey and generateKey return it. MD5 is digest-only, as in workerd.
3413
+ function __sbHmacKey(bytes, bits, extractable, usages) {
3414
+ const hash = bits === "1" ? "SHA-1" : "SHA-" + bits;
3415
+ return {
3416
+ type: "secret",
3417
+ extractable: !!extractable,
3418
+ algorithm: { name: "HMAC", hash: { name: hash }, length: bytes.length * 8 },
3419
+ usages: usages || [],
3420
+ __sbKey: bytes,
3421
+ __sbBits: bits,
3422
+ };
3423
+ }
3424
+ function __sbHmacBits(algo) {
3425
+ const bits = __sbHashBits((algo && algo.hash) || "SHA-256");
3426
+ if (!bits || bits === "5") throw __sbCryptoError("NotSupportedError", "HMAC needs SHA-1, SHA-256, SHA-384 or SHA-512");
3427
+ return bits;
3428
+ }
3056
3429
  if (globalThis.crypto.subtle == null) {
3057
3430
  globalThis.crypto.subtle = {
3058
3431
  async digest(algo, data) {
3059
3432
  const bits = __sbHashBits(algo);
3060
- if (!bits) throw new Error("crypto.subtle.digest: unsupported algorithm");
3433
+ if (!bits) throw __sbCryptoError("NotSupportedError", "crypto.subtle.digest: unsupported algorithm");
3061
3434
  const out = __sbDigestRaw(bits, __sbToBytes(data));
3062
3435
  if (!out) throw new Error("crypto.subtle.digest failed");
3063
3436
  return __sbBufFrom(out);
3064
3437
  },
3065
3438
  async importKey(format, keyData, algo, extractable, usages) {
3066
- if (format !== "raw") throw new Error("crypto.subtle.importKey: only format 'raw' is supported");
3439
+ if (format !== "raw") throw __sbCryptoError("NotSupportedError", "crypto.subtle.importKey: only format 'raw' is supported");
3067
3440
  const name = String((algo && algo.name) || algo || "").toUpperCase();
3068
- if (name !== "HMAC") throw new Error("crypto.subtle.importKey: only HMAC keys are supported");
3069
- const bits = __sbHashBits((algo && algo.hash) || "SHA-256");
3070
- if (!bits) throw new Error("crypto.subtle.importKey: unsupported hash");
3071
- return {
3072
- type: "secret",
3073
- extractable: !!extractable,
3074
- algorithm: { name: "HMAC", hash: { name: "SHA-" + bits } },
3075
- usages: usages || [],
3076
- __sbKey: __sbToBytes(keyData),
3077
- __sbBits: bits,
3078
- };
3441
+ if (name !== "HMAC") throw __sbCryptoError("NotSupportedError", "crypto.subtle.importKey: only HMAC keys are supported");
3442
+ return __sbHmacKey(__sbToBytes(keyData), __sbHmacBits(algo), extractable, usages);
3443
+ },
3444
+ async exportKey(format, key) {
3445
+ if (!key || key.__sbKey == null) throw new TypeError("crypto.subtle.exportKey: not a key from this runtime");
3446
+ if (format !== "raw") throw __sbCryptoError("NotSupportedError", "crypto.subtle.exportKey: only format 'raw' is supported");
3447
+ if (!key.extractable) throw __sbCryptoError("InvalidAccessError", "crypto.subtle.exportKey: the key is not extractable");
3448
+ return __sbBufFrom(key.__sbKey);
3449
+ },
3450
+ async generateKey(algo, extractable, usages) {
3451
+ const name = String((algo && algo.name) || algo || "").toUpperCase();
3452
+ if (name !== "HMAC") throw __sbCryptoError("NotSupportedError", "crypto.subtle.generateKey: only HMAC keys are supported");
3453
+ const bits = __sbHmacBits(algo);
3454
+ // The default length is the hash's block size: 512 bits, or 1024 for SHA-384/512.
3455
+ const length = (algo && algo.length) || (bits === "384" || bits === "512" ? 1024 : 512);
3456
+ const bytes = __sbRandomBytes(String(Math.ceil(length / 8)));
3457
+ return __sbHmacKey(bytes, bits, extractable, usages);
3079
3458
  },
3080
3459
  async sign(algo, key, data) {
3081
- if (!key || key.__sbKey == null) throw new Error("crypto.subtle.sign: only HMAC keys from importKey are supported");
3460
+ if (!key || key.__sbKey == null) throw __sbCryptoError("InvalidAccessError", "crypto.subtle.sign: only HMAC keys are supported");
3082
3461
  const out = __sbHmacRaw(key.__sbBits, key.__sbKey, __sbToBytes(data));
3083
3462
  if (!out) throw new Error("crypto.subtle.sign failed");
3084
3463
  return __sbBufFrom(out);
3085
3464
  },
3086
3465
  async verify(algo, key, signature, data) {
3087
- if (!key || key.__sbKey == null) throw new Error("crypto.subtle.verify: only HMAC keys are supported");
3466
+ if (!key || key.__sbKey == null) throw __sbCryptoError("InvalidAccessError", "crypto.subtle.verify: only HMAC keys are supported");
3088
3467
  const mac = __sbHmacRaw(key.__sbBits, key.__sbKey, __sbToBytes(data));
3089
3468
  if (!mac) return false;
3090
3469
  return __sbEqCt(mac, __sbToBytes(signature));
3091
3470
  },
3471
+ // workerd's extension: constant-time comparison of two equal-length buffers.
3472
+ timingSafeEqual(a, b) {
3473
+ const x = __sbToBytes(a), y = __sbToBytes(b);
3474
+ if (x.length !== y.length) throw new TypeError("crypto.subtle.timingSafeEqual: the buffers differ in length");
3475
+ return __sbEqCt(x, y);
3476
+ },
3092
3477
  };
3093
3478
  }
3094
3479
 
@@ -3291,9 +3676,82 @@ static void sb_sha512_final(sb_sha512_ctx* c, unsigned char* out, int is384) {
3291
3676
  for (int j = 0; j < 8; j++) out[i*8+j] = (unsigned char)(c->h[i] >> (56 - j*8));
3292
3677
  }
3293
3678
 
3294
- // One-shot digest. algo: 256 | 384 | 512. out must hold 32/48/64 bytes.
3679
+ // SHA-1 and MD5 (W2: workerd's digest has both; HMAC also takes SHA-1). One-shot over a
3680
+ // padded copy: inputs are request-sized, not streams.
3681
+ static uint32_t sb_rol32(uint32_t x, int n) { return (x << n) | (x >> (32 - n)); }
3682
+ static unsigned char* sb_md_pad(const unsigned char* msg, size_t len, size_t* total, int big_endian) {
3683
+ *total = ((len + 8) / 64 + 1) * 64;
3684
+ unsigned char* m = (unsigned char*)calloc(*total, 1);
3685
+ if (!m) return 0;
3686
+ memcpy(m, msg, len); m[len] = 0x80;
3687
+ uint64_t bits = (uint64_t)len * 8;
3688
+ for (int i = 0; i < 8; i++) m[big_endian ? *total - 1 - i : *total - 8 + i] = (unsigned char)(bits >> (8 * i));
3689
+ return m;
3690
+ }
3691
+ static size_t sb_sha1(const unsigned char* msg, size_t len, unsigned char out[20]) {
3692
+ size_t total; unsigned char* m = sb_md_pad(msg, len, &total, 1);
3693
+ if (!m) return 0;
3694
+ uint32_t h[5] = { 0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0 };
3695
+ for (size_t off = 0; off < total; off += 64) {
3696
+ uint32_t w[80];
3697
+ for (int i = 0; i < 16; i++) w[i] = ((uint32_t)m[off + 4*i] << 24) | ((uint32_t)m[off + 4*i + 1] << 16) | ((uint32_t)m[off + 4*i + 2] << 8) | m[off + 4*i + 3];
3698
+ for (int i = 16; i < 80; i++) w[i] = sb_rol32(w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16], 1);
3699
+ uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4];
3700
+ for (int i = 0; i < 80; i++) {
3701
+ uint32_t f, k;
3702
+ if (i < 20) { f = (b & c) | (~b & d); k = 0x5A827999; }
3703
+ else if (i < 40) { f = b ^ c ^ d; k = 0x6ED9EBA1; }
3704
+ else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8F1BBCDC; }
3705
+ else { f = b ^ c ^ d; k = 0xCA62C1D6; }
3706
+ uint32_t t = sb_rol32(a, 5) + f + e + k + w[i];
3707
+ e = d; d = c; c = sb_rol32(b, 30); b = a; a = t;
3708
+ }
3709
+ h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e;
3710
+ }
3711
+ free(m);
3712
+ for (int i = 0; i < 5; i++) { out[4*i] = h[i] >> 24; out[4*i+1] = h[i] >> 16; out[4*i+2] = h[i] >> 8; out[4*i+3] = h[i]; }
3713
+ return 20;
3714
+ }
3715
+ static size_t sb_md5(const unsigned char* msg, size_t len, unsigned char out[16]) {
3716
+ static const uint32_t K[64] = {
3717
+ 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
3718
+ 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
3719
+ 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
3720
+ 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
3721
+ 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
3722
+ 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
3723
+ 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
3724
+ 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391 };
3725
+ static const int S[16] = { 7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21 };
3726
+ size_t total; unsigned char* m = sb_md_pad(msg, len, &total, 0);
3727
+ if (!m) return 0;
3728
+ uint32_t h[4] = { 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476 };
3729
+ for (size_t off = 0; off < total; off += 64) {
3730
+ uint32_t w[16];
3731
+ for (int i = 0; i < 16; i++) w[i] = (uint32_t)m[off + 4*i] | ((uint32_t)m[off + 4*i + 1] << 8) | ((uint32_t)m[off + 4*i + 2] << 16) | ((uint32_t)m[off + 4*i + 3] << 24);
3732
+ uint32_t a = h[0], b = h[1], c = h[2], d = h[3];
3733
+ for (int i = 0; i < 64; i++) {
3734
+ uint32_t f; int g;
3735
+ if (i < 16) { f = (b & c) | (~b & d); g = i; }
3736
+ else if (i < 32) { f = (d & b) | (~d & c); g = (5*i + 1) % 16; }
3737
+ else if (i < 48) { f = b ^ c ^ d; g = (3*i + 5) % 16; }
3738
+ else { f = c ^ (b | ~d); g = (7*i) % 16; }
3739
+ f = f + a + K[i] + w[g];
3740
+ a = d; d = c; c = b;
3741
+ b = b + sb_rol32(f, S[(i / 16) * 4 + i % 4]);
3742
+ }
3743
+ h[0] += a; h[1] += b; h[2] += c; h[3] += d;
3744
+ }
3745
+ free(m);
3746
+ for (int i = 0; i < 4; i++) { out[4*i] = h[i]; out[4*i+1] = h[i] >> 8; out[4*i+2] = h[i] >> 16; out[4*i+3] = h[i] >> 24; }
3747
+ return 16;
3748
+ }
3749
+
3750
+ // One-shot digest. algo: 1 (SHA-1) | 5 (MD5) | 256 | 384 | 512. out must hold up to 64 bytes.
3295
3751
  // Returns the digest length, or 0 for an unknown algo.
3296
3752
  static size_t sb_digest(int algo, const unsigned char* msg, size_t mlen, unsigned char* out) {
3753
+ if (algo == 1) return sb_sha1(msg, mlen, out);
3754
+ if (algo == 5) return sb_md5(msg, mlen, out);
3297
3755
  if (algo == 256) { sb_sha256_ctx c; sb_sha256_init(&c); sb_sha256_update(&c, msg, mlen); sb_sha256_final(&c, out); return 32; }
3298
3756
  if (algo == 384) { sb_sha512_ctx c; sb_sha512_init(&c, 1); sb_sha512_update(&c, msg, mlen); sb_sha512_final(&c, out, 1); return 48; }
3299
3757
  if (algo == 512) { sb_sha512_ctx c; sb_sha512_init(&c, 0); sb_sha512_update(&c, msg, mlen); sb_sha512_final(&c, out, 0); return 64; }
@@ -3303,6 +3761,20 @@ static size_t sb_digest(int algo, const unsigned char* msg, size_t mlen, unsigne
3303
3761
  // HMAC(algo) per RFC 2104. Block size is 64 for SHA-256, 128 for SHA-384/512.
3304
3762
  static size_t sb_hmac(int algo, const unsigned char* key, size_t klen,
3305
3763
  const unsigned char* msg, size_t mlen, unsigned char* out) {
3764
+ if (algo == 1) {
3765
+ // HMAC-SHA-1: block 64, over padded copies like the one-shot digest.
3766
+ unsigned char k1[64], inner1[20];
3767
+ memset(k1, 0, sizeof(k1));
3768
+ if (klen > 64) sb_sha1(key, klen, k1); else memcpy(k1, key, klen);
3769
+ unsigned char* buf = (unsigned char*)malloc(64 + (mlen > 20 ? mlen : 20));
3770
+ if (!buf) return 0;
3771
+ for (int i = 0; i < 64; i++) buf[i] = k1[i] ^ 0x36;
3772
+ memcpy(buf + 64, msg, mlen); sb_sha1(buf, 64 + mlen, inner1);
3773
+ for (int i = 0; i < 64; i++) buf[i] = k1[i] ^ 0x5c;
3774
+ memcpy(buf + 64, inner1, 20); sb_sha1(buf, 84, out);
3775
+ free(buf);
3776
+ return 20;
3777
+ }
3306
3778
  size_t bs = algo == 256 ? 64 : 128;
3307
3779
  unsigned char k[128], ipad[128], opad[128], inner[64];
3308
3780
  memset(k, 0, sizeof(k));
@@ -3712,6 +4184,64 @@ function __sbR2Multipart(bucket, key, uploadId) {
3712
4184
  };
3713
4185
  }
3714
4186
 
4187
+ // Asset rules use the same parsed manifest in both transports. Matching mirrors
4188
+ // @sproutboat/assets; parity tests cover the portable native implementation.
4189
+ function __sbAssetRedirect(rule, path) {
4190
+ const star = rule.from.indexOf("*");
4191
+ if (star !== -1) {
4192
+ const prefix = rule.from.slice(0, star);
4193
+ if (!path.startsWith(prefix)) return null;
4194
+ return rule.to.split(":splat").join(path.slice(prefix.length));
4195
+ }
4196
+ const from = rule.from.split("/").filter(Boolean);
4197
+ const parts = path.split("/").filter(Boolean);
4198
+ if (from.length !== parts.length) return null;
4199
+ const params = {};
4200
+ for (let i = 0; i < from.length; i++) {
4201
+ if (from[i].startsWith(":")) params[from[i].slice(1)] = parts[i];
4202
+ else if (from[i] !== parts[i]) return null;
4203
+ }
4204
+ let to = rule.to;
4205
+ for (const name in params) to = to.split(":" + name).join(params[name]);
4206
+ return to;
4207
+ }
4208
+
4209
+ function __sbAssetHeaderMatches(pattern, path) {
4210
+ const from = pattern.split("/").filter(Boolean);
4211
+ const parts = path.split("/").filter(Boolean);
4212
+ for (let i = 0; i < from.length; i++) {
4213
+ if (from[i] === "*") return true;
4214
+ if (from[i] !== parts[i]) return false;
4215
+ }
4216
+ return from.length === parts.length;
4217
+ }
4218
+
4219
+ function __sbAssetResponse(path, r) {
4220
+ if (r.location != null) return new Response(null, { status: r.status, headers: { location: r.location } });
4221
+ const rules = r.rules || {};
4222
+ const redirects = rules.redirects || [];
4223
+ for (let i = 0; i < redirects.length; i++) {
4224
+ const to = __sbAssetRedirect(redirects[i], path);
4225
+ if (to !== null) return new Response(null, { status: redirects[i].status, headers: { location: to } });
4226
+ }
4227
+ const headers = new Headers();
4228
+ if (r.type) headers.set("content-type", r.type);
4229
+ if (r.found) headers.set("etag", '"' + r.hash + '"');
4230
+ // A custom 404 page is also an asset; a generated Not Found is not.
4231
+ if (r.type) {
4232
+ const list = rules.headers || [];
4233
+ for (let i = 0; i < list.length; i++) {
4234
+ const rule = list[i];
4235
+ if (!__sbAssetHeaderMatches(rule.pattern, path)) continue;
4236
+ for (let j = 0; j < rule.unset.length; j++) headers.delete(rule.unset[j]);
4237
+ for (let j = 0; j < rule.set.length; j++) headers.set(rule.set[j][0], rule.set[j][1]);
4238
+ }
4239
+ }
4240
+ // Apply the internal byte marker after user rules so they cannot remove it.
4241
+ if (r.body != null) headers.set("x-sb-raw-body", "1");
4242
+ return __sbRawBodyResponse(r.body, { status: r.status || (r.found ? 200 : 404), headers });
4243
+ }
4244
+
3715
4245
  // Installed only when the project declares bindings. `env` is the module-scoped
3716
4246
  // object from compile.ts (a `const`, but mutable); we add the binding accessors
3717
4247
  // to it in place. compile.ts emits `__sbInstallBindings(env, {...})` right after
@@ -3935,16 +4465,7 @@ globalThis.__sbInstallBindings = function (target, bindings) {
3935
4465
  } catch {
3936
4466
  /* use as-is */
3937
4467
  }
3938
- const r = globalThis.__sbAssetsGet(path);
3939
- const headers = {};
3940
- if (r.type) headers["content-type"] = r.type;
3941
- if (r.found) headers["etag"] = '"' + r.hash + '"';
3942
- // #176: these bytes came off disk already-finished (UTF-8 text or
3943
- // binary, doesn't matter which) -- the reserved x-sb-raw-body header
3944
- // tells the C write path to pass them through untouched instead of
3945
- // re-encoding as if this were a JS string a handler built.
3946
- if (r.body != null) headers["x-sb-raw-body"] = "1";
3947
- return __sbRawBodyResponse(r.body, { status: r.status || (r.found ? 200 : 404), headers });
4468
+ return __sbAssetResponse(path, globalThis.__sbAssetsGet(path));
3948
4469
  },
3949
4470
  };
3950
4471
  }
@@ -3989,26 +4510,48 @@ globalThis.__sbInstallBindings = function (target, bindings) {
3989
4510
  // prelude must not replace fetch just by being evaluated.
3990
4511
  globalThis.__sbInstallFetch = function () {
3991
4512
  globalThis.fetch = function (input, init) {
3992
- const url = __sbIsStr(input) ? input : String(input.url);
4513
+ // A Request's method, headers and body apply unless init overrides them; a URL
4514
+ // object stands for its href.
4515
+ const req = input != null && !__sbIsStr(input) && input.url != null ? input : null;
4516
+ const url = req ? String(req.url) : input != null && input.href != null ? String(input.href) : String(input);
3993
4517
  const opts = init || {};
4518
+ const source = opts.headers != null ? opts.headers : req ? req.headers : null;
3994
4519
  const headers = [];
3995
- if (opts.headers) {
3996
- if (__sbIsFn(opts.headers.forEach)) opts.headers.forEach((v, k) => headers.push([k, v]));
3997
- else for (const k in opts.headers) headers.push([k, opts.headers[k]]);
3998
- }
3999
- const r = globalThis.__sbFetchUpstream("fetch", {
4000
- url,
4001
- method: opts.method || "GET",
4002
- headers,
4003
- ...__sbRequestBody(opts.body),
4520
+ if (source) {
4521
+ if (__sbIsFn(source.forEach)) source.forEach((v, k) => headers.push([k, v]));
4522
+ else for (const k in source) headers.push([k, source[k]]);
4523
+ }
4524
+ const body = opts.body !== undefined ? opts.body : req && req.body !== "" ? req.body : null;
4525
+ const method = opts.method || (req && req.method) || "GET";
4526
+ const msg = { url, method, headers, ...__sbRequestBody(body) };
4527
+ const respond = (r) => {
4528
+ const respHeaders = new Headers();
4529
+ for (let j = 0; j < (r.headers || []).length; j++) respHeaders.set(r.headers[j][0], r.headers[j][1]);
4530
+ // #176: bytes from an outbound HTTP response, not a string this handler
4531
+ // built -- must not be re-encoded if the handler proxies it straight
4532
+ // through (see the assets binding for the same reasoning).
4533
+ if (r.body != null) respHeaders.set("x-sb-raw-body", "1");
4534
+ return __sbRawBodyResponse(r.body, { status: r.status || 502, headers: respHeaders });
4535
+ };
4536
+ if (!__sbHttpAsyncEnabled()) return respond(globalThis.__sbFetchUpstream("fetch", msg));
4537
+ // B18: the host runs the request off the event loop. Its reply fires a timer
4538
+ // that never fires on its own; the timer table keeps the callback alive.
4539
+ return new Promise((resolve, reject) => {
4540
+ const id = setTimeout(() => {
4541
+ try {
4542
+ resolve(respond(__sbFetchReply(__sbHttpAsyncResult(id))));
4543
+ } catch (e) {
4544
+ reject(new Error("sproutboat fetch: " + ((e && e.message) || e)));
4545
+ }
4546
+ }, 2147483647);
4547
+ try {
4548
+ msg.asyncId = id;
4549
+ globalThis.__sbFetchUpstream("fetch", msg);
4550
+ } catch (e) {
4551
+ clearTimeout(id);
4552
+ reject(e);
4553
+ }
4004
4554
  });
4005
- const respHeaders = new Headers();
4006
- for (let j = 0; j < (r.headers || []).length; j++) respHeaders.set(r.headers[j][0], r.headers[j][1]);
4007
- // #176: bytes from an outbound HTTP response, not a string this handler
4008
- // built -- must not be re-encoded if the handler proxies it straight
4009
- // through (see the assets binding for the same reasoning).
4010
- if (r.body != null) respHeaders.set("x-sb-raw-body", "1");
4011
- return __sbRawBodyResponse(r.body, { status: r.status || 502, headers: respHeaders });
4012
4555
  };
4013
4556
  };
4014
4557
 
@@ -4067,7 +4610,7 @@ function __sbMakeDONamespace(binding, className) {
4067
4610
  }
4068
4611
  const inst = __sbGetDOInstance(className, idStr);
4069
4612
  const res = inst.fetch(req);
4070
- if (inst.__sbTasks.length || (res && __sbIsFn(res.then))) return __sbFetchWithDrain(res, inst.__sbTasks);
4613
+ if (inst.__sbTasks.length || (res && __sbIsFn(res.then)) || __sbHasStreamBody(res)) return __sbFetchWithDrain(res, inst.__sbTasks);
4071
4614
  return res;
4072
4615
  },
4073
4616
  };
@@ -4350,14 +4893,17 @@ function __sbTrustedProxyHeader(request, name) {
4350
4893
  // own.
4351
4894
  const __SB_WAITUNTIL_TIMEOUT_MS = 25000;
4352
4895
 
4353
- function __sbTimeout(ms) {
4354
- return new Promise((resolve) => setTimeout(resolve, ms));
4355
- }
4356
-
4357
4896
  async function __sbDrainWaitUntil(tasks) {
4358
4897
  if (!tasks.length) return;
4359
4898
  const pending = tasks.splice(0, tasks.length);
4360
- await Promise.race([__sbDrainAll(pending), __sbTimeout(__SB_WAITUNTIL_TIMEOUT_MS)]);
4899
+ // Clear the cap's timer once the tasks finish: a pending timer kept the process
4900
+ // busy for the full 25 s (sproutd will not reap a worker with timers left, B22).
4901
+ let timer;
4902
+ const cap = new Promise((resolve) => {
4903
+ timer = setTimeout(resolve, __SB_WAITUNTIL_TIMEOUT_MS);
4904
+ });
4905
+ await Promise.race([__sbDrainAll(pending), cap]);
4906
+ clearTimeout(timer);
4361
4907
  }
4362
4908
 
4363
4909
  async function __sbDrainAll(pending) {
@@ -4373,15 +4919,130 @@ async function __sbDrainAll(pending) {
4373
4919
  // Tail-called from `__sbEntry`/DO dispatch so the promise this returns is the
4374
4920
  // one *this* async function creates, not a `.then()` derived from someone
4375
4921
  // else's — see the note below on why that distinction matters here.
4922
+ // B19 (first step): a ReadableStream response body is read to the end and sent buffered;
4923
+ // the native writer had sent "[object ReadableStream]". Chunks must be bytes, as in workerd
4924
+ // (a string chunk is a TypeError), and the whole body is capped like a fetched one.
4925
+ const __SB_STREAM_BODY_MAX = 32 * 1024 * 1024;
4926
+ async function __sbCollectBody(stream) {
4927
+ const reader = stream.getReader();
4928
+ const chunks = [];
4929
+ let size = 0;
4930
+ for (;;) {
4931
+ const { value, done } = await reader.read();
4932
+ if (done) break;
4933
+ let bytes;
4934
+ if (value instanceof Uint8Array) bytes = value;
4935
+ else if (value instanceof ArrayBuffer) bytes = new Uint8Array(value);
4936
+ else if (ArrayBuffer.isView(value)) bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
4937
+ else throw new TypeError("This ReadableStream did not return bytes.");
4938
+ size += bytes.length;
4939
+ if (size > __SB_STREAM_BODY_MAX) throw new RangeError("Response body stream exceeds 32 MiB");
4940
+ chunks.push(bytes);
4941
+ }
4942
+ const out = new Uint8Array(size);
4943
+ let at = 0;
4944
+ for (const chunk of chunks) {
4945
+ out.set(chunk, at);
4946
+ at += chunk.length;
4947
+ }
4948
+ return out;
4949
+ }
4950
+ function __sbHasStreamBody(res) {
4951
+ return res != null && res.body instanceof ReadableStream;
4952
+ }
4953
+
4954
+ // B19: on a host that streams (a standalone sprout's native server defines sb_stream_*), a
4955
+ // stream body goes out as it is produced: the response is marked with the internal
4956
+ // x-sb-stream header, the native writer sends the status and headers and holds the
4957
+ // connection open, and each chunk is written as it arrives. Hosts that do not stream
4958
+ // (sproutd's workers, for now) link the weak fallbacks, report unsupported and keep buffering.
4959
+ // op: "s" supported, "o" open, "w" write data, "e" end. Returns a short string.
4960
+ // oxlint-disable-next-line no-unused-vars -- read inside the RawC block, not by JS.
4961
+ function __sbStreamOp(op, id, data) {
4962
+ let out = "";
4963
+ // oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
4964
+ Porffor.c`
4965
+ extern int sb_stream_supported(void);
4966
+ extern unsigned int sb_stream_open(void);
4967
+ extern int sb_stream_write(unsigned int, const char*, size_t);
4968
+ extern void sb_stream_end(unsigned int);
4969
+ const char* __o; size_t __ol; char* __oo = 0;
4970
+ porf_native_fetch_read_value(op, &__o, &__ol, &__oo);
4971
+ char __op = __ol ? __o[0] : 0;
4972
+ if (__oo) free(__oo);
4973
+ const char* __i; size_t __il; char* __io = 0;
4974
+ porf_native_fetch_read_value(id, &__i, &__il, &__io);
4975
+ char __ib[16]; size_t __ik = __il < 15 ? __il : 15;
4976
+ memcpy(__ib, __i, __ik); __ib[__ik] = 0;
4977
+ if (__io) free(__io);
4978
+ unsigned int __id = (unsigned int)strtoul(__ib, 0, 10);
4979
+ char __r[24]; __r[0] = 0;
4980
+ if (sb_stream_supported()) {
4981
+ if (__op == 's') snprintf(__r, sizeof(__r), "1");
4982
+ else if (__op == 'o') snprintf(__r, sizeof(__r), "%u", sb_stream_open());
4983
+ else if (__op == 'w') {
4984
+ const char* __d = 0; size_t __dl = 0;
4985
+ int __w = -1;
4986
+ if (porf_native_fetch_read_raw_bytes(data, &__d, &__dl) == 0) __w = sb_stream_write(__id, __d, __dl);
4987
+ snprintf(__r, sizeof(__r), "%d", __w);
4988
+ } else if (__op == 'e') { sb_stream_end(__id); snprintf(__r, sizeof(__r), "0"); }
4989
+ }
4990
+ out = porf_box((f64)porf_native_fetch_alloc_bytestring(__r, strlen(__r)), 195);
4991
+ `;
4992
+ return out;
4993
+ }
4994
+ let __sbStreaming = -1;
4995
+ function __sbStreamingSupported() {
4996
+ if (__sbStreaming < 0) __sbStreaming = __sbStreamOp("s", "0", "") === "1" ? 1 : 0;
4997
+ return __sbStreaming === 1;
4998
+ }
4999
+ function __sbStreamResponse(response) {
5000
+ const stream = response.body;
5001
+ const id = __sbStreamOp("o", "0", "");
5002
+ response.headers.set("x-sb-stream", id);
5003
+ response.body = "";
5004
+ (async () => {
5005
+ const reader = stream.getReader();
5006
+ try {
5007
+ for (;;) {
5008
+ const { value, done } = await reader.read();
5009
+ if (done) break;
5010
+ let bytes;
5011
+ if (value instanceof Uint8Array) bytes = value;
5012
+ else if (value instanceof ArrayBuffer) bytes = new Uint8Array(value);
5013
+ else if (ArrayBuffer.isView(value)) bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
5014
+ else throw new TypeError("This ReadableStream did not return bytes.");
5015
+ if (bytes.length > 0 && __sbStreamOp("w", id, __sbToBytes(bytes)) !== "0") {
5016
+ // The client went away: stop the source.
5017
+ reader.cancel();
5018
+ break;
5019
+ }
5020
+ }
5021
+ } catch (error) {
5022
+ // The status line is already out; log and end what was sent.
5023
+ __sbLogUncaught("fetch", error);
5024
+ }
5025
+ __sbStreamOp("e", id, "");
5026
+ })();
5027
+ return response;
5028
+ }
5029
+
4376
5030
  async function __sbFetchWithDrain(res, tasks) {
4377
5031
  let r;
4378
5032
  try {
4379
5033
  r = res && __sbIsFn(res.then) ? await res : res;
5034
+ if (__sbHasStreamBody(r)) {
5035
+ if (__sbStreamingSupported()) __sbStreamResponse(r);
5036
+ else r.body = await __sbCollectBody(r.body);
5037
+ }
4380
5038
  } catch (error) {
4381
5039
  // #179 — a rejected handler promise must not take the process down with it.
4382
5040
  r = __sbErrorResponse(error);
4383
5041
  }
4384
- await __sbDrainWaitUntil(tasks);
5042
+ // The response goes out now and ctx.waitUntil work keeps running after it, as in
5043
+ // workerd; it was awaited first, so a 1.5 s background task delayed the response by
5044
+ // 1.5 s. The 25 s cap still bounds it.
5045
+ __sbDrainWaitUntil(tasks);
4385
5046
  return r;
4386
5047
  }
4387
5048
 
@@ -4391,7 +5052,10 @@ async function __sbFetchWithDrain(res, tasks) {
4391
5052
  // nothing on stderr, which hid #168 entirely.
4392
5053
  function __sbErrorResponse(error) {
4393
5054
  __sbLogUncaught("fetch", error);
4394
- return new Response("Internal Server Error", { status: 500 });
5055
+ // Untyped, as workerd sends it: the text/plain default is for handlers' bodies.
5056
+ const response = new Response("Internal Server Error", { status: 500 });
5057
+ response.headers.delete("content-type");
5058
+ return response;
4395
5059
  }
4396
5060
 
4397
5061
  function __sbLogUncaught(where, error) {
@@ -4416,7 +5080,7 @@ async function __sb204WithDrain(res, tasks) {
4416
5080
  __sbLogUncaught("scheduled/alarm", error);
4417
5081
  }
4418
5082
  await __sbDrainWaitUntil(tasks);
4419
- return new Response("", { status: 204 });
5083
+ return new Response(null, { status: 204 });
4420
5084
  }
4421
5085
 
4422
5086
  async function __sbQueueWithDrain(result, tasks) {
@@ -4489,7 +5153,7 @@ globalThis.__sbEntry = function (handlers, request) {
4489
5153
  }
4490
5154
  // Same promise-identity rule as above applies to `__sbFetchWithDrain`'s
4491
5155
  // own return, which is why it's tail-called rather than chained on here.
4492
- if (__sbTasks.length || (__res && __sbIsFn(__res.then))) return __sbFetchWithDrain(__res, __sbTasks);
5156
+ if (__sbTasks.length || (__res && __sbIsFn(__res.then)) || __sbHasStreamBody(__res)) return __sbFetchWithDrain(__res, __sbTasks);
4493
5157
  return __sbTagCpu(__res, __t0);
4494
5158
  }
4495
5159
  if (!__sbTriggerAuthed(request)) return new Response("forbidden", { status: 403 });
@@ -4515,10 +5179,10 @@ globalThis.__sbEntry = function (handlers, request) {
4515
5179
  } catch (error) {
4516
5180
  // #179 — same rule as fetch: don't let a throw here crash the process.
4517
5181
  __sbLogUncaught("scheduled", error);
4518
- return new Response("", { status: 204 });
5182
+ return new Response(null, { status: 204 });
4519
5183
  }
4520
5184
  if (__sbTasks.length || (__sres && __sbIsFn(__sres.then))) return __sb204WithDrain(__sres, __sbTasks);
4521
- return new Response("", { status: 204 });
5185
+ return new Response(null, { status: 204 });
4522
5186
  }
4523
5187
 
4524
5188
  if (trigger === "queue") {
@@ -4550,12 +5214,12 @@ globalThis.__sbEntry = function (handlers, request) {
4550
5214
  } catch (error) {
4551
5215
  // #179 — same rule as fetch: don't let a throw here crash the process.
4552
5216
  __sbLogUncaught("alarm", error);
4553
- return new Response("", { status: 204 });
5217
+ return new Response(null, { status: 204 });
4554
5218
  }
4555
5219
  // Was fire-and-forget before: an async `alarm()` and any `state.waitUntil()`
4556
5220
  // it queued were both dropped the moment 204 went out. Await both.
4557
5221
  if (inst.__sbTasks.length || (__ares && __sbIsFn(__ares.then))) return __sb204WithDrain(__ares, inst.__sbTasks);
4558
- return new Response("", { status: 204 });
5222
+ return new Response(null, { status: 204 });
4559
5223
  }
4560
5224
 
4561
5225
  return new Response("unknown trigger", { status: 400 });