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