quickwin 2026.5.2-3.145209

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 (83) hide show
  1. package/README.md +6 -0
  2. package/examples/pdf_preview.js +440 -0
  3. package/examples/pdf_preview.ts +470 -0
  4. package/examples/preact_demo.js +35 -0
  5. package/examples/preact_demo.tsx +49 -0
  6. package/examples/tray_demo.js +75 -0
  7. package/examples/tray_demo.tsx +79 -0
  8. package/lib/fetch.js +746 -0
  9. package/lib/fetch.ts +811 -0
  10. package/lib/polyfill.js +500 -0
  11. package/lib/polyfill.ts +454 -0
  12. package/lib/preact/hooks.js +287 -0
  13. package/lib/preact/hooks.ts +330 -0
  14. package/lib/preact/jsx-runtime.js +1 -0
  15. package/lib/preact/jsx-runtime.ts +2 -0
  16. package/lib/preact/jsx.d.ts +36 -0
  17. package/lib/preact/layout.js +153 -0
  18. package/lib/preact/layout.ts +183 -0
  19. package/lib/preact/preact.js +54 -0
  20. package/lib/preact/preact.ts +133 -0
  21. package/lib/preact/props.js +99 -0
  22. package/lib/preact/props.ts +119 -0
  23. package/lib/preact/render.js +320 -0
  24. package/lib/preact/render.ts +353 -0
  25. package/lib/websocket.js +540 -0
  26. package/lib/websocket.ts +574 -0
  27. package/package.json +32 -0
  28. package/quickwin.d.ts +657 -0
  29. package/test/add.wasm +0 -0
  30. package/test/complex.wasm +0 -0
  31. package/test/complex_imports.wasm +0 -0
  32. package/test/global_imports.wasm +0 -0
  33. package/test/import_func.wasm +0 -0
  34. package/test/imports.wasm +0 -0
  35. package/test/run.js +86 -0
  36. package/test/run.ts +90 -0
  37. package/test/sjlj.wasm +0 -0
  38. package/test/test_basic.js +7 -0
  39. package/test/test_basic.ts +9 -0
  40. package/test/test_brotli.js +48 -0
  41. package/test/test_brotli.ts +52 -0
  42. package/test/test_fetch_cache.js +131 -0
  43. package/test/test_fetch_cache.ts +141 -0
  44. package/test/test_ffi.js +157 -0
  45. package/test/test_ffi.ts +174 -0
  46. package/test/test_frame_encoding.js +128 -0
  47. package/test/test_frame_encoding.ts +132 -0
  48. package/test/test_helper.js +84 -0
  49. package/test/test_helper.ts +80 -0
  50. package/test/test_http_import.js +78 -0
  51. package/test/test_http_import.ts +74 -0
  52. package/test/test_mupdf_render.js +69 -0
  53. package/test/test_mupdf_render.ts +74 -0
  54. package/test/test_mupdf_twice.js +77 -0
  55. package/test/test_mupdf_twice.ts +81 -0
  56. package/test/test_mupdf_wasm.js +33 -0
  57. package/test/test_mupdf_wasm.ts +30 -0
  58. package/test/test_net_event.js +63 -0
  59. package/test/test_net_event.ts +59 -0
  60. package/test/test_net_fetch.js +153 -0
  61. package/test/test_net_fetch.ts +131 -0
  62. package/test/test_net_websocket.js +158 -0
  63. package/test/test_net_websocket.ts +144 -0
  64. package/test/test_polyfill.js +58 -0
  65. package/test/test_polyfill.ts +60 -0
  66. package/test/test_url.js +173 -0
  67. package/test/test_url.ts +183 -0
  68. package/test/test_wasm_basic.js +82 -0
  69. package/test/test_wasm_basic.ts +70 -0
  70. package/test/test_wasm_import_global.js +41 -0
  71. package/test/test_wasm_import_global.ts +39 -0
  72. package/test/test_wasm_sjlj.js +153 -0
  73. package/test/test_wasm_sjlj.ts +134 -0
  74. package/test/test_wasm_types.js +96 -0
  75. package/test/test_wasm_types.ts +108 -0
  76. package/test/types.wasm +0 -0
  77. package/tsconfig.json +18 -0
  78. package/vendor/mupdf-wasm/mupdf-wasm.d.ts +571 -0
  79. package/vendor/mupdf-wasm/mupdf-wasm.js +2749 -0
  80. package/vendor/mupdf-wasm/mupdf-wasm.wasm +0 -0
  81. package/vendor/mupdf-wasm/mupdf.d.ts +939 -0
  82. package/vendor/mupdf-wasm/mupdf.js +3317 -0
  83. package/win-mingw64.exe +0 -0
package/lib/fetch.ts ADDED
@@ -0,0 +1,811 @@
1
+ import '../lib/polyfill.js'
2
+ import * as sock from 'sock'
3
+ import * as wolfssl from 'wolfssl'
4
+ import * as os from 'os'
5
+ import * as brotli from 'brotli'
6
+
7
+ const setTimeout = os.setTimeout
8
+ const clearTimeout = os.clearTimeout
9
+
10
+ interface RequestOptions {
11
+ method?: string
12
+ headers?: { [key: string]: string }
13
+ body?: string
14
+ timeout?: number
15
+ redirect?: 'follow' | 'manual' | 'error'
16
+ maxRedirects?: number
17
+ }
18
+
19
+ function str2ab(str: string): ArrayBuffer {
20
+ const buf = new ArrayBuffer(str.length)
21
+ const view = new Uint8Array(buf)
22
+ for (let i = 0; i < str.length; i++) view[i] = str.charCodeAt(i)
23
+ return buf
24
+ }
25
+
26
+ function ab2str(buf: ArrayBuffer): string {
27
+ const view = new Uint8Array(buf)
28
+ let str = ''
29
+ for (let i = 0; i < view.length; i++) str += String.fromCharCode(view[i])
30
+ return str
31
+ }
32
+
33
+ function getArrayBuffer(view: Uint8Array): ArrayBuffer {
34
+ if (view.buffer instanceof ArrayBuffer) return view.buffer
35
+ const copy = new ArrayBuffer(view.byteLength)
36
+ new Uint8Array(copy).set(view)
37
+ return copy
38
+ }
39
+
40
+ // ── ReadableStream implementation ──
41
+
42
+ type ReadResult =
43
+ | { done: true; value?: undefined }
44
+ | { done: false; value: Uint8Array }
45
+
46
+ type PendingRead = { resolve: (result: ReadResult) => void, reject: (err: Error) => void }
47
+
48
+ class _QuickReadableStream {
49
+ _chunks: Uint8Array[] = []
50
+ _state: 'readable' | 'closed' | 'errored' = 'readable'
51
+ _pendingRead: PendingRead | null = null
52
+ _locked: boolean = false
53
+ _sock: number | null
54
+ _ssl: number | null
55
+ _isHTTPS: boolean
56
+ _cleanup: (() => void) | null
57
+ _contentLength: number
58
+ _receivedBytes: number = 0
59
+
60
+ constructor(sock: number, ssl: number | null, isHTTPS: boolean, cleanup: () => void, contentLength: number = 0) {
61
+ this._sock = sock
62
+ this._ssl = ssl
63
+ this._isHTTPS = isHTTPS
64
+ this._cleanup = cleanup
65
+ this._contentLength = contentLength
66
+ }
67
+
68
+ get locked(): boolean { return this._locked }
69
+
70
+ getReader(): _QuickReader {
71
+ if (this._locked) throw new TypeError('ReadableStream is locked')
72
+ this._locked = true
73
+ return new _QuickReader(this)
74
+ }
75
+
76
+ cancel(reason?: any): void {
77
+ if (this._state !== 'readable') return
78
+ this._state = 'closed'
79
+ this._locked = false
80
+ if (this._cleanup) {
81
+ this._cleanup()
82
+ this._cleanup = null
83
+ }
84
+ if (this._pendingRead) {
85
+ const pr = this._pendingRead
86
+ this._pendingRead = null
87
+ pr.resolve({ done: true })
88
+ }
89
+ }
90
+
91
+ // ── Internal methods called by socket handler ──
92
+
93
+ _pushChunk(buf: ArrayBuffer): void {
94
+ if (this._state !== 'readable') return
95
+ const chunk = new Uint8Array(buf)
96
+ this._receivedBytes += chunk.length
97
+ if (this._pendingRead) {
98
+ const pr = this._pendingRead
99
+ this._pendingRead = null
100
+ pr.resolve({ done: false, value: chunk })
101
+ } else {
102
+ this._chunks.push(chunk)
103
+ }
104
+ // Auto-close if Content-Length is satisfied
105
+ if (this._contentLength > 0 && this._receivedBytes >= this._contentLength) {
106
+ this._close()
107
+ }
108
+ }
109
+
110
+ _close(): void {
111
+ if (this._state !== 'readable') return
112
+ this._state = 'closed'
113
+ if (this._cleanup) {
114
+ this._cleanup()
115
+ this._cleanup = null
116
+ }
117
+ if (this._pendingRead) {
118
+ const pr = this._pendingRead
119
+ this._pendingRead = null
120
+ pr.resolve({ done: true })
121
+ }
122
+ }
123
+
124
+ _error(err: Error): void {
125
+ if (this._state !== 'readable') return
126
+ this._state = 'errored'
127
+ if (this._pendingRead) {
128
+ const pr = this._pendingRead
129
+ this._pendingRead = null
130
+ pr.reject(err)
131
+ }
132
+ }
133
+
134
+ _tryRead(): Promise<ReadResult> | null {
135
+ if (this._chunks.length > 0) {
136
+ const chunk = this._chunks.shift()!
137
+ return Promise.resolve({ done: false, value: chunk })
138
+ }
139
+ if (this._state === 'closed') return Promise.resolve({ done: true })
140
+ if (this._state === 'errored') return Promise.reject(new Error('Stream errored'))
141
+ return null
142
+ }
143
+ }
144
+
145
+ class _PreloadedStream {
146
+ _buffer: Uint8Array
147
+ _offset: number = 0
148
+ _state: 'readable' | 'closed' = 'readable'
149
+ _pendingRead: PendingRead | null = null
150
+ _locked: boolean = false
151
+
152
+ constructor(buffer: ArrayBuffer) {
153
+ this._buffer = new Uint8Array(buffer)
154
+ }
155
+
156
+ get locked(): boolean { return this._locked }
157
+
158
+ getReader() {
159
+ if (this._locked) throw new TypeError('ReadableStream is locked')
160
+ this._locked = true
161
+ const stream = this
162
+ return {
163
+ read(): Promise<ReadResult> {
164
+ if (stream._offset < stream._buffer.length) {
165
+ const chunk = stream._buffer.slice(stream._offset, stream._offset + 8192)
166
+ stream._offset += chunk.length
167
+ return Promise.resolve({ done: false, value: chunk })
168
+ }
169
+ return Promise.resolve({ done: true })
170
+ },
171
+ cancel(reason?: any): void {
172
+ stream._state = 'closed'
173
+ stream._locked = false
174
+ },
175
+ releaseLock(): void {
176
+ // no-op
177
+ }
178
+ }
179
+ }
180
+
181
+ cancel(reason?: any): void {
182
+ this._state = 'closed'
183
+ this._locked = false
184
+ }
185
+
186
+ _tryRead(): Promise<ReadResult> | null {
187
+ if (this._offset < this._buffer.length) {
188
+ const chunk = this._buffer.slice(this._offset, this._offset + 8192)
189
+ this._offset += chunk.length
190
+ return Promise.resolve({ done: false, value: chunk })
191
+ }
192
+ if (this._state === 'closed') return Promise.resolve({ done: true })
193
+ return null
194
+ }
195
+ }
196
+
197
+ class _QuickReader {
198
+ _stream: _QuickReadableStream | null
199
+
200
+ constructor(stream: _QuickReadableStream) {
201
+ this._stream = stream
202
+ }
203
+
204
+ read(): Promise<ReadResult> {
205
+ if (!this._stream) throw new TypeError('Reader released')
206
+ const result = this._stream._tryRead()
207
+ if (result) return result
208
+ return new Promise((resolve, reject) => {
209
+ if (!this._stream) { reject(new TypeError('Reader released')); return }
210
+ this._stream._pendingRead = { resolve, reject }
211
+ })
212
+ }
213
+
214
+ cancel(reason?: any): void {
215
+ if (this._stream) {
216
+ this._stream.cancel(reason)
217
+ this._stream = null
218
+ }
219
+ }
220
+
221
+ releaseLock(): void {
222
+ this._stream = null
223
+ }
224
+ }
225
+
226
+ // ── Headers ──
227
+
228
+ class FetchHeaders {
229
+ private _headers: { [key: string]: string } = {}
230
+
231
+ constructor(init?: { [key: string]: string } | FetchHeaders) {
232
+ if (init) {
233
+ if (init instanceof FetchHeaders) {
234
+ this._headers = { ...init._headers }
235
+ } else if (typeof init === 'object') {
236
+ for (const key in init) {
237
+ this._headers[key.toLowerCase()] = init[key]
238
+ }
239
+ }
240
+ }
241
+ }
242
+
243
+ append(name: string, value: string): void {
244
+ const key = name.toLowerCase()
245
+ if (this._headers[key]) {
246
+ this._headers[key] += ', ' + value
247
+ } else {
248
+ this._headers[key] = value
249
+ }
250
+ }
251
+
252
+ delete(name: string): void {
253
+ delete this._headers[name.toLowerCase()]
254
+ }
255
+
256
+ get(name: string): string | null {
257
+ return this._headers[name.toLowerCase()] || null
258
+ }
259
+
260
+ has(name: string): boolean {
261
+ return name.toLowerCase() in this._headers
262
+ }
263
+
264
+ set(name: string, value: string): void {
265
+ this._headers[name.toLowerCase()] = value
266
+ }
267
+
268
+ forEach(callback: (value: string, name: string, headers: FetchHeaders) => void): void {
269
+ for (const key in this._headers) {
270
+ callback(this._headers[key], key, this)
271
+ }
272
+ }
273
+
274
+ entries(): IterableIterator<[string, string]> {
275
+ const entries: [string, string][] = []
276
+ for (const key in this._headers) {
277
+ entries.push([key, this._headers[key]])
278
+ }
279
+ return entries[Symbol.iterator]() as IterableIterator<[string, string]>
280
+ }
281
+
282
+ keys(): IterableIterator<string> {
283
+ return Object.keys(this._headers)[Symbol.iterator]() as IterableIterator<string>
284
+ }
285
+
286
+ values(): IterableIterator<string> {
287
+ const values: string[] = []
288
+ for (const key in this._headers) {
289
+ values.push(this._headers[key])
290
+ }
291
+ return values[Symbol.iterator]() as IterableIterator<string>
292
+ }
293
+
294
+ [Symbol.iterator](): IterableIterator<[string, string]> {
295
+ return this.entries()
296
+ }
297
+ }
298
+
299
+ // ── Response ──
300
+
301
+ class FetchResponse {
302
+ readonly status: number
303
+ readonly statusText: string
304
+ readonly headers: FetchHeaders
305
+ readonly ok: boolean
306
+ redirected: boolean
307
+ type: string
308
+ url: string
309
+ body: _QuickReadableStream
310
+ private _bodyConsumed: boolean = false
311
+ _preloadedBody: ArrayBuffer | null = null
312
+
313
+ get bodyUsed(): boolean {
314
+ return this._bodyConsumed || this.body.locked
315
+ }
316
+
317
+ constructor(status: number, statusText: string, headers: FetchHeaders, bodyStream: _QuickReadableStream) {
318
+ this.status = status
319
+ this.statusText = statusText
320
+ this.headers = headers
321
+ this.ok = status >= 200 && status < 300
322
+ this.redirected = false
323
+ this.type = 'basic'
324
+ this.url = ''
325
+ this.body = bodyStream
326
+ }
327
+
328
+ async text(): Promise<string> {
329
+ if (this._preloadedBody) {
330
+ if (this._bodyConsumed) throw new TypeError('Body already used')
331
+ this._bodyConsumed = true
332
+ return ab2str(this._preloadedBody)
333
+ }
334
+ if (this.bodyUsed) throw new TypeError('Body already used')
335
+ this._bodyConsumed = true
336
+ const reader = this.body.getReader()
337
+ let result = ''
338
+ while (true) {
339
+ const { done, value } = await reader.read()
340
+ if (done) break
341
+ for (let i = 0; i < value.length; i++) result += String.fromCharCode(value[i])
342
+ }
343
+ return result
344
+ }
345
+
346
+ async json(): Promise<any> {
347
+ const text = await this.text()
348
+ return JSON.parse(text)
349
+ }
350
+
351
+ async arrayBuffer(): Promise<ArrayBuffer> {
352
+ if (this._preloadedBody) {
353
+ if (this._bodyConsumed) throw new TypeError('Body already used')
354
+ this._bodyConsumed = true
355
+ return this._preloadedBody
356
+ }
357
+ if (this.bodyUsed) throw new TypeError('Body already used')
358
+ this._bodyConsumed = true
359
+ const reader = this.body.getReader()
360
+ const chunks: Uint8Array[] = []
361
+ let totalLength = 0
362
+ while (true) {
363
+ const { done, value } = await reader.read()
364
+ if (done) break
365
+ chunks.push(value)
366
+ totalLength += value.length
367
+ }
368
+ const result = new Uint8Array(totalLength)
369
+ let offset = 0
370
+ for (const chunk of chunks) {
371
+ result.set(chunk, offset)
372
+ offset += chunk.length
373
+ }
374
+ return getArrayBuffer(result)
375
+ }
376
+ }
377
+
378
+ // ── HTTP Response Parser ──
379
+
380
+ interface ParsedResponse {
381
+ status: number
382
+ statusText: string
383
+ headers: FetchHeaders
384
+ }
385
+
386
+ function parseHeaders(data: string): ParsedResponse | null {
387
+ const headerEnd = data.indexOf('\r\n\r\n')
388
+ if (headerEnd < 0) return null
389
+
390
+ const headerPart = data.slice(0, headerEnd)
391
+ const lines = headerPart.split('\r\n')
392
+ const statusLine = lines[0]
393
+ const match = statusLine.match(/^HTTP\/\d\.\d\s+(\d+)\s+(.*)$/)
394
+ if (!match) throw new Error('Invalid HTTP response: ' + statusLine)
395
+
396
+ const status = parseInt(match[1], 10)
397
+ const statusText = match[2]
398
+
399
+ const headers = new FetchHeaders()
400
+ for (let i = 1; i < lines.length; i++) {
401
+ const colonIndex = lines[i].indexOf(':')
402
+ if (colonIndex > 0) {
403
+ const name = lines[i].slice(0, colonIndex).trim()
404
+ const value = lines[i].slice(colonIndex + 1).trim()
405
+ headers.append(name, value)
406
+ }
407
+ }
408
+
409
+ return { status, statusText, headers }
410
+ }
411
+
412
+ // ── State machine constants ──
413
+
414
+ const ST_CONNECTING = 0
415
+ const ST_HANDSHAKE = 1
416
+ const ST_SEND = 2
417
+ const ST_RECV_HEADERS = 3
418
+ const ST_RECV_BODY = 4
419
+ const ST_DONE = 5
420
+
421
+ // ── Main fetch request ──
422
+
423
+ function fetchRequest(parsedUrl: { protocol: string; hostname: string; port: string; pathname: string }, options: RequestOptions): Promise<FetchResponse> {
424
+ return new Promise((resolve, reject) => {
425
+ const method = options.method || 'GET'
426
+ const headers = new FetchHeaders(options.headers)
427
+ const body = options.body || null
428
+ const timeout = options.timeout || 30000
429
+ const isHTTPS = parsedUrl.protocol === 'https:'
430
+
431
+ if (!headers.has('host')) headers.set('Host', parsedUrl.hostname)
432
+ if (!headers.has('user-agent')) headers.set('User-Agent', 'QuickJS/1.0')
433
+ if (!headers.has('connection')) headers.set('Connection', 'close')
434
+ if (!headers.has('accept-encoding')) headers.set('Accept-Encoding', 'br')
435
+ if (body && !headers.has('content-length')) headers.set('Content-Length', String(body.length))
436
+
437
+ let request = method + ' ' + parsedUrl.pathname + ' HTTP/1.1\r\n'
438
+ headers.forEach((value: string, name: string) => {
439
+ request += name + ': ' + value + '\r\n'
440
+ })
441
+ request += '\r\n'
442
+ if (body) request += body
443
+
444
+ let s: number | null = null
445
+ let ssl: number | null = null
446
+ let ctx: number | null = null
447
+ let state = ST_CONNECTING
448
+ let resolved = false
449
+ let timerId: number | undefined
450
+ let stream: _QuickReadableStream | null = null
451
+ let headerBuffer = ''
452
+
453
+ const cleanupSocket = (): void => {
454
+ state = ST_DONE
455
+ if (ssl) { wolfssl.wolfSSL_free(ssl); ssl = null }
456
+ if (ctx) { wolfssl.wolfSSL_CTX_free(ctx); ctx = null }
457
+ if (s) { sock.closesocket(s); s = null }
458
+ }
459
+
460
+ const cleanup = (): void => {
461
+ if (timerId) { clearTimeout(timerId); timerId = undefined }
462
+ }
463
+
464
+ const doResolve = (response: FetchResponse): void => {
465
+ if (!resolved) { resolved = true; cleanup(); resolve(response) }
466
+ }
467
+
468
+ const doReject = (error: Error): void => {
469
+ if (!resolved) { resolved = true; cleanup(); cleanupSocket(); reject(error) }
470
+ }
471
+
472
+ const streamCleanup = (): void => {
473
+ cleanup()
474
+ cleanupSocket()
475
+ }
476
+
477
+ timerId = setTimeout(() => {
478
+ doReject(new Error('Request timeout'))
479
+ }, timeout)
480
+
481
+ s = sock.socket()
482
+ if (!s || s === 0) { doReject(new Error('Failed to create socket')); return }
483
+ const fd: number = s
484
+
485
+ sock.set_on_event(fd, (event: { lNetworkEvents: number; iErrorCode: number[] }) => {
486
+ if (state === ST_DONE) return
487
+
488
+ if (event.lNetworkEvents & sock.FD_CONNECT) {
489
+ const err = event.iErrorCode[0]
490
+ if (err !== 0) { doReject(new Error('Connection failed: ' + err)); return }
491
+
492
+ if (isHTTPS) {
493
+ const method = wolfssl.wolfTLSv1_2_client_method()
494
+ ctx = wolfssl.wolfSSL_CTX_new(method)
495
+ wolfssl.wolfSSL_CTX_set_verify(ctx, wolfssl.SSL_VERIFY_NONE)
496
+ ssl = wolfssl.wolfSSL_new(ctx)
497
+ if (!ssl) { doReject(new Error('SSL_new failed')); return }
498
+ wolfssl.wolfSSL_set_fd(ssl, sock.get_fd(fd))
499
+ const sniHost = headers.get('host') || parsedUrl.hostname
500
+ if (sniHost) wolfssl.wolfSSL_UseSNI(ssl, wolfssl.WOLFSSL_SNI_HOST_NAME, sniHost)
501
+ state = ST_HANDSHAKE
502
+ } else {
503
+ sock.send(fd, str2ab(request))
504
+ state = ST_RECV_HEADERS
505
+ }
506
+ }
507
+
508
+ if ((event.lNetworkEvents & sock.FD_READ) || (event.lNetworkEvents & sock.FD_WRITE)) {
509
+ if (state === ST_HANDSHAKE) {
510
+ if (!ssl) { doReject(new Error('TLS not initialized')); return }
511
+ const ret = wolfssl.wolfSSL_connect(ssl)
512
+ if (ret === wolfssl.SSL_SUCCESS) {
513
+ wolfssl.wolfSSL_write(ssl, str2ab(request))
514
+ state = ST_RECV_HEADERS
515
+ } else {
516
+ const err = wolfssl.wolfSSL_get_error(ssl, ret)
517
+ if (err !== wolfssl.WOLFSSL_ERROR_WANT_READ &&
518
+ err !== wolfssl.WOLFSSL_ERROR_WANT_WRITE) {
519
+ doReject(new Error('TLS handshake failed: ' + err))
520
+ }
521
+ }
522
+ }
523
+ else if (state === ST_RECV_HEADERS) {
524
+ while (true) {
525
+ if (!s && !ssl) break
526
+ let data: ArrayBuffer | null
527
+ if (isHTTPS && ssl) {
528
+ data = wolfssl.wolfSSL_read(ssl, 8192)
529
+ } else if (s) {
530
+ data = sock.recv(s, 8192)
531
+ } else { break }
532
+ if (!data || data.byteLength === 0) break
533
+ headerBuffer += ab2str(data)
534
+ const headerEnd = headerBuffer.indexOf('\r\n\r\n')
535
+ if (headerEnd >= 0) {
536
+ const parsed = parseHeaders(headerBuffer)
537
+ if (!parsed) { doReject(new Error('Failed to parse HTTP headers')); return }
538
+
539
+ const trailingBody = headerBuffer.slice(headerEnd + 4)
540
+
541
+ const contentLength = parseInt(
542
+ parsed.headers.get('content-length') || '0', 10
543
+ )
544
+ stream = new _QuickReadableStream(fd, ssl, isHTTPS, streamCleanup, contentLength)
545
+ if (trailingBody.length > 0) {
546
+ stream._pushChunk(str2ab(trailingBody))
547
+ }
548
+
549
+ const response = new FetchResponse(
550
+ parsed.status, parsed.statusText, parsed.headers, stream
551
+ )
552
+ state = ST_RECV_BODY
553
+ doResolve(response)
554
+ // Break out of header recv loop — any remaining data
555
+ // in the socket will be handled by the ST_RECV_BODY path below
556
+ break
557
+ }
558
+ }
559
+ }
560
+ else if (state === ST_RECV_BODY && stream) {
561
+ while (true) {
562
+ if (!s && !ssl) break
563
+ let data: ArrayBuffer | null
564
+ if (isHTTPS && ssl) {
565
+ data = wolfssl.wolfSSL_read(ssl, 8192)
566
+ } else if (s) {
567
+ data = sock.recv(s, 8192)
568
+ } else { break }
569
+ if (!data || data.byteLength === 0) break
570
+ stream._pushChunk(data)
571
+ }
572
+ }
573
+ }
574
+
575
+ if (event.lNetworkEvents & sock.FD_CLOSE) {
576
+ if (state === ST_DONE) return
577
+ if (state === ST_RECV_HEADERS) {
578
+ doReject(new Error('Connection closed before response'))
579
+ } else if (state === ST_RECV_BODY && stream) {
580
+ while (true) {
581
+ if (!s && !ssl) break
582
+ let data: ArrayBuffer | null
583
+ if (isHTTPS && ssl) {
584
+ data = wolfssl.wolfSSL_read(ssl, 8192)
585
+ } else if (s) {
586
+ data = sock.recv(s, 8192)
587
+ } else { break }
588
+ if (!data || data.byteLength === 0) break
589
+ stream._pushChunk(data)
590
+ }
591
+ stream._close()
592
+ stream = null
593
+ state = ST_DONE
594
+ }
595
+ }
596
+ })
597
+
598
+ const ip = sock.resolve(parsedUrl.hostname)
599
+ if (!ip) {
600
+ doReject(new Error('DNS resolution failed for: ' + parsedUrl.hostname))
601
+ return
602
+ }
603
+ sock.connect(s, ip, parseInt(parsedUrl.port, 10) || (isHTTPS ? 443 : 80))
604
+ })
605
+ }
606
+
607
+ // ── Public fetch (with redirect handling and caching) ──
608
+
609
+ function headersToObj(headers: FetchHeaders): { [key: string]: string } {
610
+ const obj: { [key: string]: string } = {}
611
+ headers.forEach((value: string, name: string) => { obj[name] = value })
612
+ return obj
613
+ }
614
+
615
+ function parseMaxAge(cc: string): number {
616
+ const m = cc.match(/max-age=(\d+)/)
617
+ return m ? parseInt(m[1], 10) : 0
618
+ }
619
+
620
+ async function fetch(url: string, options: RequestOptions = {}): Promise<FetchResponse> {
621
+ const redirectMode = options.redirect || 'follow'
622
+ const maxRedirects = redirectMode === 'follow' ? (options.maxRedirects || 5) : 0
623
+ let currentUrl = url
624
+ let redirectCount = 0
625
+ const method = options.method || 'GET'
626
+ const cache = typeof __httpCache__ !== 'undefined' ? __httpCache__ : null
627
+
628
+ // ── Cache lookup (GET only) ──
629
+ let cachedMeta: any = null
630
+ let conditionalHeaders: { [key: string]: string } = {}
631
+
632
+ if (cache && method === 'GET') {
633
+ const metaStr = cache.readMeta(currentUrl)
634
+ if (metaStr) {
635
+ cachedMeta = JSON.parse(metaStr)
636
+ const age = Math.floor(Date.now() / 1000) - cachedMeta.storedAt
637
+ if (cachedMeta.maxAge > 0 && age < cachedMeta.maxAge) {
638
+ const body = cache.readBody(currentUrl)
639
+ if (body) {
640
+ const resp = new FetchResponse(
641
+ cachedMeta.status, cachedMeta.statusText,
642
+ new FetchHeaders(cachedMeta.headers || {}),
643
+ new _PreloadedStream(body) as any
644
+ )
645
+ resp.url = currentUrl
646
+ resp._preloadedBody = body
647
+ return resp
648
+ }
649
+ }
650
+ if (cachedMeta.etag) conditionalHeaders['If-None-Match'] = cachedMeta.etag
651
+ if (cachedMeta.lastModified) conditionalHeaders['If-Modified-Since'] = cachedMeta.lastModified
652
+ }
653
+ }
654
+
655
+ while (true) {
656
+ const mergedOptions: RequestOptions = { ...options }
657
+ const mergedHeaders = { ...(options.headers || {}) }
658
+ for (const key in conditionalHeaders) {
659
+ mergedHeaders[key] = conditionalHeaders[key]
660
+ }
661
+ if (Object.keys(mergedHeaders).length > 0) mergedOptions.headers = mergedHeaders
662
+
663
+ const parsedUrl = new URL(currentUrl)
664
+ const response = await fetchRequest(parsedUrl, mergedOptions)
665
+
666
+ response.url = currentUrl
667
+
668
+ // ── Handle brotli Content-Encoding ──
669
+ const contentEncoding = response.headers.get('content-encoding') || ''
670
+ if (contentEncoding.includes('br')) {
671
+ const compressedBody = await response.arrayBuffer()
672
+ const decompressedBody = brotli.decompress(compressedBody)
673
+ const newHeaders = new FetchHeaders()
674
+ response.headers.forEach((v: string, k: string) => {
675
+ if (k !== 'content-encoding') newHeaders.set(k, v)
676
+ })
677
+ newHeaders.set('content-length', String(decompressedBody.byteLength))
678
+ const stream = new _PreloadedStream(decompressedBody) as any
679
+ ;(response as any)._preloadedBody = decompressedBody
680
+ ;(response as any)._bodyConsumed = false
681
+ ;(response as any).body = stream
682
+ ;(response as any).headers = newHeaders
683
+ }
684
+
685
+ // ── Handle 304 Not Modified ──
686
+ if (response.status === 304 && cachedMeta && cache) {
687
+ const body = cache.readBody(currentUrl)
688
+ if (body) {
689
+ cachedMeta.storedAt = Math.floor(Date.now() / 1000)
690
+ response.headers.forEach((value: string, name: string) => {
691
+ cachedMeta.headers[name] = value
692
+ })
693
+ cache.writeMeta(currentUrl, JSON.stringify(cachedMeta))
694
+ const resp = new FetchResponse(
695
+ cachedMeta.status, cachedMeta.statusText,
696
+ new FetchHeaders(cachedMeta.headers),
697
+ new _PreloadedStream(body) as any
698
+ )
699
+ resp.url = currentUrl
700
+ resp._preloadedBody = body
701
+ return resp
702
+ }
703
+ }
704
+
705
+ // ── Cache 200 GET responses ──
706
+ if (cache && method === 'GET' && response.status === 200 && !cachedMeta) {
707
+ const body = await response.arrayBuffer()
708
+ const cc = response.headers.get('cache-control') || ''
709
+ const maxAge = parseMaxAge(cc)
710
+ if (maxAge > 0) {
711
+ cache.writeCache(currentUrl, maxAge, body)
712
+ const meta = JSON.stringify({
713
+ storedAt: Math.floor(Date.now() / 1000),
714
+ maxAge,
715
+ status: response.status,
716
+ statusText: response.statusText,
717
+ headers: headersToObj(response.headers),
718
+ etag: response.headers.get('etag') || undefined,
719
+ lastModified: response.headers.get('last-modified') || undefined,
720
+ })
721
+ cache.writeMeta(currentUrl, meta)
722
+ }
723
+ const resp = new FetchResponse(
724
+ response.status, response.statusText,
725
+ response.headers, new _PreloadedStream(body) as any
726
+ )
727
+ resp.url = currentUrl
728
+ resp._preloadedBody = body
729
+ return resp
730
+ }
731
+
732
+ // ── Redirect handling ──
733
+ const isRedirect = response.status === 301 || response.status === 302 ||
734
+ response.status === 303 || response.status === 307 || response.status === 308
735
+
736
+ if (isRedirect) {
737
+ if (redirectMode === 'error') {
738
+ response.body.cancel('redirect')
739
+ throw new Error('Redirect not allowed for: ' + currentUrl)
740
+ }
741
+ if (redirectMode === 'manual') {
742
+ response.redirected = true
743
+ return response
744
+ }
745
+ if (maxRedirects > 0 && redirectCount < maxRedirects) {
746
+ const location = response.headers.get('location')
747
+ if (!location) throw new Error('Redirect response missing Location header')
748
+
749
+ response.body.cancel('redirect')
750
+ currentUrl = new URL(location, currentUrl).href
751
+
752
+ redirectCount++
753
+ response.redirected = true
754
+
755
+ if (response.status === 303) {
756
+ options.method = 'GET'
757
+ delete options.body
758
+ }
759
+ } else {
760
+ if (redirectCount > 0) response.redirected = true
761
+ return response
762
+ }
763
+ } else {
764
+ if (redirectCount > 0) response.redirected = true
765
+ return response
766
+ }
767
+ }
768
+ }
769
+
770
+ // ── Global declarations ──
771
+ // These are available to files that import './lib/fetch.js'
772
+
773
+ declare global {
774
+ interface Headers {
775
+ append(name: string, value: string): void;
776
+ delete(name: string): void;
777
+ get(name: string): string | null;
778
+ has(name: string): boolean;
779
+ set(name: string, value: string): void;
780
+ forEach(callback: (value: string, name: string, parent: Headers) => void): void;
781
+ entries(): IterableIterator<[string, string]>;
782
+ keys(): IterableIterator<string>;
783
+ values(): IterableIterator<string>;
784
+ [Symbol.iterator](): IterableIterator<[string, string]>;
785
+ }
786
+ var Headers: typeof FetchHeaders;
787
+
788
+ interface Response {
789
+ readonly status: number;
790
+ readonly statusText: string;
791
+ readonly headers: Headers;
792
+ readonly ok: boolean;
793
+ readonly redirected: boolean;
794
+ readonly type: string;
795
+ readonly url: string;
796
+ readonly body: ReadableStream;
797
+ readonly bodyUsed: boolean;
798
+ text(): Promise<string>;
799
+ json(): Promise<any>;
800
+ arrayBuffer(): Promise<ArrayBuffer>;
801
+ }
802
+ var Response: typeof FetchResponse;
803
+
804
+ var fetch: (url: string, init?: RequestInit) => Promise<Response>;
805
+ }
806
+
807
+ // ── Register globals ──
808
+
809
+ globalThis.fetch = fetch
810
+ globalThis.Response = FetchResponse
811
+ globalThis.Headers = FetchHeaders