@ccheever/exact-ibex-runtime 0.1.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.
Files changed (161) hide show
  1. package/package.json +63 -0
  2. package/src/abort/AbortController.ts +23 -0
  3. package/src/abort/AbortSignal.ts +152 -0
  4. package/src/abort/index.ts +2 -0
  5. package/src/accessibility.ts +12 -0
  6. package/src/arraybuffer-detach.ts +109 -0
  7. package/src/base64/base64.ts +168 -0
  8. package/src/base64/index.ts +1 -0
  9. package/src/blob/Blob.ts +259 -0
  10. package/src/blob/File.ts +59 -0
  11. package/src/blob/FormData.ts +323 -0
  12. package/src/blob/index.ts +3 -0
  13. package/src/bootstrap.ts +1946 -0
  14. package/src/broadcast/BroadcastChannel.ts +280 -0
  15. package/src/broadcast/index.ts +5 -0
  16. package/src/cache/Cache.ts +349 -0
  17. package/src/cache/CacheStorage.ts +89 -0
  18. package/src/cache/index.ts +27 -0
  19. package/src/camera/index.ts +6202 -0
  20. package/src/camera/processor.worker.ts +194 -0
  21. package/src/camera/scene.ts +195 -0
  22. package/src/clipboard/Clipboard.ts +129 -0
  23. package/src/clipboard/ClipboardItem.ts +97 -0
  24. package/src/clipboard/index.ts +6 -0
  25. package/src/clone/index.ts +1 -0
  26. package/src/clone/structuredClone.ts +389 -0
  27. package/src/clone/transferableSymbols.ts +2 -0
  28. package/src/compression/CompressionStream.ts +146 -0
  29. package/src/compression/DecompressionStream.ts +342 -0
  30. package/src/compression/index.ts +4 -0
  31. package/src/console/Console.ts +341 -0
  32. package/src/console/index.ts +2 -0
  33. package/src/core/accessibility-state.ts +263 -0
  34. package/src/core/accessibility.ts +184 -0
  35. package/src/core/agent-state.ts +37 -0
  36. package/src/core/diagnostics-logs.ts +144 -0
  37. package/src/core/host-call-bridge.ts +16 -0
  38. package/src/core/i18n-helpers.ts +189 -0
  39. package/src/core/locale-state.ts +253 -0
  40. package/src/core/locale.ts +95 -0
  41. package/src/crypto/Crypto.ts +2743 -0
  42. package/src/crypto/index.ts +1 -0
  43. package/src/diagnostics/logs.ts +7 -0
  44. package/src/encoding/TextDecoder.ts +1181 -0
  45. package/src/encoding/TextDecoderStream.ts +58 -0
  46. package/src/encoding/TextEncoder.ts +180 -0
  47. package/src/encoding/TextEncoderStream.ts +39 -0
  48. package/src/encoding/index.ts +8 -0
  49. package/src/events/CloseEvent.ts +91 -0
  50. package/src/events/DOMException.ts +409 -0
  51. package/src/events/ErrorEvent.ts +39 -0
  52. package/src/events/Event.ts +151 -0
  53. package/src/events/EventTarget.ts +280 -0
  54. package/src/events/FocusEvent.ts +27 -0
  55. package/src/events/KeyboardEvent.ts +46 -0
  56. package/src/events/MessageEvent.ts +61 -0
  57. package/src/events/ProgressEvent.ts +33 -0
  58. package/src/events/PromiseRejectionEvent.ts +31 -0
  59. package/src/events/index.ts +52 -0
  60. package/src/eventsource/EventSource.ts +371 -0
  61. package/src/eventsource/index.ts +2 -0
  62. package/src/fetch/Headers.ts +642 -0
  63. package/src/fetch/Request.ts +760 -0
  64. package/src/fetch/Response.ts +543 -0
  65. package/src/fetch/body.ts +1256 -0
  66. package/src/fetch/cookie-jar.ts +566 -0
  67. package/src/fetch/demo.ts +207 -0
  68. package/src/fetch/errors.ts +101 -0
  69. package/src/fetch/fetch.ts +2610 -0
  70. package/src/fetch/index.ts +101 -0
  71. package/src/fetch/native-bridge.ts +65 -0
  72. package/src/fetch/types.ts +258 -0
  73. package/src/filereader/FileReader.ts +236 -0
  74. package/src/filereader/index.ts +1 -0
  75. package/src/fs/Dirent.ts +39 -0
  76. package/src/fs/ExactFile.ts +450 -0
  77. package/src/fs/Stats.ts +80 -0
  78. package/src/fs/index.ts +944 -0
  79. package/src/fs/promises.ts +386 -0
  80. package/src/fs/shared.ts +328 -0
  81. package/src/http-server/index.js +697 -0
  82. package/src/http-server/index.ts +27 -0
  83. package/src/identity.generated.ts +14 -0
  84. package/src/index.ts +283 -0
  85. package/src/indexeddb/IDBCursor.ts +188 -0
  86. package/src/indexeddb/IDBDatabase.ts +343 -0
  87. package/src/indexeddb/IDBFactory.ts +269 -0
  88. package/src/indexeddb/IDBIndex.ts +194 -0
  89. package/src/indexeddb/IDBKeyRange.ts +109 -0
  90. package/src/indexeddb/IDBObjectStore.ts +468 -0
  91. package/src/indexeddb/IDBRequest.ts +163 -0
  92. package/src/indexeddb/IDBTransaction.ts +207 -0
  93. package/src/indexeddb/index.ts +34 -0
  94. package/src/indexeddb/utils.ts +52 -0
  95. package/src/inspect/index.ts +1 -0
  96. package/src/inspect/inspect.ts +465 -0
  97. package/src/internal/detect.ts +104 -0
  98. package/src/locale.ts +10 -0
  99. package/src/location/index.ts +1059 -0
  100. package/src/locks/LockManager.ts +460 -0
  101. package/src/locks/index.ts +12 -0
  102. package/src/media/VideoFrame.ts +58 -0
  103. package/src/messaging/MessageChannel.ts +31 -0
  104. package/src/messaging/MessagePort.ts +180 -0
  105. package/src/messaging/index.ts +2 -0
  106. package/src/messaging.ts +247 -0
  107. package/src/native/NativeModules.ts +354 -0
  108. package/src/native/index.ts +1 -0
  109. package/src/navigator/Navigator.ts +351 -0
  110. package/src/navigator/index.ts +1 -0
  111. package/src/node/Buffer.ts +1786 -0
  112. package/src/node/index.ts +4 -0
  113. package/src/node/path.ts +495 -0
  114. package/src/node/process.ts +2528 -0
  115. package/src/performance/Performance.ts +532 -0
  116. package/src/performance/index.ts +21 -0
  117. package/src/polyfills/array.ts +236 -0
  118. package/src/polyfills/arraybuffer.ts +172 -0
  119. package/src/polyfills/groupby.ts +85 -0
  120. package/src/polyfills/index.ts +85 -0
  121. package/src/polyfills/intl.ts +1956 -0
  122. package/src/polyfills/iterator.ts +479 -0
  123. package/src/polyfills/promise.ts +37 -0
  124. package/src/polyfills/set.ts +245 -0
  125. package/src/polyfills/string.ts +85 -0
  126. package/src/polyfills/typedarray.ts +110 -0
  127. package/src/promise-rejection-tracking.ts +464 -0
  128. package/src/react-native/index.ts +388 -0
  129. package/src/runtime-entry.ts +55 -0
  130. package/src/scheduling/AnimationFrame.ts +105 -0
  131. package/src/scheduling/IdleCallback.ts +167 -0
  132. package/src/scheduling/index.ts +13 -0
  133. package/src/security/Capabilities.ts +1146 -0
  134. package/src/security/Permissions.ts +392 -0
  135. package/src/security/capability-bits.generated.ts +63 -0
  136. package/src/security/index.ts +16 -0
  137. package/src/sqlite/Database.ts +456 -0
  138. package/src/sqlite/Statement.ts +206 -0
  139. package/src/sqlite/constants.ts +79 -0
  140. package/src/sqlite/errors.ts +25 -0
  141. package/src/sqlite/index.ts +34 -0
  142. package/src/sqlite/module.js +438 -0
  143. package/src/storage/Storage.ts +291 -0
  144. package/src/storage/StorageManager.ts +91 -0
  145. package/src/storage/index.ts +3 -0
  146. package/src/stream-compat.ts +47 -0
  147. package/src/streams/ReadableStream.ts +4131 -0
  148. package/src/streams/TransformStream.ts +375 -0
  149. package/src/streams/WritableStream.ts +866 -0
  150. package/src/streams/index.ts +41 -0
  151. package/src/timers/Timers.ts +296 -0
  152. package/src/timers/index.ts +11 -0
  153. package/src/url/URL.ts +656 -0
  154. package/src/url/URLPattern.ts +850 -0
  155. package/src/url/URLSearchParams.ts +244 -0
  156. package/src/url/index.ts +9 -0
  157. package/src/websocket/WebSocket.ts +770 -0
  158. package/src/websocket/WebSocketError.ts +52 -0
  159. package/src/websocket/WebSocketStream.ts +628 -0
  160. package/src/websocket/index.ts +7 -0
  161. package/src/window/index.ts +872 -0
@@ -0,0 +1,1256 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Body Handling Utilities
4
+ *
5
+ * Implements body mixin functionality for Request and Response.
6
+ * @see https://fetch.spec.whatwg.org/#body-mixin
7
+ */
8
+
9
+ import type { BodyInit, BufferSource } from './types.js';
10
+ import { BodyConsumedError } from './errors.js';
11
+ import { isReadableStream, ReadableStream as RuntimeReadableStream } from '../streams/index.js';
12
+
13
+ /**
14
+ * Text encoder/decoder - lazy initialization with fallbacks for Hermes.
15
+ */
16
+ let _textEncoder: { encode(s: string): Uint8Array } | null = null;
17
+ let _textDecoder: { decode(b: BufferSource): string } | null = null;
18
+ let _textDecoderNoBOM: { decode(b: BufferSource): string } | null = null;
19
+
20
+ function isBunCompatFetchTest(): boolean {
21
+ if ((globalThis as { __exactRuntimeContext?: string }).__exactRuntimeContext === 'shell') {
22
+ return false;
23
+ }
24
+ if (typeof process !== 'object' || !process || typeof process.env !== 'object') {
25
+ return false;
26
+ }
27
+
28
+ return process.env.EXACT_COMPAT_TEST === '1' && process.env.EXACT_TEST_SECTION === 'bun';
29
+ }
30
+
31
+ export function normalizeBlobContentType(contentType: string | null): string {
32
+ if (!contentType) {
33
+ return '';
34
+ }
35
+
36
+ const normalized = contentType
37
+ .split(';')
38
+ .map((part) => part.trim())
39
+ .filter(Boolean)
40
+ .join(';')
41
+ .toLowerCase();
42
+
43
+ if (!isBunCompatFetchTest() || normalized.includes('charset=')) {
44
+ return normalized;
45
+ }
46
+
47
+ const mediaType = normalized.split(';', 1)[0];
48
+ const isUtf8TextLike =
49
+ mediaType.startsWith('text/') ||
50
+ mediaType === 'application/json' ||
51
+ mediaType === 'application/javascript' ||
52
+ mediaType === 'application/x-www-form-urlencoded' ||
53
+ mediaType === 'application/xml' ||
54
+ mediaType.endsWith('+json') ||
55
+ mediaType.endsWith('+xml');
56
+
57
+ return isUtf8TextLike ? `${normalized};charset=utf-8` : normalized;
58
+ }
59
+
60
+ export function getTextEncoder(): { encode(s: string): Uint8Array } {
61
+ if (_textEncoder) return _textEncoder;
62
+
63
+ if (typeof TextEncoder !== 'undefined') {
64
+ _textEncoder = new TextEncoder();
65
+ } else {
66
+ // UTF-8 fallback for Hermes (handles full Unicode including surrogate pairs)
67
+ _textEncoder = {
68
+ encode(str: string): Uint8Array {
69
+ const bytes = new Uint8Array(str.length * 4); // Max 4 bytes per code unit
70
+ let offset = 0;
71
+ for (let i = 0; i < str.length; i++) {
72
+ const code = str.charCodeAt(i);
73
+ if (code < 0x80) {
74
+ bytes[offset++] = code;
75
+ } else if (code < 0x800) {
76
+ bytes[offset++] = 0xc0 | (code >> 6);
77
+ bytes[offset++] = 0x80 | (code & 0x3f);
78
+ } else if (code >= 0xd800 && code <= 0xdbff) {
79
+ // High surrogate: combine with next low surrogate to get full codepoint
80
+ const low = str.charCodeAt(i + 1);
81
+ if (low >= 0xdc00 && low <= 0xdfff) {
82
+ const cp = (code - 0xd800) * 0x400 + (low - 0xdc00) + 0x10000;
83
+ bytes[offset++] = 0xf0 | (cp >> 18);
84
+ bytes[offset++] = 0x80 | ((cp >> 12) & 0x3f);
85
+ bytes[offset++] = 0x80 | ((cp >> 6) & 0x3f);
86
+ bytes[offset++] = 0x80 | (cp & 0x3f);
87
+ i++; // Skip the low surrogate
88
+ } else {
89
+ // Lone high surrogate: encode as replacement character U+FFFD
90
+ bytes[offset++] = 0xef;
91
+ bytes[offset++] = 0xbf;
92
+ bytes[offset++] = 0xbd;
93
+ }
94
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
95
+ // Lone low surrogate: encode as replacement character U+FFFD
96
+ bytes[offset++] = 0xef;
97
+ bytes[offset++] = 0xbf;
98
+ bytes[offset++] = 0xbd;
99
+ } else {
100
+ bytes[offset++] = 0xe0 | (code >> 12);
101
+ bytes[offset++] = 0x80 | ((code >> 6) & 0x3f);
102
+ bytes[offset++] = 0x80 | (code & 0x3f);
103
+ }
104
+ }
105
+ return bytes.slice(0, offset);
106
+ },
107
+ };
108
+ }
109
+ return _textEncoder;
110
+ }
111
+
112
+ function getTextDecoder(): { decode(b: BufferSource): string } {
113
+ if (_textDecoder) return _textDecoder;
114
+
115
+ if (typeof TextDecoder !== 'undefined') {
116
+ _textDecoder = new TextDecoder();
117
+ } else {
118
+ // Simple UTF-8 fallback for Hermes
119
+ _textDecoder = {
120
+ decode(buffer: BufferSource): string {
121
+ const bytes = buffer instanceof ArrayBuffer
122
+ ? new Uint8Array(buffer)
123
+ : arrayBufferViewToUint8Array(buffer);
124
+
125
+ let result = '';
126
+ let i = 0;
127
+ while (i < bytes.length) {
128
+ const byte = bytes[i];
129
+ if (byte < 0x80) {
130
+ result += String.fromCharCode(byte);
131
+ i++;
132
+ } else if ((byte & 0xe0) === 0xc0) {
133
+ result += String.fromCharCode(((byte & 0x1f) << 6) | (bytes[i + 1] & 0x3f));
134
+ i += 2;
135
+ } else if ((byte & 0xf0) === 0xe0) {
136
+ result += String.fromCharCode(
137
+ ((byte & 0x0f) << 12) | ((bytes[i + 1] & 0x3f) << 6) | (bytes[i + 2] & 0x3f)
138
+ );
139
+ i += 3;
140
+ } else if ((byte & 0xf8) === 0xf0) {
141
+ // 4-byte sequence: decode full codepoint and convert to surrogate pair
142
+ const cp =
143
+ ((byte & 0x07) << 18) |
144
+ ((bytes[i + 1] & 0x3f) << 12) |
145
+ ((bytes[i + 2] & 0x3f) << 6) |
146
+ (bytes[i + 3] & 0x3f);
147
+ result += String.fromCodePoint(cp);
148
+ i += 4;
149
+ } else {
150
+ // Invalid leading byte: skip it
151
+ i++;
152
+ }
153
+ }
154
+ return result;
155
+ },
156
+ };
157
+ }
158
+ return _textDecoder;
159
+ }
160
+
161
+ function getTextDecoderNoBOM(): { decode(b: BufferSource): string } {
162
+ if (_textDecoderNoBOM) return _textDecoderNoBOM;
163
+
164
+ if (typeof TextDecoder !== 'undefined') {
165
+ _textDecoderNoBOM = new TextDecoder('utf-8', { fatal: false, ignoreBOM: true });
166
+ } else {
167
+ _textDecoderNoBOM = getTextDecoder();
168
+ }
169
+ return _textDecoderNoBOM;
170
+ }
171
+
172
+ function decodeTextForFormData(buffer: ArrayBuffer): string {
173
+ const text = getTextDecoderNoBOM().decode(new Uint8Array(buffer));
174
+ return text.startsWith("\uFEFF") ? text.slice(1) : text;
175
+ }
176
+
177
+ /**
178
+ * Check if a value is a Blob-like object.
179
+ */
180
+ export function isBlob(value: unknown): value is Blob {
181
+ return (
182
+ typeof value === 'object' &&
183
+ value !== null &&
184
+ typeof (value as Blob).arrayBuffer === 'function' &&
185
+ typeof (value as Blob).slice === 'function' &&
186
+ typeof (value as Blob).size === 'number' &&
187
+ typeof (value as Blob).type === 'string'
188
+ );
189
+ }
190
+
191
+ /**
192
+ * FormData with entries method (for iteration).
193
+ */
194
+ interface FormDataWithEntries extends FormData {
195
+ entries(): IterableIterator<[string, FormDataEntryValue]>;
196
+ }
197
+
198
+ /**
199
+ * Check if a value is FormData-like.
200
+ */
201
+ export function isFormData(value: unknown): value is FormDataWithEntries {
202
+ return (
203
+ typeof value === 'object' &&
204
+ value !== null &&
205
+ typeof (value as FormData).append === 'function' &&
206
+ typeof (value as FormData & { entries?: unknown }).entries === 'function'
207
+ );
208
+ }
209
+
210
+ /**
211
+ * Check if a value is an ArrayBuffer.
212
+ */
213
+ export function isArrayBuffer(value: unknown): value is ArrayBuffer {
214
+ return value instanceof ArrayBuffer;
215
+ }
216
+
217
+ /**
218
+ * Check if a value is an ArrayBufferView (TypedArray or DataView).
219
+ */
220
+ export function isArrayBufferView(value: unknown): value is ArrayBufferView {
221
+ return ArrayBuffer.isView(value);
222
+ }
223
+
224
+ export function getArrayBufferViewByteLength(view: ArrayBufferView): number {
225
+ const reportedByteLength = view.byteLength;
226
+ const typedArrayLength = (view as ArrayBufferView & { length?: unknown }).length;
227
+ const bytesPerElement =
228
+ view &&
229
+ view.constructor &&
230
+ typeof (view.constructor as { BYTES_PER_ELEMENT?: unknown }).BYTES_PER_ELEMENT === 'number'
231
+ ? (view.constructor as { BYTES_PER_ELEMENT: number }).BYTES_PER_ELEMENT
232
+ : null;
233
+
234
+ if (
235
+ typeof typedArrayLength === 'number' &&
236
+ typeof bytesPerElement === 'number'
237
+ ) {
238
+ const computedByteLength = typedArrayLength * bytesPerElement;
239
+ if (computedByteLength >= 0 && computedByteLength <= reportedByteLength) {
240
+ return computedByteLength;
241
+ }
242
+ }
243
+
244
+ return reportedByteLength;
245
+ }
246
+
247
+ export function arrayBufferViewToUint8Array(view: ArrayBufferView): Uint8Array {
248
+ return new Uint8Array(view.buffer, view.byteOffset, getArrayBufferViewByteLength(view));
249
+ }
250
+
251
+ export function normalizeBodyChunk(
252
+ chunk: unknown,
253
+ source = 'ReadableStream body'
254
+ ): Uint8Array {
255
+ if (chunk instanceof Uint8Array) {
256
+ return chunk;
257
+ }
258
+
259
+ if (typeof chunk === 'string') {
260
+ return getTextEncoder().encode(chunk);
261
+ }
262
+
263
+ if (isArrayBuffer(chunk)) {
264
+ return new Uint8Array(chunk.slice(0));
265
+ }
266
+
267
+ if (isArrayBufferView(chunk)) {
268
+ return arrayBufferViewToUint8Array(chunk);
269
+ }
270
+
271
+ throw new TypeError(`${source} chunk must be a string, Buffer, or ArrayBufferView.`);
272
+ }
273
+
274
+ export function normalizeReadableStreamBody(
275
+ stream: ReadableStream<unknown>,
276
+ source = 'ReadableStream body'
277
+ ): ReadableStream<Uint8Array> {
278
+ const reader = stream.getReader();
279
+ reader.closed.catch(function () {});
280
+ let released = false;
281
+
282
+ function releaseReader(): void {
283
+ if (released) {
284
+ return;
285
+ }
286
+ released = true;
287
+ try {
288
+ reader.releaseLock();
289
+ reader.closed.catch(function () {});
290
+ } catch {}
291
+ }
292
+
293
+ return new RuntimeReadableStream<Uint8Array>({
294
+ pull(controller) {
295
+ const readPromise = reader.read();
296
+ readPromise.catch(function () {});
297
+ return readPromise.then(
298
+ function (result) {
299
+ if (result.done) {
300
+ releaseReader();
301
+ controller.close();
302
+ return;
303
+ }
304
+ try {
305
+ controller.enqueue(normalizeBodyChunk(result.value, source));
306
+ } catch (error) {
307
+ try { reader.cancel(error).catch(function () {}); } catch {}
308
+ releaseReader();
309
+ throw error;
310
+ }
311
+ },
312
+ function (error) {
313
+ releaseReader();
314
+ throw error;
315
+ }
316
+ );
317
+ },
318
+ cancel(reason) {
319
+ const cancelResult = reader.cancel(reason);
320
+ if (cancelResult && typeof (cancelResult as Promise<void>).then === 'function') {
321
+ return (cancelResult as Promise<void>).then(
322
+ function () {
323
+ releaseReader();
324
+ },
325
+ function () {
326
+ releaseReader();
327
+ }
328
+ );
329
+ }
330
+ releaseReader();
331
+ return undefined;
332
+ },
333
+ });
334
+ }
335
+
336
+ export function isAsyncIterableBody(value: unknown): value is AsyncIterable<unknown> {
337
+ return (
338
+ value !== null &&
339
+ typeof value === 'object' &&
340
+ typeof (value as AsyncIterable<unknown>)[Symbol.asyncIterator] === 'function'
341
+ );
342
+ }
343
+
344
+ export function createReadableStreamFromAsyncIterableBody(
345
+ body: AsyncIterable<unknown>,
346
+ source = 'Async iterable body'
347
+ ): ReadableStream<Uint8Array> {
348
+ const iterator = body[Symbol.asyncIterator]();
349
+ return new RuntimeReadableStream<Uint8Array>({
350
+ async pull(controller) {
351
+ const result = await iterator.next();
352
+ if (result.done) {
353
+ controller.close();
354
+ return;
355
+ }
356
+ controller.enqueue(normalizeBodyChunk(result.value, source));
357
+ },
358
+ async cancel(reason) {
359
+ if (typeof iterator.return === 'function') {
360
+ await iterator.return(reason);
361
+ }
362
+ },
363
+ });
364
+ }
365
+
366
+ export async function asyncIterableToUint8Array(
367
+ body: AsyncIterable<unknown>,
368
+ source = 'Async iterable body'
369
+ ): Promise<Uint8Array> {
370
+ const chunks: Uint8Array[] = [];
371
+ const iterator = body[Symbol.asyncIterator]();
372
+ let shouldCloseIterator = false;
373
+
374
+ try {
375
+ while (true) {
376
+ const result = await iterator.next();
377
+ if (result.done) {
378
+ break;
379
+ }
380
+
381
+ // Track whether we have accepted a chunk but not yet finished handling
382
+ // it so abrupt failures can still signal iterator cleanup.
383
+ shouldCloseIterator = true;
384
+ chunks.push(normalizeBodyChunk(result.value, source));
385
+ shouldCloseIterator = false;
386
+ }
387
+ } catch (error) {
388
+ if (shouldCloseIterator && typeof iterator.return === 'function') {
389
+ try {
390
+ await iterator.return();
391
+ } catch {
392
+ // Preserve the original body conversion failure.
393
+ }
394
+ }
395
+ throw error;
396
+ }
397
+
398
+ return concatUint8Arrays(chunks);
399
+ }
400
+
401
+ /**
402
+ * Convert various body types to Uint8Array.
403
+ */
404
+ export async function bodyToUint8Array(
405
+ body: BodyInit | null | undefined
406
+ ): Promise<Uint8Array | null> {
407
+ if (body === null || body === undefined) {
408
+ return null;
409
+ }
410
+
411
+ if (typeof body === 'string') {
412
+ return getTextEncoder().encode(body);
413
+ }
414
+
415
+ if (isArrayBuffer(body)) {
416
+ return new Uint8Array(body);
417
+ }
418
+
419
+ if (isArrayBufferView(body)) {
420
+ return arrayBufferViewToUint8Array(body);
421
+ }
422
+
423
+ if (isBlob(body)) {
424
+ const buffer = await body.arrayBuffer();
425
+ return new Uint8Array(buffer);
426
+ }
427
+
428
+ if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
429
+ return getTextEncoder().encode(body.toString());
430
+ }
431
+
432
+ if (isFormData(body)) {
433
+ // Check cache first (populated by getContentTypeForBody)
434
+ let cached = formDataEncodeCache.get(body);
435
+ if (cached) {
436
+ return cached.body;
437
+ }
438
+
439
+ // Use FormData's built-in multipart encoding if available
440
+ if (typeof (body as any)._encode === 'function') {
441
+ const result = (body as any)._encode();
442
+ formDataEncodeCache.set(body, result);
443
+ return result.body;
444
+ }
445
+
446
+ // Fallback: Convert FormData to multipart/form-data format
447
+ const encoded = await encodeFormDataInternalAsync(body);
448
+ formDataEncodeCache.set(body, encoded);
449
+ return encoded.body;
450
+ }
451
+
452
+ if (isReadableStream(body)) {
453
+ return await readableStreamToUint8Array(normalizeReadableStreamBody(body));
454
+ }
455
+
456
+ if (isAsyncIterableBody(body)) {
457
+ return await asyncIterableToUint8Array(body);
458
+ }
459
+
460
+ throw new TypeError('Unsupported body type');
461
+ }
462
+
463
+ /**
464
+ * Resolve a promise with a value, preventing Object.prototype.then from
465
+ * making the value appear as a thenable (per Fetch spec, internal algorithms
466
+ * should not be affected by prototype pollution).
467
+ */
468
+ export function resolveWithoutThenable<T>(resolve: (value: T) => void, value: T): void {
469
+ if (value != null && typeof value === 'object' && typeof (value as any).then === 'function') {
470
+ Object.defineProperty(value, 'then', { value: undefined, configurable: true, writable: true });
471
+ resolve(value);
472
+ delete (value as any).then;
473
+ } else {
474
+ resolve(value);
475
+ }
476
+ }
477
+
478
+ /**
479
+ * Read a ReadableStream to Uint8Array.
480
+ * Uses explicit promise chains (not async/await) to prevent compiled async
481
+ * wrappers from creating intermediate promises that trigger Object.prototype.then.
482
+ */
483
+ export function readableStreamToUint8Array(
484
+ stream: ReadableStream<Uint8Array>,
485
+ signal?: AbortSignal | null
486
+ ): Promise<Uint8Array> {
487
+ return new Promise<Uint8Array>(function (outerResolve, outerReject) {
488
+ const reader = stream.getReader();
489
+ reader.closed.catch(function () {});
490
+ const chunks: Uint8Array[] = [];
491
+ let settled = false;
492
+
493
+ function cleanupAbortListener(): void {
494
+ if (signal) {
495
+ signal.removeEventListener('abort', abortReader);
496
+ }
497
+ }
498
+
499
+ function rejectWith(error: unknown): void {
500
+ if (settled) {
501
+ return;
502
+ }
503
+ settled = true;
504
+ cleanupAbortListener();
505
+ try { reader.cancel(error).catch(function () {}); } catch {}
506
+ try {
507
+ reader.releaseLock();
508
+ reader.closed.catch(function () {});
509
+ } catch {}
510
+ outerReject(error);
511
+ }
512
+
513
+ function abortReader(): void {
514
+ rejectWith(
515
+ signal?.reason ??
516
+ new DOMException('The operation was aborted.', 'AbortError')
517
+ );
518
+ }
519
+
520
+ if (signal) {
521
+ if (signal.aborted) {
522
+ abortReader();
523
+ return;
524
+ }
525
+ signal.addEventListener('abort', abortReader);
526
+ }
527
+
528
+ function pump(): void {
529
+ if (settled) {
530
+ return;
531
+ }
532
+ if (signal?.aborted) {
533
+ abortReader();
534
+ return;
535
+ }
536
+
537
+ const readPromise = reader.read();
538
+ readPromise.catch(function () {});
539
+ readPromise.then(
540
+ function (result) {
541
+ if (settled) {
542
+ return;
543
+ }
544
+ if (result.done) {
545
+ settled = true;
546
+ cleanupAbortListener();
547
+ reader.releaseLock();
548
+ const concatenated = concatUint8Arrays(chunks);
549
+ resolveWithoutThenable(outerResolve, concatenated);
550
+ return;
551
+ }
552
+ try {
553
+ chunks.push(normalizeBodyChunk(result.value));
554
+ } catch (error) {
555
+ rejectWith(error);
556
+ return;
557
+ }
558
+ if (signal?.aborted) {
559
+ abortReader();
560
+ return;
561
+ }
562
+ pump();
563
+ },
564
+ function (err) {
565
+ rejectWith(err);
566
+ }
567
+ );
568
+ }
569
+
570
+ pump();
571
+ });
572
+ }
573
+
574
+ /**
575
+ * Concatenate multiple Uint8Arrays into one.
576
+ */
577
+ export function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {
578
+ const totalLength = arrays.reduce((acc, arr) => acc + arr.length, 0);
579
+ const result = new Uint8Array(totalLength);
580
+ let offset = 0;
581
+ for (const arr of arrays) {
582
+ result.set(arr, offset);
583
+ offset += arr.length;
584
+ }
585
+ return result;
586
+ }
587
+
588
+ /**
589
+ * Generate a boundary string for multipart/form-data.
590
+ */
591
+ export function generateBoundary(): string {
592
+ return '----ExactFormBoundary' + Math.random().toString(36).slice(2);
593
+ }
594
+
595
+ /**
596
+ * Cache for FormData encoding results to ensure consistent boundaries.
597
+ * Using WeakMap so FormData objects can be garbage collected.
598
+ */
599
+ const formDataEncodeCache = new WeakMap<FormData, { body: Uint8Array; contentType: string }>();
600
+
601
+ /**
602
+ * Get the Content-Type header for a body type.
603
+ * For FormData, this will cache the encoding result so that bodyToUint8Array
604
+ * returns the same bytes with matching boundary.
605
+ */
606
+ export function getContentTypeForBody(body: BodyInit | null): string | null {
607
+ if (body === null || body === undefined) {
608
+ return null;
609
+ }
610
+
611
+ if (typeof body === 'string') {
612
+ return 'text/plain;charset=UTF-8';
613
+ }
614
+
615
+ if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
616
+ return 'application/x-www-form-urlencoded;charset=UTF-8';
617
+ }
618
+
619
+ if (isBlob(body) && body.type) {
620
+ return body.type;
621
+ }
622
+
623
+ if (isFormData(body)) {
624
+ // Check cache first
625
+ let cached = formDataEncodeCache.get(body);
626
+ if (!cached) {
627
+ // Encode once and cache the result
628
+ cached = encodeFormDataInternal(body);
629
+ formDataEncodeCache.set(body, cached);
630
+ }
631
+ return cached.contentType;
632
+ }
633
+
634
+ // For ArrayBuffer, TypedArray, etc., no default Content-Type
635
+ return null;
636
+ }
637
+
638
+ /**
639
+ * Internal sync encoding for FormData (uses _getBytes for Blobs).
640
+ */
641
+ function encodeFormDataInternal(body: FormData): { body: Uint8Array; contentType: string } {
642
+ // Use FormData's built-in encoding if available
643
+ if (typeof (body as any)._encode === 'function') {
644
+ return (body as any)._encode();
645
+ }
646
+
647
+ const boundary = generateBoundary();
648
+ const parts: Uint8Array[] = [];
649
+
650
+ for (const [name, value] of (body as any).entries()) {
651
+ if (typeof value === 'string') {
652
+ parts.push(getTextEncoder().encode(
653
+ `--${boundary}\r\n` +
654
+ `Content-Disposition: form-data; name="${name}"\r\n\r\n` +
655
+ `${value}\r\n`
656
+ ));
657
+ } else if (isBlob(value)) {
658
+ const fileBytes = (value as any)._getBytes?.() ?? new Uint8Array(0);
659
+ const fileName = (value as File).name || 'blob';
660
+ const contentType = value.type || 'application/octet-stream';
661
+
662
+ parts.push(getTextEncoder().encode(
663
+ `--${boundary}\r\n` +
664
+ `Content-Disposition: form-data; name="${name}"; filename="${fileName}"\r\n` +
665
+ `Content-Type: ${contentType}\r\n\r\n`
666
+ ));
667
+ parts.push(fileBytes);
668
+ parts.push(getTextEncoder().encode('\r\n'));
669
+ }
670
+ }
671
+
672
+ // Per WPT: empty FormData produces an empty body
673
+ if (parts.length === 0) {
674
+ return {
675
+ body: new Uint8Array(0),
676
+ contentType: `multipart/form-data; boundary=${boundary}`,
677
+ };
678
+ }
679
+
680
+ parts.push(getTextEncoder().encode(`--${boundary}--\r\n`));
681
+
682
+ return {
683
+ body: concatUint8Arrays(parts),
684
+ contentType: `multipart/form-data; boundary=${boundary}`,
685
+ };
686
+ }
687
+
688
+ /**
689
+ * Internal async encoding for FormData (uses arrayBuffer for Blobs).
690
+ */
691
+ async function encodeFormDataInternalAsync(body: FormData): Promise<{ body: Uint8Array; contentType: string }> {
692
+ // Use FormData's built-in encoding if available
693
+ if (typeof (body as any)._encode === 'function') {
694
+ return (body as any)._encode();
695
+ }
696
+
697
+ const boundary = generateBoundary();
698
+ const parts: Uint8Array[] = [];
699
+
700
+ for (const [name, value] of (body as any).entries()) {
701
+ if (typeof value === 'string') {
702
+ parts.push(getTextEncoder().encode(
703
+ `--${boundary}\r\n` +
704
+ `Content-Disposition: form-data; name="${name}"\r\n\r\n` +
705
+ `${value}\r\n`
706
+ ));
707
+ } else if (isBlob(value)) {
708
+ const buffer = await value.arrayBuffer();
709
+ const fileBytes = new Uint8Array(buffer);
710
+ const fileName = (value as File).name || 'blob';
711
+ const contentType = value.type || 'application/octet-stream';
712
+
713
+ parts.push(getTextEncoder().encode(
714
+ `--${boundary}\r\n` +
715
+ `Content-Disposition: form-data; name="${name}"; filename="${fileName}"\r\n` +
716
+ `Content-Type: ${contentType}\r\n\r\n`
717
+ ));
718
+ parts.push(fileBytes);
719
+ parts.push(getTextEncoder().encode('\r\n'));
720
+ }
721
+ }
722
+
723
+ // Per WPT: empty FormData produces an empty body
724
+ if (parts.length === 0) {
725
+ return {
726
+ body: new Uint8Array(0),
727
+ contentType: `multipart/form-data; boundary=${boundary}`,
728
+ };
729
+ }
730
+
731
+ parts.push(getTextEncoder().encode(`--${boundary}--\r\n`));
732
+
733
+ return {
734
+ body: concatUint8Arrays(parts),
735
+ contentType: `multipart/form-data; boundary=${boundary}`,
736
+ };
737
+ }
738
+
739
+ /**
740
+ * Encode FormData body with its content type (for consistent boundary).
741
+ * Returns both the body bytes and content-type header.
742
+ */
743
+ export function encodeFormData(body: FormData): { body: Uint8Array; contentType: string } {
744
+ // Check cache first — getContentTypeForBody() may have already encoded this FormData
745
+ // with a specific boundary. We must reuse it to keep Content-Type and body consistent.
746
+ let cached = formDataEncodeCache.get(body);
747
+ if (cached) return cached;
748
+
749
+ if (typeof (body as any)._encode === 'function') {
750
+ const result = (body as any)._encode();
751
+ formDataEncodeCache.set(body, result);
752
+ return result;
753
+ }
754
+
755
+ // Fallback encoding
756
+ const result = encodeFormDataInternal(body);
757
+ formDataEncodeCache.set(body, result);
758
+ return result;
759
+ }
760
+
761
+ /**
762
+ * Parse array buffer as JSON.
763
+ */
764
+ export function parseJson(buffer: ArrayBuffer): unknown {
765
+ // Per Fetch spec: reject UTF-16 encoded content (only UTF-8 is valid for JSON)
766
+ const bytes = new Uint8Array(buffer);
767
+ if (bytes.length >= 2) {
768
+ // UTF-16 BE BOM: FE FF, or UTF-16 LE BOM: FF FE
769
+ if ((bytes[0] === 0xFE && bytes[1] === 0xFF) || (bytes[0] === 0xFF && bytes[1] === 0xFE)) {
770
+ throw new SyntaxError('Invalid JSON: UTF-16 encoded content is not valid');
771
+ }
772
+ // Detect BOM-less UTF-16: valid JSON always starts with an ASCII character
773
+ // (whitespace, '{', '[', '"', digit, 't', 'f', 'n'). In UTF-16 LE the
774
+ // second byte is 0x00, in UTF-16 BE the first byte is 0x00.
775
+ if (bytes[0] === 0x00 || bytes[1] === 0x00) {
776
+ throw new SyntaxError('Invalid JSON: UTF-16 encoded content is not valid');
777
+ }
778
+ }
779
+ const text = getTextDecoder().decode(buffer);
780
+ return JSON.parse(text);
781
+ }
782
+
783
+ /**
784
+ * Parse array buffer as text.
785
+ */
786
+ export function parseText(buffer: ArrayBuffer): string {
787
+ return getTextDecoder().decode(buffer);
788
+ }
789
+
790
+ /**
791
+ * Create a ReadableStream from a Uint8Array.
792
+ * Returns null if ReadableStream is not available (falls back to buffer-based reading).
793
+ */
794
+ export function createReadableStreamFromUint8Array(
795
+ data: Uint8Array | null
796
+ ): ReadableStream<Uint8Array> | null {
797
+ if (data === null) {
798
+ return null;
799
+ }
800
+
801
+ // Byte streams may detach the enqueued chunk's backing buffer. Keep the
802
+ // caller-owned buffer stable by enqueueing a cloned view instead.
803
+ const streamData = new Uint8Array(data.byteLength);
804
+ streamData.set(data);
805
+
806
+ const streamCtor =
807
+ (globalThis as any).ReadableStream || RuntimeReadableStream;
808
+
809
+ // Check if a ReadableStream constructor is available - it might not be in
810
+ // all environments.
811
+ if (typeof streamCtor !== 'function') {
812
+ return null;
813
+ }
814
+
815
+ // Create a byte stream (type: 'bytes') so that BYOB readers work
816
+ return new (streamCtor as typeof ReadableStream)({
817
+ type: 'bytes',
818
+ start(controller: any) {
819
+ if (streamData.byteLength > 0) {
820
+ controller.enqueue(streamData);
821
+ }
822
+ controller.close();
823
+ },
824
+ } as any);
825
+ }
826
+
827
+ /**
828
+ * Extract the boundary parameter from a multipart/form-data Content-Type header.
829
+ * @param contentType The Content-Type header value
830
+ * @returns The boundary string, or null if not found
831
+ */
832
+ export function extractBoundary(contentType: string): string | null {
833
+ // Match boundary parameter: boundary=VALUE or boundary="VALUE"
834
+ const match = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/i);
835
+ if (!match) return null;
836
+ return match[1] ?? match[2] ?? null;
837
+ }
838
+
839
+ /**
840
+ * Find the index of a byte sequence (needle) within a Uint8Array (haystack),
841
+ * starting from the given offset.
842
+ */
843
+ function indexOfBytes(haystack: Uint8Array, needle: Uint8Array, offset: number = 0): number {
844
+ const haystackLen = haystack.length;
845
+ const needleLen = needle.length;
846
+ if (needleLen === 0) return offset;
847
+ if (offset + needleLen > haystackLen) return -1;
848
+
849
+ outer:
850
+ for (let i = offset; i <= haystackLen - needleLen; i++) {
851
+ for (let j = 0; j < needleLen; j++) {
852
+ if (haystack[i + j] !== needle[j]) {
853
+ continue outer;
854
+ }
855
+ }
856
+ return i;
857
+ }
858
+ return -1;
859
+ }
860
+
861
+ /**
862
+ * Parse a Content-Disposition header value to extract name and filename parameters.
863
+ */
864
+ function parseContentDisposition(header: string): { name: string; filename?: string } {
865
+ let name = '';
866
+ let filename: string | undefined;
867
+
868
+ // Extract name parameter
869
+ const nameMatch = header.match(/\bname=(?:"([^"]*(?:\\.[^"]*)*)"|([^\s;]+))/i);
870
+ if (nameMatch) {
871
+ name = (nameMatch[1] ?? nameMatch[2] ?? '').replace(/\\"/g, '"');
872
+ }
873
+
874
+ // Extract filename parameter
875
+ const filenameMatch = header.match(/\bfilename=(?:"([^"]*(?:\\.[^"]*)*)"|([^\s;]+))/i);
876
+ if (filenameMatch) {
877
+ filename = (filenameMatch[1] ?? filenameMatch[2] ?? '').replace(/\\"/g, '"');
878
+ }
879
+
880
+ return { name, filename };
881
+ }
882
+
883
+ /**
884
+ * Parse the headers section of a multipart part.
885
+ * Returns a map of lowercased header names to their values.
886
+ */
887
+ function parsePartHeaders(headerBytes: Uint8Array): Map<string, string> {
888
+ const decoder = getTextDecoder();
889
+ const headerText = decoder.decode(headerBytes);
890
+ const headers = new Map<string, string>();
891
+
892
+ // Split by CRLF, handling potential line folding
893
+ const lines = headerText.split('\r\n');
894
+ for (const line of lines) {
895
+ if (!line) continue;
896
+ const colonIndex = line.indexOf(':');
897
+ if (colonIndex === -1) continue;
898
+ const name = line.slice(0, colonIndex).trim().toLowerCase();
899
+ const value = line.slice(colonIndex + 1).trim();
900
+ headers.set(name, value);
901
+ }
902
+
903
+ return headers;
904
+ }
905
+
906
+ /**
907
+ * Parse a multipart/form-data body into a FormData object.
908
+ *
909
+ * @param body The raw body bytes
910
+ * @param boundary The boundary string from the Content-Type header
911
+ * @returns A populated FormData object
912
+ */
913
+ export function parseMultipartFormData(body: Uint8Array, boundary: string): FormData {
914
+ const encoder = getTextEncoder();
915
+ const formData = new FormData();
916
+
917
+ // Boundary markers
918
+ const dashBoundary = encoder.encode('--' + boundary);
919
+ const crlf = encoder.encode('\r\n');
920
+ const doubleCrlf = encoder.encode('\r\n\r\n');
921
+
922
+ // Find the first boundary
923
+ let pos = indexOfBytes(body, dashBoundary, 0);
924
+ if (pos === -1) {
925
+ throw new TypeError('Failed to parse multipart/form-data body: boundary not found');
926
+ }
927
+
928
+ // Move past the first boundary line
929
+ pos += dashBoundary.length;
930
+
931
+ // Check if immediately followed by -- (closing boundary with no parts)
932
+ if (pos + 2 <= body.length && body[pos] === 0x2d && body[pos + 1] === 0x2d) {
933
+ return formData;
934
+ }
935
+
936
+ // Skip CRLF after boundary
937
+ if (pos + 2 <= body.length && body[pos] === 0x0d && body[pos + 1] === 0x0a) {
938
+ pos += 2;
939
+ }
940
+
941
+ let foundClosingBoundary = false;
942
+
943
+ while (pos < body.length) {
944
+ // Find the end of headers (double CRLF)
945
+ const headersEnd = indexOfBytes(body, doubleCrlf, pos);
946
+ if (headersEnd === -1) break;
947
+
948
+ // Parse headers for this part
949
+ const headerBytes = body.slice(pos, headersEnd);
950
+ const headers = parsePartHeaders(headerBytes);
951
+
952
+ // Move past the double CRLF to the start of the body
953
+ const bodyStart = headersEnd + doubleCrlf.length;
954
+
955
+ // Find the next boundary to determine where this part's body ends
956
+ const nextBoundary = indexOfBytes(body, dashBoundary, bodyStart);
957
+ if (nextBoundary === -1) break;
958
+
959
+ // The body ends before the CRLF preceding the boundary
960
+ // The format is: body\r\n--boundary
961
+ let bodyEnd = nextBoundary;
962
+ if (bodyEnd >= 2 && body[bodyEnd - 2] === 0x0d && body[bodyEnd - 1] === 0x0a) {
963
+ bodyEnd -= 2;
964
+ }
965
+
966
+ const partBody = body.slice(bodyStart, bodyEnd);
967
+
968
+ // Parse Content-Disposition to get name and filename
969
+ const disposition = headers.get('content-disposition') ?? '';
970
+ const { name, filename } = parseContentDisposition(disposition);
971
+
972
+ if (!name) {
973
+ // Skip parts without a name (per spec)
974
+ pos = nextBoundary + dashBoundary.length;
975
+ // Check for closing boundary
976
+ if (pos + 2 <= body.length && body[pos] === 0x2d && body[pos + 1] === 0x2d) {
977
+ foundClosingBoundary = true;
978
+ break;
979
+ }
980
+ // Skip CRLF
981
+ if (pos + 2 <= body.length && body[pos] === 0x0d && body[pos + 1] === 0x0a) pos += 2;
982
+ continue;
983
+ }
984
+
985
+ if (filename !== undefined) {
986
+ // File field: create a File object
987
+ const contentType = headers.get('content-type') ?? 'application/octet-stream';
988
+ const file = new File([partBody], filename, { type: contentType });
989
+ formData.append(name, file);
990
+ } else {
991
+ // Text field: decode as UTF-8
992
+ const decoder = getTextDecoder();
993
+ const text = decoder.decode(partBody);
994
+ formData.append(name, text);
995
+ }
996
+
997
+ // Move past the boundary
998
+ pos = nextBoundary + dashBoundary.length;
999
+
1000
+ // Check for closing boundary (--)
1001
+ if (pos + 2 <= body.length && body[pos] === 0x2d && body[pos + 1] === 0x2d) {
1002
+ foundClosingBoundary = true;
1003
+ break;
1004
+ }
1005
+
1006
+ // Skip CRLF after boundary
1007
+ if (pos + 2 <= body.length && body[pos] === 0x0d && body[pos + 1] === 0x0a) {
1008
+ pos += 2;
1009
+ }
1010
+ }
1011
+
1012
+ // Per spec: if the multipart body doesn't have a valid closing boundary, reject
1013
+ if (!foundClosingBoundary) {
1014
+ throw new TypeError('Failed to parse multipart/form-data body: missing closing boundary');
1015
+ }
1016
+
1017
+ return formData;
1018
+ }
1019
+
1020
+ /**
1021
+ * Body mixin base class.
1022
+ * Provides common body handling functionality for Request and Response.
1023
+ */
1024
+ export abstract class BodyMixin {
1025
+ protected _body: ReadableStream<Uint8Array> | null = null;
1026
+ protected _bodyUsed = false;
1027
+ protected _bodyBuffer: ArrayBuffer | null = null;
1028
+
1029
+ /**
1030
+ * Per Fetch spec: the abort signal associated with the fetch that created
1031
+ * this response. Body consumption methods check this and reject with the
1032
+ * abort reason if the signal is aborted.
1033
+ */
1034
+ public _signal: AbortSignal | null = null;
1035
+
1036
+ /**
1037
+ * A ReadableStream of the body contents.
1038
+ */
1039
+ get body(): ReadableStream<Uint8Array> | null {
1040
+ return this._body;
1041
+ }
1042
+
1043
+ /**
1044
+ * A Boolean that indicates whether the body has been read.
1045
+ * Per spec: also true when the body stream has been disturbed (read from)
1046
+ * or locked (e.g., via getReader() or pipeTo()).
1047
+ */
1048
+ get bodyUsed(): boolean {
1049
+ if (this._bodyUsed) return true;
1050
+ if (this._body !== null) {
1051
+ if ((this._body as any)._disturbed) return true;
1052
+ }
1053
+ return false;
1054
+ }
1055
+
1056
+ /**
1057
+ * Whether this object has a body. Subclasses can override to check
1058
+ * additional state (e.g., Request._bodyInit).
1059
+ */
1060
+ protected _hasBody(): boolean {
1061
+ return this._body !== null || this._bodyBuffer !== null;
1062
+ }
1063
+
1064
+ /**
1065
+ * Determine whether a body read should fail before attempting consumption.
1066
+ * Bun compat expects a reused body to report "Body already used" even if the
1067
+ * underlying stream is already locked from the prior read.
1068
+ */
1069
+ protected _getBodyReadPreconditionError(): TypeError | null {
1070
+ if (this.bodyUsed) {
1071
+ return new BodyConsumedError();
1072
+ }
1073
+
1074
+ if (this._body !== null && typeof (this._body as any).locked !== 'undefined' && (this._body as any).locked) {
1075
+ return new TypeError(
1076
+ isBunCompatFetchTest() ? 'ReadableStream is locked' : 'Failed to execute: body stream is locked'
1077
+ );
1078
+ }
1079
+
1080
+ return null;
1081
+ }
1082
+
1083
+ /**
1084
+ * Mark body as used and throw if already used.
1085
+ * Per spec: consuming a null body returns empty without setting bodyUsed.
1086
+ */
1087
+ protected _consumeBody(): void {
1088
+ if (this.bodyUsed) {
1089
+ throw new BodyConsumedError();
1090
+ }
1091
+ // Per spec: only set bodyUsed if there is actually a body
1092
+ if (this._hasBody()) {
1093
+ this._bodyUsed = true;
1094
+ }
1095
+ // Per Fetch spec: consuming a body locks its stream.
1096
+ // When _bodyBuffer is cached, _getBodyBuffer() won't read the stream,
1097
+ // so we must lock it here. For stream-only bodies (_bodyBuffer === null),
1098
+ // readableStreamToUint8Array() will lock the stream when it runs.
1099
+ if (this._body !== null && this._bodyBuffer !== null && !this._body.locked) {
1100
+ (this._body as any)._disturbed = true;
1101
+ try { this._body.getReader(); } catch {}
1102
+ }
1103
+ }
1104
+
1105
+ /**
1106
+ * Get the body as ArrayBuffer.
1107
+ * Must be implemented by subclasses.
1108
+ */
1109
+ protected abstract _getBodyBuffer(): Promise<ArrayBuffer>;
1110
+
1111
+ /**
1112
+ * Get the Content-Type header value.
1113
+ * Must be implemented by subclasses to support multipart formData() parsing.
1114
+ */
1115
+ protected abstract _getContentType(): string | null;
1116
+
1117
+ /**
1118
+ * Per Fetch spec: check if the associated abort signal is aborted, and if so
1119
+ * reject with the signal's reason.
1120
+ */
1121
+ protected _checkAbortSignal(): Error | null {
1122
+ if (this._signal && this._signal.aborted) {
1123
+ return this._signal.reason ??
1124
+ new DOMException('The operation was aborted.', 'AbortError');
1125
+ }
1126
+ return null;
1127
+ }
1128
+
1129
+ /**
1130
+ * Returns a promise that resolves with an ArrayBuffer.
1131
+ * Note: uses explicit promise chain (not async) to avoid compiled async
1132
+ * wrapper creating intermediate promises that trigger unhandled rejection tracking.
1133
+ */
1134
+ arrayBuffer(): Promise<ArrayBuffer> {
1135
+ const preconditionError = this._getBodyReadPreconditionError();
1136
+ if (preconditionError) {
1137
+ return Promise.reject(preconditionError);
1138
+ }
1139
+ try {
1140
+ this._consumeBody();
1141
+ } catch (e) {
1142
+ return Promise.reject(e);
1143
+ }
1144
+ return this._getBodyBuffer();
1145
+ }
1146
+
1147
+ /**
1148
+ * Returns a promise that resolves with a Blob.
1149
+ */
1150
+ blob(): Promise<Blob> {
1151
+ const preconditionError = this._getBodyReadPreconditionError();
1152
+ if (preconditionError) {
1153
+ return Promise.reject(preconditionError);
1154
+ }
1155
+ return this.arrayBuffer().then((buffer) => {
1156
+ const type = normalizeBlobContentType(this._getContentType());
1157
+ return type ? new Blob([buffer], { type }) : new Blob([buffer]);
1158
+ });
1159
+ }
1160
+
1161
+ /**
1162
+ * Returns a promise that resolves with a Uint8Array.
1163
+ */
1164
+ bytes(): Promise<Uint8Array> {
1165
+ const preconditionError = this._getBodyReadPreconditionError();
1166
+ if (preconditionError) {
1167
+ return Promise.reject(preconditionError);
1168
+ }
1169
+ return this.arrayBuffer().then((buffer) => new Uint8Array(buffer));
1170
+ }
1171
+
1172
+ /**
1173
+ * Returns a promise that resolves with a FormData.
1174
+ * Supports both multipart/form-data and application/x-www-form-urlencoded.
1175
+ */
1176
+ formData(): Promise<FormData> {
1177
+ const preconditionError = this._getBodyReadPreconditionError();
1178
+ if (preconditionError) {
1179
+ return Promise.reject(preconditionError);
1180
+ }
1181
+
1182
+ try {
1183
+ this._consumeBody();
1184
+ } catch (e) {
1185
+ return Promise.reject(e);
1186
+ }
1187
+
1188
+ const contentType = this._getContentType();
1189
+ const ct = contentType?.toLowerCase() ?? '';
1190
+ const isMultipart = ct.startsWith('multipart/form-data');
1191
+ const isUrlEncoded = ct.startsWith('application/x-www-form-urlencoded');
1192
+
1193
+ const self = this;
1194
+ // Read the body buffer first so that stream errors propagate before
1195
+ // content-type checks (per spec and response-error-from-stream WPT tests).
1196
+ return this._getBodyBuffer().then(function (buffer) {
1197
+ if (!isMultipart && !isUrlEncoded) {
1198
+ throw new TypeError(
1199
+ "Failed to execute 'formData': Content-Type is not 'multipart/form-data' or 'application/x-www-form-urlencoded'"
1200
+ );
1201
+ }
1202
+ // multipart/form-data
1203
+ if (isMultipart) {
1204
+ const boundary = extractBoundary(contentType!);
1205
+ if (!boundary) {
1206
+ throw new TypeError('multipart/form-data Content-Type missing boundary');
1207
+ }
1208
+ const bytes = new Uint8Array(buffer);
1209
+ // Empty body handling: only return empty FormData if the body was
1210
+ // explicitly set (e.g. new Response(new FormData())). A null body
1211
+ // with a multipart Content-Type header should reject per spec.
1212
+ if (bytes.length === 0) {
1213
+ if (self._hasBody()) {
1214
+ return new FormData();
1215
+ }
1216
+ throw new TypeError('Could not parse content as FormData.');
1217
+ }
1218
+ return parseMultipartFormData(bytes, boundary);
1219
+ }
1220
+
1221
+ // application/x-www-form-urlencoded
1222
+ const text = decodeTextForFormData(buffer);
1223
+ const formData = new FormData();
1224
+ const params = new URLSearchParams(text);
1225
+ // Use indexed iteration to avoid the Rolldown for-of → forEach transform
1226
+ // which rewrites the loop body into a regular function, breaking returns.
1227
+ const paramEntries = Array.from(params);
1228
+ for (let i = 0; i < paramEntries.length; i++) {
1229
+ formData.append(paramEntries[i][0], paramEntries[i][1]);
1230
+ }
1231
+ return formData;
1232
+ });
1233
+ }
1234
+
1235
+ /**
1236
+ * Returns a promise that resolves with the result of parsing the body as JSON.
1237
+ */
1238
+ json(): Promise<unknown> {
1239
+ const preconditionError = this._getBodyReadPreconditionError();
1240
+ if (preconditionError) {
1241
+ return Promise.reject(preconditionError);
1242
+ }
1243
+ return this.arrayBuffer().then(parseJson);
1244
+ }
1245
+
1246
+ /**
1247
+ * Returns a promise that resolves with a string (decoded as UTF-8).
1248
+ */
1249
+ text(): Promise<string> {
1250
+ const preconditionError = this._getBodyReadPreconditionError();
1251
+ if (preconditionError) {
1252
+ return Promise.reject(preconditionError);
1253
+ }
1254
+ return this.arrayBuffer().then(parseText);
1255
+ }
1256
+ }