quickwin 3.0.0 → 3.0.2

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 (253) hide show
  1. package/README.en.md +3 -0
  2. package/README.md +2 -0
  3. package/lib/abort.d.ts +21 -0
  4. package/lib/assert.d.ts +2 -0
  5. package/lib/bitmap.d.ts +7 -0
  6. package/lib/cache-utils.d.ts +6 -0
  7. package/lib/certs.d.ts +1 -0
  8. package/lib/dpi.d.ts +9 -0
  9. package/lib/dumpRects.d.ts +2 -0
  10. package/lib/exec-log.d.ts +23 -0
  11. package/lib/fetch.d.ts +4 -0
  12. package/lib/ffi/bind.d.ts +70 -0
  13. package/lib/ffi/ctype.d.ts +117 -0
  14. package/lib/ffi/index.d.ts +2 -0
  15. package/lib/ffi/struct.d.ts +174 -0
  16. package/lib/http-common.d.ts +143 -0
  17. package/lib/http-server.d.ts +59 -0
  18. package/lib/polyfill.d.ts +22 -0
  19. package/lib/react-qw/components/Button.d.ts +9 -0
  20. package/lib/react-qw/components/CheckBox.d.ts +11 -0
  21. package/lib/react-qw/components/ComboBox.d.ts +11 -0
  22. package/lib/react-qw/components/DateTimePicker.d.ts +15 -0
  23. package/lib/react-qw/components/Input.d.ts +15 -0
  24. package/lib/react-qw/components/Link.d.ts +12 -0
  25. package/lib/react-qw/components/ListBox.d.ts +13 -0
  26. package/lib/react-qw/components/ListView.d.ts +37 -0
  27. package/lib/react-qw/components/PathPicker.d.ts +23 -0
  28. package/lib/react-qw/components/ProgressBar.d.ts +9 -0
  29. package/lib/react-qw/components/RadioButton.d.ts +11 -0
  30. package/lib/react-qw/components/ScrollView.d.ts +9 -0
  31. package/lib/react-qw/components/Slider.d.ts +12 -0
  32. package/lib/react-qw/components/Tab.d.ts +14 -0
  33. package/lib/react-qw/components/Tooltip.d.ts +7 -0
  34. package/lib/react-qw/components/TreeView.d.ts +18 -0
  35. package/lib/react-qw/components/index.d.ts +16 -0
  36. package/lib/react-qw/index.d.ts +24 -0
  37. package/lib/react-qw/layout.d.ts +29 -0
  38. package/lib/react-qw/nmhdr.d.ts +1 -0
  39. package/lib/react-qw/props.d.ts +2 -0
  40. package/lib/react-qw/reconciler.d.ts +21 -0
  41. package/lib/stream.d.ts +20 -0
  42. package/lib/text-codec.d.ts +39 -0
  43. package/lib/text-measure.d.ts +14 -0
  44. package/lib/url.d.ts +54 -0
  45. package/lib/websocket.d.ts +73 -0
  46. package/lib/windows/{advapi32.ts → advapi32.d.ts} +6 -21
  47. package/lib/windows/comctl32.d.ts +14 -0
  48. package/lib/windows/comdlg32.d.ts +4 -0
  49. package/lib/windows/{gdi32.ts → gdi32.d.ts} +161 -98
  50. package/lib/windows/{kernel32.ts → kernel32.d.ts} +138 -115
  51. package/lib/windows/{msimg32.ts → msimg32.d.ts} +11 -14
  52. package/lib/windows/ntdll.d.ts +5 -0
  53. package/lib/windows/ole32.d.ts +3 -0
  54. package/lib/windows/{psapi.ts → psapi.d.ts} +9 -18
  55. package/lib/windows/{shell32.ts → shell32.d.ts} +25 -24
  56. package/lib/windows/structs.d.ts +3141 -0
  57. package/lib/windows/{user32.ts → user32.d.ts} +196 -193
  58. package/lib/windows/version.d.ts +27 -0
  59. package/lib/windows/{winspool.ts → winspool.d.ts} +18 -32
  60. package/package.json +15 -8
  61. package/qwin-nowasm-x86.exe +0 -0
  62. package/qwin-nowasm.exe +0 -0
  63. package/qwin-x86.exe +0 -0
  64. package/qwin.exe +0 -0
  65. package/examples/PdfCanvas.js +0 -38
  66. package/examples/PdfCanvas.tsx +0 -54
  67. package/examples/execd/main.js +0 -23379
  68. package/examples/execd/tray.js +0 -192
  69. package/examples/execd/worker.js +0 -122
  70. package/examples/pdf_preview2.js +0 -398
  71. package/examples/pdf_preview2.ts +0 -353
  72. package/examples/pdf_viewer.js +0 -22515
  73. package/examples/pdf_viewer.tsx +0 -228
  74. package/examples/test_gallery.js +0 -18692
  75. package/examples/test_gallery.tsx +0 -291
  76. package/examples/test_react_lazy.js +0 -18560
  77. package/examples/test_react_lazy.tsx +0 -65
  78. package/examples/test_react_listbox.js +0 -18562
  79. package/examples/test_react_listbox.tsx +0 -73
  80. package/examples/test_root_window.js +0 -18532
  81. package/examples/test_root_window.tsx +0 -41
  82. package/examples/test_tab.js +0 -18563
  83. package/examples/test_tab.tsx +0 -77
  84. package/examples/tray_demo.js +0 -86
  85. package/examples/tray_demo.tsx +0 -89
  86. package/lib/abort.ts +0 -69
  87. package/lib/assert.ts +0 -7
  88. package/lib/bitmap.ts +0 -39
  89. package/lib/cache-utils.ts +0 -40
  90. package/lib/certs.ts +0 -258
  91. package/lib/dpi.ts +0 -31
  92. package/lib/dumpRects.ts +0 -34
  93. package/lib/exec-log.ts +0 -30
  94. package/lib/fetch.ts +0 -589
  95. package/lib/ffi/bind.ts +0 -540
  96. package/lib/ffi/ctype.ts +0 -371
  97. package/lib/ffi/index.ts +0 -6
  98. package/lib/ffi/struct.ts +0 -780
  99. package/lib/http-common.ts +0 -441
  100. package/lib/http-server.ts +0 -604
  101. package/lib/polyfill.ts +0 -171
  102. package/lib/react-qw/components/index.ts +0 -17
  103. package/lib/react-qw/index.ts +0 -96
  104. package/lib/react-qw/layout.ts +0 -111
  105. package/lib/react-qw/nmhdr.ts +0 -7
  106. package/lib/react-qw/props.ts +0 -41
  107. package/lib/react-qw/reconciler.ts +0 -379
  108. package/lib/stream.ts +0 -22
  109. package/lib/text-codec.ts +0 -171
  110. package/lib/text-measure.ts +0 -44
  111. package/lib/url.ts +0 -201
  112. package/lib/websocket.ts +0 -600
  113. package/lib/windows/comctl32.ts +0 -20
  114. package/lib/windows/comdlg32.ts +0 -10
  115. package/lib/windows/ntdll.ts +0 -11
  116. package/lib/windows/ole32.ts +0 -9
  117. package/lib/windows/structs.ts +0 -807
  118. package/lib/windows/version.ts +0 -35
  119. package/test/add.wasm +0 -0
  120. package/test/bench_ffi_callraw.js +0 -235
  121. package/test/bench_ffi_callraw.ts +0 -235
  122. package/test/bench_ffi_overhead.js +0 -139
  123. package/test/bench_ffi_overhead.ts +0 -141
  124. package/test/bench_sendmessage.js +0 -104
  125. package/test/bench_sendmessage.ts +0 -100
  126. package/test/bench_sendmessage_nonnull.js +0 -187
  127. package/test/bench_sendmessage_nonnull.ts +0 -188
  128. package/test/complex.wasm +0 -0
  129. package/test/complex_imports.wasm +0 -0
  130. package/test/debug/http_server_e2e.js +0 -31
  131. package/test/debug/minwasm.js +0 -52
  132. package/test/debug/mupdf_debug.js +0 -86
  133. package/test/debug/wasm.js +0 -122
  134. package/test/debug/wasm_export_debug.js +0 -54
  135. package/test/debug/wasm_names.js +0 -36
  136. package/test/example.pdf +0 -0
  137. package/test/global_imports.wasm +0 -0
  138. package/test/import_func.wasm +0 -0
  139. package/test/imports.wasm +0 -0
  140. package/test/mupdf-cdn.js +0 -40
  141. package/test/mupdf-cdn.ts +0 -43
  142. package/test/run.js +0 -111
  143. package/test/run.ts +0 -116
  144. package/test/sjlj.wasm +0 -0
  145. package/test/test_basic.js +0 -7
  146. package/test/test_basic.ts +0 -9
  147. package/test/test_basic_fetch.js +0 -63
  148. package/test/test_basic_fetch.ts +0 -59
  149. package/test/test_brotli.js +0 -48
  150. package/test/test_brotli.ts +0 -52
  151. package/test/test_fetch_cache.js +0 -169
  152. package/test/test_fetch_cache.ts +0 -187
  153. package/test/test_ffi.js +0 -157
  154. package/test/test_ffi.ts +0 -170
  155. package/test/test_ffi_abi.js +0 -290
  156. package/test/test_ffi_abi.ts +0 -274
  157. package/test/test_ffi_array.js +0 -116
  158. package/test/test_ffi_array.ts +0 -141
  159. package/test/test_ffi_bind.js +0 -656
  160. package/test/test_ffi_bind.ts +0 -670
  161. package/test/test_ffi_struct.js +0 -779
  162. package/test/test_ffi_struct.ts +0 -828
  163. package/test/test_ffi_struct_layout.js +0 -495
  164. package/test/test_ffi_struct_layout.ts +0 -543
  165. package/test/test_ffi_user32.js +0 -103
  166. package/test/test_ffi_user32.ts +0 -113
  167. package/test/test_ffi_windows.js +0 -172
  168. package/test/test_ffi_windows.ts +0 -187
  169. package/test/test_files.js +0 -102
  170. package/test/test_files.ts +0 -121
  171. package/test/test_frame_encoding.js +0 -128
  172. package/test/test_frame_encoding.ts +0 -132
  173. package/test/test_gdi_draw.js +0 -681
  174. package/test/test_gdi_draw.ts +0 -725
  175. package/test/test_helper.js +0 -91
  176. package/test/test_helper.ts +0 -88
  177. package/test/test_http_import.js +0 -96
  178. package/test/test_http_import.ts +0 -93
  179. package/test/test_http_server.js +0 -214
  180. package/test/test_http_server.ts +0 -211
  181. package/test/test_import_dir.js +0 -4
  182. package/test/test_import_dir.ts +0 -5
  183. package/test/test_ipv6.js +0 -134
  184. package/test/test_ipv6.ts +0 -130
  185. package/test/test_jsx.js +0 -1314
  186. package/test/test_meta_url.js +0 -3
  187. package/test/test_meta_url.ts +0 -4
  188. package/test/test_mupdf_render.js +0 -51
  189. package/test/test_mupdf_render.ts +0 -55
  190. package/test/test_mupdf_twice.js +0 -62
  191. package/test/test_mupdf_twice.ts +0 -65
  192. package/test/test_mupdf_wasm.js +0 -39
  193. package/test/test_mupdf_wasm.ts +0 -32
  194. package/test/test_net_event.js +0 -57
  195. package/test/test_net_event.ts +0 -56
  196. package/test/test_net_fetch.js +0 -384
  197. package/test/test_net_fetch.ts +0 -373
  198. package/test/test_net_websocket.js +0 -180
  199. package/test/test_net_websocket.ts +0 -164
  200. package/test/test_polyfill.js +0 -144
  201. package/test/test_polyfill.ts +0 -159
  202. package/test/test_printer.js +0 -137
  203. package/test/test_printer.ts +0 -148
  204. package/test/test_proc.js +0 -141
  205. package/test/test_proc.ts +0 -163
  206. package/test/test_react_complex.js +0 -18678
  207. package/test/test_react_counter.js +0 -18555
  208. package/test/test_react_flex.js +0 -18871
  209. package/test/test_react_hidden.js +0 -18560
  210. package/test/test_react_listview_link.js +0 -18590
  211. package/test/test_react_notify.js +0 -18797
  212. package/test/test_react_render.js +0 -18550
  213. package/test/test_registry.js +0 -74
  214. package/test/test_registry.ts +0 -95
  215. package/test/test_sysinfo.js +0 -122
  216. package/test/test_sysinfo.ts +0 -141
  217. package/test/test_url.js +0 -173
  218. package/test/test_url.ts +0 -183
  219. package/test/test_wasm_basic.js +0 -85
  220. package/test/test_wasm_basic.ts +0 -73
  221. package/test/test_wasm_import_global.js +0 -41
  222. package/test/test_wasm_import_global.ts +0 -45
  223. package/test/test_wasm_sjlj.js +0 -145
  224. package/test/test_wasm_sjlj.ts +0 -125
  225. package/test/test_wasm_types.js +0 -97
  226. package/test/test_wasm_types.ts +0 -124
  227. package/test/test_wolfssl_read.js +0 -203
  228. package/test/test_wolfssl_read.ts +0 -184
  229. package/test/test_worker.js +0 -21
  230. package/test/test_worker.ts +0 -24
  231. package/test/test_worker_concurrent.js +0 -45
  232. package/test/test_worker_concurrent.ts +0 -56
  233. package/test/test_worker_data_url.js +0 -46
  234. package/test/test_worker_data_url.ts +0 -51
  235. package/test/test_worker_fetch.js +0 -26
  236. package/test/test_worker_fetch.ts +0 -27
  237. package/test/test_worker_net.js +0 -69
  238. package/test/test_worker_net.ts +0 -66
  239. package/test/test_worker_wasm_concurrent.js +0 -44
  240. package/test/test_worker_wasm_concurrent.ts +0 -45
  241. package/test/types.wasm +0 -0
  242. package/test/worker_fetch.js +0 -13
  243. package/test/worker_fetch.ts +0 -13
  244. package/test/worker_helper.js +0 -1
  245. package/test/worker_helper.ts +0 -1
  246. package/test/worker_http.js +0 -18
  247. package/test/worker_http.ts +0 -20
  248. package/test/worker_import_lib.js +0 -12
  249. package/test/worker_import_lib.ts +0 -13
  250. package/test/worker_module.js +0 -16
  251. package/test/worker_module.ts +0 -17
  252. package/test/worker_wasm_concurrent.js +0 -22
  253. package/test/worker_wasm_concurrent.ts +0 -22
package/lib/dpi.ts DELETED
@@ -1,31 +0,0 @@
1
- // DPI 与系统字体 —— gui 模块下沉到 JS 层的两处复合逻辑(原 js_getScaleFactor /
2
- // js_createSystemDpiFont)。两者都只按「一次操作一次」调用,FFI 的固定溢价无感;
3
- // 留在 C 只会与 lib/windows 的绑定重复一层,遂收在此处单点实现。
4
- import { NULL, type Ptr } from './ffi/ctype.js'
5
- import { GetDC, ReleaseDC, SystemParametersInfo } from './windows/user32.js'
6
- import { GetDeviceCaps, CreateFontIndirect } from './windows/gdi32.js'
7
- import { NONCLIENTMETRICSW } from './windows/structs.js'
8
- import { DeviceCap, Spi } from '../constants'
9
-
10
- /** 屏幕缩放系数(96 DPI = 1):GetDC(NULL) → GetDeviceCaps(LOGPIXELSX) → ReleaseDC 成对释放 */
11
- export function getScaleFactor(): number {
12
- const hdc = GetDC(NULL)
13
- const dpi = hdc ? GetDeviceCaps(hdc, DeviceCap.LOGPIXELSX) : 96
14
- if (hdc) ReleaseDC(NULL, hdc)
15
- return dpi / 96
16
- }
17
-
18
- /** 系统消息字体(CreateFontIndirectW(NONCLIENTMETRICS.lfMessageFont))——界面控件的默认字体;
19
- * 出参位不注册 encoder:encode({cbSize}) 建缓冲带初值 → 传 .ptr → decode 读回。
20
- * cbSize/uiParam 必须与系统认知的结构尺寸一致:Vista+ 为 504(含尾部 iPaddedBorderWidth)、
21
- * XP/2003 为 500 —— 先按 504 试,失败退回 500(lfMessageFont 落在前 500 字节内,尾部留 0);
22
- * 两次都失败 → NULL,调用方按「无字体」处理(控件回落系统默认字体)。 */
23
- export function createSystemDpiFont(): Ptr<'HGDIOBJ'> | NULL {
24
- const ncm = NONCLIENTMETRICSW.encode({ cbSize: NONCLIENTMETRICSW.size })
25
- if (!SystemParametersInfo(Spi.GETNONCLIENTMETRICS, NONCLIENTMETRICSW.size, ncm, 0)) {
26
- const legacySize = NONCLIENTMETRICSW.size - 4
27
- NONCLIENTMETRICSW.encode({ cbSize: legacySize }, ncm)
28
- if (!SystemParametersInfo(Spi.GETNONCLIENTMETRICS, legacySize, ncm, 0)) return NULL
29
- }
30
- return CreateFontIndirect(NONCLIENTMETRICSW.decode(ncm).lfMessageFont)
31
- }
package/lib/dumpRects.ts DELETED
@@ -1,34 +0,0 @@
1
- import * as os from 'os'
2
- import * as gui from 'gui'
3
- import { GetWindowCmd } from '../constants'
4
- import { GetClientRect, GetWindow, GetWindowRect, GetWindowText, GetWindowTextLength } from '../lib/windows/user32.js'
5
- import { WCHAR } from '../lib/ffi/bind.js'
6
- import { RECT } from '../lib/windows/structs.js'
7
-
8
- export function startDumpRects(rootHwnd: gui.HWND, intervalMs: number = 3000): void {
9
- function dumpChild(parent: gui.HWND, indent: string): void {
10
- var ch = GetWindow(parent, GetWindowCmd.CHILD)
11
- var n = 0
12
- while (ch) {
13
- n++
14
- const wr = RECT.alloc()
15
- const cr = RECT.alloc()
16
- GetWindowRect(ch, wr.ptr)
17
- GetClientRect(ch, cr.ptr)
18
- const cap = GetWindowTextLength(ch) + 1
19
- const textBuf = WCHAR.alloc(cap)
20
- GetWindowText(ch, textBuf, cap)
21
- var txt = WCHAR.decode(textBuf).slice(0, 16)
22
- console.log(indent + '[' + n + '] hwnd=' + String(ch) + ' txt="' + txt + '" wr=' + JSON.stringify(RECT.decode(wr)) + ' cr=' + JSON.stringify(RECT.decode(cr)))
23
- dumpChild(ch, indent + ' ')
24
- ch = GetWindow(ch, GetWindowCmd.NEXT)
25
- }
26
- if (n === 0) console.log(indent + '(no children)')
27
- }
28
- function tick(): void {
29
- console.log('--- dump rects ---')
30
- dumpChild(rootHwnd, '')
31
- os.setTimeout(tick, intervalMs)
32
- }
33
- tick()
34
- }
package/lib/exec-log.ts DELETED
@@ -1,30 +0,0 @@
1
- export interface LogEntry {
2
- /** epoch 秒 */
3
- ts: number
4
- /** 展示用命令/请求描述 */
5
- cmd: string
6
- /** HTTP 状态码(401/404/500/504 等);异常终止用 -1 */
7
- status: number
8
- /** X-Exit-Code(子进程退出码);无则 null */
9
- exit: number | null
10
- /** 耗时 ms */
11
- durMs: number
12
- /** 附加说明(如超时/拒绝原因) */
13
- note?: string
14
- }
15
-
16
- /** 请求日志:固定容量环形缓冲,GUI ListBox 每行一条。 */
17
- export class RequestLog {
18
- private buf: LogEntry[] = []
19
-
20
- constructor(private readonly cap = 100) { }
21
-
22
- get size(): number { return this.buf.length }
23
-
24
- push(e: LogEntry): void {
25
- this.buf.push(e)
26
- if (this.buf.length > this.cap) this.buf.splice(0, this.buf.length - this.cap)
27
- }
28
-
29
- list(): LogEntry[] { return this.buf.slice() }
30
- }
package/lib/fetch.ts DELETED
@@ -1,589 +0,0 @@
1
- import * as sock from 'sock'
2
- import * as wolfssl from 'wolfssl'
3
- import * as brotli from 'brotli'
4
- import { parseIni, toIni } from '../lib/cache-utils.js'
5
- import {
6
- _concat,
7
- _toReadableStream,
8
- _readStream,
9
- _hasChunkedEnd,
10
- decodeChunked,
11
- HeadersImpl,
12
- RequestImpl,
13
- ResponseImpl,
14
- } from './http-common.js'
15
- import { FdEvent, ReturnCode, SniType, VerifyMode, WolfsslErrorCode } from '../constants'
16
-
17
- // ── HTTP Response Parser ──
18
-
19
- interface ParsedResponse {
20
- status: number
21
- statusText: string
22
- headers: HeadersImpl
23
- }
24
-
25
- function parseHeaders(data: string): ParsedResponse | null {
26
- const headerEnd = data.indexOf('\r\n\r\n')
27
- if (headerEnd < 0) return null
28
-
29
- const headerPart = data.slice(0, headerEnd)
30
- const lines = headerPart.split('\r\n')
31
- const statusLine = lines[0]!
32
- const match = statusLine.match(/^HTTP\/\d\.\d\s+(\d+)\s+(.*)$/)
33
- if (!match) throw new Error('Invalid HTTP response: ' + statusLine)
34
-
35
- const status = parseInt(match[1]!, 10)
36
- const statusText = match[2]!
37
-
38
- const headers = new HeadersImpl()
39
- for (let i = 1; i < lines.length; i++) {
40
- const colonIndex = lines[i]!.indexOf(':')
41
- if (colonIndex > 0) {
42
- const name = lines[i]!.slice(0, colonIndex).trim()
43
- const value = lines[i]!.slice(colonIndex + 1).trim()
44
- headers.append(name, value)
45
- }
46
- }
47
-
48
- return { status, statusText, headers }
49
- }
50
-
51
- /** True when the chunked body is complete. With a declared `Trailer`,
52
- * the terminating block is followed by 0..n header lines and a final
53
- * empty line (`\r\n0\r\n<trailer...>\r\n\r\n`) instead of the bare
54
- * `\r\n0\r\n\r\n` form. */
55
- function chunkedEndReached(data: Uint8Array, hasTrailer: boolean): boolean {
56
- if (!hasTrailer) return _hasChunkedEnd(data)
57
- const tail = '\r\n0\r\n'
58
- for (let i = data.length - 1; i >= 0; i--) {
59
- let j = 0
60
- while (j < tail.length && i + j < data.length && data[i + j] === tail.charCodeAt(j)) j++
61
- if (j === tail.length) {
62
- const after = i + tail.length
63
- if (data.length >= after + 2 &&
64
- data[data.length - 2] === 0x0D && data[data.length - 1] === 0x0A) return true
65
- }
66
- }
67
- return false
68
- }
69
-
70
- // ── State machine constants ──
71
-
72
- const ST_CONNECTING = 0
73
- const ST_HANDSHAKE = 1
74
- const ST_RECV_HEADERS = 3
75
- const ST_RECV_BODY = 4
76
- const ST_DONE = 5
77
-
78
- // ── Main fetch request ──
79
-
80
- async function fetchRequest(req: RequestImpl): Promise<ResponseImpl> {
81
- const parsedUrl = new URL(req.url)
82
- if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
83
- throw new TypeError('fetch: unsupported protocol "' + parsedUrl.protocol + '"')
84
- }
85
- const method = req.method
86
- const headers = new HeadersImpl(req.headers)
87
- const timeout = req.timeout || 30000
88
- const isHTTPS = parsedUrl.protocol === 'https:'
89
-
90
- const defaultPort = isHTTPS ? 443 : 80
91
- const hostname = parsedUrl.hostname.includes(':') ? `[${parsedUrl.hostname}]` : parsedUrl.hostname
92
- const hostHeader = parsedUrl.port && parsedUrl.port !== String(defaultPort)
93
- ? hostname + ':' + parsedUrl.port
94
- : hostname
95
- if (!headers.has('host')) headers.set('Host', hostHeader)
96
- if (!headers.has('user-agent')) headers.set('User-Agent', navigator.userAgent)
97
- if (!headers.has('connection')) headers.set('Connection', 'close')
98
- if (!headers.has('accept-encoding')) headers.set('Accept-Encoding', 'br')
99
- let bodyBytes: Uint8Array | null = null
100
- if (req.body) {
101
- bodyBytes = await _readStream(req.body)
102
- }
103
- if (bodyBytes && !headers.has('content-length')) headers.set('Content-Length', String(bodyBytes.length))
104
-
105
- return new Promise((resolve, reject) => {
106
- let request = method + ' ' + parsedUrl.pathname + (parsedUrl.search || '') + ' HTTP/1.1\r\n'
107
- headers.forEach((value: string, name: string) => {
108
- request += name + ': ' + value + '\r\n'
109
- })
110
- request += '\r\n'
111
-
112
- const requestBytes = new TextEncoder().encode(request)
113
- const httpRequest = _concat(
114
- bodyBytes ? [requestBytes, bodyBytes] : [requestBytes]
115
- ).buffer
116
-
117
- let s: sock.SockHandle | null = null
118
- let ssl: wolfssl.WOLFSSL | null = null
119
- let ctx: wolfssl.WOLFSSL_CTX | null = null
120
- let state = ST_CONNECTING
121
- let resolved = false
122
- let timerId: TimerId | undefined
123
- let stream: ReadableStream<Uint8Array> | null = null
124
- let _controller: ReadableStreamDefaultController<Uint8Array> | null = null
125
- let headerRaw: Uint8Array = new Uint8Array(0)
126
- let isChunked = false
127
- let hasTrailer = false
128
- let contentLength = 0
129
- let chunkedParts: Uint8Array[] = []
130
- let receivedBytes = 0
131
-
132
- const cleanupSocket = (): void => {
133
- state = ST_DONE
134
- if (timerId) { clearTimeout(timerId); timerId = undefined }
135
- if (ssl) { wolfssl.wolfSSL_free(ssl); ssl = null }
136
- if (ctx) { wolfssl.wolfSSL_CTX_free(ctx); ctx = null }
137
- if (s !== null && s >= 0) { sock.closesocket(s); s = null }
138
- }
139
-
140
- const cleanup = (): void => {
141
- if (timerId) { clearTimeout(timerId); timerId = undefined }
142
- }
143
-
144
- const armTimeout = (): void => {
145
- if (timerId) { clearTimeout(timerId); timerId = undefined }
146
- timerId = setTimeout(() => {
147
- timerId = undefined
148
- if (state === ST_DONE) return
149
- if (!resolved) {
150
- doReject(new Error('Request timeout'))
151
- return
152
- }
153
- if (state === ST_RECV_BODY && stream && _controller) {
154
- try { _controller.error(new Error('Body timeout')) } catch { /* already closed */ }
155
- stream = null
156
- cleanupSocket()
157
- }
158
- }, timeout)
159
- }
160
-
161
- const doResolve = (response: ResponseImpl): void => {
162
- if (resolved) return
163
- resolved = true
164
- if (state === ST_DONE) cleanup()
165
- else armTimeout()
166
- resolve(response)
167
- }
168
-
169
- const doReject = (error: Error): void => {
170
- if (!resolved) { resolved = true; cleanup(); cleanupSocket(); reject(error) }
171
- }
172
-
173
- armTimeout()
174
-
175
- s = sock.socket()
176
- if (s < 0) { doReject(new Error('Failed to create socket')); return }
177
- const fd = s
178
-
179
- sock.set_on_event(fd, (event: { lNetworkEvents: number; iErrorCode: number[] }) => {
180
- if (state === ST_DONE) return
181
-
182
- if (event.lNetworkEvents & FdEvent.FD_CONNECT) {
183
- const err = event.iErrorCode[0]
184
- if (err !== 0) { doReject(new Error('Connection failed: ' + err)); return }
185
-
186
- if (isHTTPS) {
187
- const tlsMethod = wolfssl.wolfTLSv1_2_client_method()
188
- ctx = wolfssl.wolfSSL_CTX_new(tlsMethod)
189
- if (!ctx) { doReject(new Error('SSL_CTX_new failed')); return }
190
- if (req.rejectUnauthorized === false) {
191
- wolfssl.wolfSSL_CTX_set_verify(ctx, VerifyMode.SSL_VERIFY_NONE)
192
- } else {
193
- wolfssl.wolfSSL_CTX_set_verify(ctx, VerifyMode.SSL_VERIFY_PEER)
194
- wolfssl.loadTrustedCerts(ctx)
195
- }
196
- ssl = wolfssl.wolfSSL_new(ctx)
197
- if (!ssl) { doReject(new Error('SSL_new failed')); return }
198
- wolfssl.wolfSSL_set_fd(ssl, sock.get_fd(fd))
199
- if (parsedUrl.hostname) {
200
- wolfssl.wolfSSL_UseSNI(ssl, SniType.WOLFSSL_SNI_HOST_NAME, parsedUrl.hostname)
201
- if (req.rejectUnauthorized !== false) {
202
- wolfssl.wolfSSL_check_domain_name(ssl, parsedUrl.hostname)
203
- }
204
- }
205
- state = ST_HANDSHAKE
206
- } else {
207
- sock.send(fd, httpRequest)
208
- state = ST_RECV_HEADERS
209
- }
210
- }
211
-
212
- if ((event.lNetworkEvents & FdEvent.FD_READ) || (event.lNetworkEvents & FdEvent.FD_WRITE)) {
213
- if (state === ST_HANDSHAKE) {
214
- if (!ssl) { doReject(new Error('TLS not initialized')); return }
215
- const ret = wolfssl.wolfSSL_connect(ssl)
216
- if (ret === ReturnCode.SSL_SUCCESS) {
217
- wolfssl.wolfSSL_write(ssl, httpRequest)
218
- state = ST_RECV_HEADERS
219
- } else {
220
- const err = wolfssl.wolfSSL_get_error(ssl, ret)
221
- if (err !== WolfsslErrorCode.WOLFSSL_ERROR_WANT_READ &&
222
- err !== WolfsslErrorCode.WOLFSSL_ERROR_WANT_WRITE) {
223
- doReject(new Error('TLS handshake failed: ' + err))
224
- }
225
- }
226
- }
227
- else if (state === ST_RECV_HEADERS) {
228
- while (true) {
229
- if (s !== null && s < 0 && !ssl) break
230
- let data: ArrayBuffer | null
231
- if (isHTTPS && ssl) {
232
- const buf = new ArrayBuffer(8192)
233
- const n = wolfssl.wolfSSL_read(ssl, buf)
234
- if (n > 0) {
235
- data = buf.slice(0, n)
236
- } else {
237
- const err = wolfssl.wolfSSL_get_error(ssl, n)
238
- if (err === WolfsslErrorCode.WOLFSSL_ERROR_WANT_READ ||
239
- err === WolfsslErrorCode.WOLFSSL_ERROR_WANT_WRITE) break
240
- if (n === 0 || err === WolfsslErrorCode.WOLFSSL_ERROR_ZERO_RETURN) break
241
- doReject(new Error('TLS read failed: ' + err))
242
- return
243
- }
244
- } else if (s !== null && s >= 0) {
245
- data = sock.recv(s, 8192)
246
- } else { break }
247
- if (!data || data.byteLength === 0) break
248
- const incoming = new Uint8Array(data)
249
- headerRaw = _concat([headerRaw, incoming])
250
-
251
- let headerEnd = -1
252
- for (let i = 0; i < headerRaw.length - 3; i++) {
253
- if (headerRaw[i] === 0x0D && headerRaw[i+1] === 0x0A && headerRaw[i+2] === 0x0D && headerRaw[i+3] === 0x0A) {
254
- headerEnd = i; break
255
- }
256
- }
257
- if (headerEnd >= 0) {
258
- const headerStr = new TextDecoder('utf-8').decode(headerRaw.subarray(0, headerEnd + 4))
259
- const parsed = parseHeaders(headerStr)
260
- if (!parsed) { doReject(new Error('Failed to parse HTTP headers')); return }
261
-
262
- const trailingBodyBytes = headerRaw.subarray(headerEnd + 4)
263
-
264
- isChunked = (parsed.headers.get('transfer-encoding') || '').toLowerCase().includes('chunked')
265
- hasTrailer = parsed.headers.get('trailer') != null
266
- contentLength = isChunked ? 0 : parseInt(
267
- parsed.headers.get('content-length') || '0', 10
268
- )
269
- _controller = null
270
- stream = new ReadableStream<Uint8Array>({
271
- start(ctrl: ReadableStreamDefaultController<Uint8Array>) { _controller = ctrl },
272
- cancel() { cleanup(); cleanupSocket() }
273
- })
274
- if (trailingBodyBytes.length > 0) {
275
- if (isChunked) {
276
- chunkedParts = [trailingBodyBytes]
277
- if (chunkedEndReached(trailingBodyBytes, hasTrailer)) {
278
- const decoded = decodeChunked(trailingBodyBytes)
279
- _controller!.enqueue(decoded)
280
- _controller!.close()
281
- state = ST_DONE
282
- cleanupSocket()
283
- }
284
- } else {
285
- receivedBytes += trailingBodyBytes.length
286
- _controller!.enqueue(trailingBodyBytes)
287
- if (contentLength > 0 && receivedBytes >= contentLength) {
288
- _controller!.close()
289
- state = ST_DONE
290
- cleanupSocket()
291
- }
292
- }
293
- }
294
-
295
- const response = new ResponseImpl(
296
- stream, { status: parsed.status, statusText: parsed.statusText, headers: parsed.headers }
297
- )
298
- if (state !== ST_DONE) {
299
- state = ST_RECV_BODY
300
- }
301
- doResolve(response)
302
- // Break out of header recv loop — any remaining data
303
- // in the socket will be handled by the ST_RECV_BODY path below
304
- break
305
- }
306
- }
307
- }
308
- else if (state === ST_RECV_BODY && stream) {
309
- while (true) {
310
- if (s !== null && s < 0 && !ssl) break
311
- let data: ArrayBuffer | null
312
- if (isHTTPS && ssl) {
313
- const buf = new ArrayBuffer(8192)
314
- const n = wolfssl.wolfSSL_read(ssl, buf)
315
- if (n > 0) {
316
- data = buf.slice(0, n)
317
- } else {
318
- const err = wolfssl.wolfSSL_get_error(ssl, n)
319
- if (err === WolfsslErrorCode.WOLFSSL_ERROR_WANT_READ ||
320
- err === WolfsslErrorCode.WOLFSSL_ERROR_WANT_WRITE) break
321
- if (n === 0 || err === WolfsslErrorCode.WOLFSSL_ERROR_ZERO_RETURN) break
322
- // Fatal TLS error mid-body: the response already
323
- // resolved, so fail the body stream instead of
324
- // silently waiting for an FD event that never comes.
325
- if (_controller) { try { _controller.error(new Error('TLS read failed: ' + err)) } catch { /* already closed */ } }
326
- stream = null
327
- cleanupSocket()
328
- return
329
- }
330
- } else if (s !== null && s >= 0) {
331
- data = sock.recv(s, 8192)
332
- } else { break }
333
- if (!data || data.byteLength === 0) break
334
- if (isChunked) {
335
- chunkedParts.push(new Uint8Array(data))
336
- const combined = _concat(chunkedParts)
337
- if (chunkedEndReached(combined, hasTrailer)) {
338
- const decoded = decodeChunked(combined)
339
- _controller!.enqueue(decoded)
340
- _controller!.close()
341
- stream = null
342
- state = ST_DONE
343
- cleanupSocket()
344
- break
345
- }
346
- } else {
347
- receivedBytes += data.byteLength
348
- _controller!.enqueue(new Uint8Array(data))
349
- if (contentLength > 0 && receivedBytes >= contentLength) {
350
- _controller!.close()
351
- state = ST_DONE
352
- cleanupSocket()
353
- }
354
- }
355
- }
356
- }
357
- }
358
-
359
- if (event.lNetworkEvents & FdEvent.FD_CLOSE) {
360
- if (state === ST_DONE) return
361
- if (state === ST_RECV_HEADERS) {
362
- doReject(new Error('Connection closed before response'))
363
- } else if (state === ST_RECV_BODY && stream) {
364
- let remainingParts: Uint8Array[] = []
365
- while (true) {
366
- if (s !== null && s < 0 && !ssl) break
367
- let data: ArrayBuffer | null
368
- if (isHTTPS && ssl) {
369
- const buf = new ArrayBuffer(8192)
370
- const n = wolfssl.wolfSSL_read(ssl, buf)
371
- // Best-effort drain: any non-data result (EOF, WANT_*,
372
- // late error) ends the drain — the stream closes below.
373
- if (n > 0) data = buf.slice(0, n)
374
- else break
375
- } else if (s !== null && s >= 0) {
376
- data = sock.recv(s, 8192)
377
- } else { break }
378
- if (!data || data.byteLength === 0) break
379
- remainingParts.push(new Uint8Array(data))
380
- }
381
- if (isChunked) {
382
- chunkedParts.push(...remainingParts)
383
- const decoded = decodeChunked(_concat(chunkedParts))
384
- _controller!.enqueue(decoded)
385
- } else if (remainingParts.length > 0) {
386
- _controller!.enqueue(_concat(remainingParts))
387
- }
388
- _controller!.close()
389
- stream = null
390
- state = ST_DONE
391
- cleanupSocket()
392
- } else {
393
- // ST_CONNECTING or ST_HANDSHAKE — socket died early,
394
- // reject so the Promise doesn't hang forever
395
- doReject(new Error('Connection closed'))
396
- }
397
- }
398
- })
399
-
400
- const ip = sock.resolve(parsedUrl.hostname)
401
- if (!ip) {
402
- doReject(new Error('DNS resolution failed for: ' + parsedUrl.hostname))
403
- return
404
- }
405
- sock.connect(s, ip, parseInt(parsedUrl.port, 10) || (isHTTPS ? 443 : 80))
406
- })
407
- }
408
-
409
- // ── Public fetch (with redirect handling and caching) ──
410
-
411
- function parseMaxAge(cc: string): number {
412
- const m = cc.match(/max-age=(\d+)/)
413
- return m ? parseInt(m[1]!, 10) : 0
414
- }
415
-
416
- interface CacheMeta {
417
- storedAt: number;
418
- maxAge: number;
419
- status: number;
420
- statusText: string;
421
- headers: Record<string, string>;
422
- etag?: string;
423
- lastModified?: string;
424
- }
425
-
426
- async function doFetch(url: string | Request, init: RequestInit, redirectCount: number): Promise<ResponseImpl> {
427
- let req = new RequestImpl(url, init)
428
- const redirectMode = req.redirect || 'follow'
429
- const maxRedirects = redirectMode === 'follow' ? (req.maxRedirects || 5) : 0
430
- const cache = typeof __httpCache__ !== 'undefined' ? __httpCache__ : null
431
- const method = req.method
432
- const currentUrl = req.url
433
-
434
- // ── Cache lookup (GET only) ──
435
- let cachedMeta: CacheMeta | null = null
436
-
437
- if (cache && method === 'GET') {
438
- const metaStr = cache.readMeta(currentUrl)
439
- if (metaStr) {
440
- const ini = parseIni(metaStr)
441
- cachedMeta = {
442
- storedAt: parseInt(ini.meta.storedAt || '0', 10),
443
- maxAge: parseInt(ini.meta.maxAge || '0', 10),
444
- status: parseInt(ini.meta.status || '200', 10),
445
- statusText: ini.meta.statusText || 'OK',
446
- headers: ini.headers,
447
- etag: ini.headers['etag'] || undefined,
448
- lastModified: ini.headers['last-modified'] || undefined,
449
- }
450
- const age = Math.floor(Date.now() / 1000) - cachedMeta.storedAt
451
- if (cachedMeta.maxAge > 0 && age < cachedMeta.maxAge) {
452
- const body = cache.readBody(currentUrl)
453
- if (body) {
454
- ini.meta.lastAccess = String(Math.floor(Date.now() / 1000))
455
- cache.writeMeta(currentUrl, toIni(ini.headers, ini.meta))
456
- const resp = new ResponseImpl(
457
- _toReadableStream(new Uint8Array(body)), { status: cachedMeta.status, statusText: cachedMeta.statusText, headers: new HeadersImpl(cachedMeta.headers || {}) }
458
- )
459
- resp._url = currentUrl
460
- return resp
461
- }
462
- }
463
- if (cachedMeta.etag) req.headers.set('If-None-Match', cachedMeta.etag)
464
- if (cachedMeta.lastModified) req.headers.set('If-Modified-Since', cachedMeta.lastModified)
465
- }
466
- }
467
-
468
- const response = await fetchRequest(req)
469
-
470
- response._url = currentUrl
471
-
472
- // ── Handle brotli Content-Encoding ──
473
- const contentEncoding = response.headers.get('content-encoding') || ''
474
- if (contentEncoding.includes('br')) {
475
- const compressedBody = await response.arrayBuffer()
476
- const decompressedBody = brotli.decompress(compressedBody)
477
- const newHeaders = new HeadersImpl()
478
- response.headers.forEach((v: string, k: string) => {
479
- if (k !== 'content-encoding') newHeaders.set(k, v)
480
- })
481
- newHeaders.set('content-length', String(decompressedBody.byteLength))
482
- const stream = _toReadableStream(new Uint8Array(decompressedBody))
483
- response._applyDecompressedBody(stream, decompressedBody, newHeaders)
484
- }
485
-
486
- // ── Handle 304 Not Modified ──
487
- if (response.status === 304 && cachedMeta && cache) {
488
- const body = cache.readBody(currentUrl)
489
- if (body) {
490
- cachedMeta.storedAt = Math.floor(Date.now() / 1000)
491
- response.headers.forEach((value: string, name: string) => {
492
- cachedMeta.headers[name] = value
493
- })
494
- const now = String(Math.floor(Date.now() / 1000))
495
- const iniMeta: Record<string, string> = {
496
- url: currentUrl,
497
- storedAt: String(cachedMeta.storedAt),
498
- maxAge: String(cachedMeta.maxAge),
499
- lastAccess: now,
500
- status: String(cachedMeta.status),
501
- statusText: cachedMeta.statusText,
502
- }
503
- cache.writeMeta(currentUrl, toIni(cachedMeta.headers, iniMeta))
504
- const resp = new ResponseImpl(
505
- _toReadableStream(new Uint8Array(body)), { status: cachedMeta.status, statusText: cachedMeta.statusText, headers: new HeadersImpl(cachedMeta.headers) }
506
- )
507
- resp._url = currentUrl
508
- return resp
509
- }
510
- }
511
-
512
- // ── Cache 200 GET responses ──
513
- if (cache && method === 'GET' && response.status === 200) {
514
- const body = await response.arrayBuffer()
515
- const cc = response.headers.get('cache-control') || ''
516
- const maxAge = parseMaxAge(cc)
517
- if (maxAge > 0) {
518
- const resHeaders = Object.fromEntries(response.headers)
519
- const now = String(Math.floor(Date.now() / 1000))
520
- const iniMeta: Record<string, string> = {
521
- url: currentUrl,
522
- storedAt: now,
523
- maxAge: String(maxAge),
524
- lastAccess: now,
525
- status: String(response.status),
526
- statusText: response.statusText,
527
- }
528
- cache.writeBodyOnly(currentUrl, body)
529
- cache.writeMeta(currentUrl, toIni(resHeaders, iniMeta))
530
- }
531
- const resp = new ResponseImpl(
532
- _toReadableStream(new Uint8Array(body)), { status: response.status, statusText: response.statusText, headers: response.headers }
533
- )
534
- resp._url = currentUrl
535
- return resp
536
- }
537
-
538
- // ── Redirect handling ──
539
- const isRedirect = response.status === 301 || response.status === 302 ||
540
- response.status === 303 || response.status === 307 || response.status === 308
541
-
542
- if (isRedirect) {
543
- if (redirectMode === 'error') {
544
- response.body.cancel('redirect')
545
- throw new TypeError('Redirect not allowed for: ' + currentUrl)
546
- }
547
- if (redirectMode === 'manual') {
548
- response._redirected = true
549
- return response
550
- }
551
- if (redirectCount >= maxRedirects) {
552
- response.body.cancel('redirect')
553
- throw new TypeError('Redirect count exceeded')
554
- }
555
-
556
- const location = response.headers.get('location')
557
- if (!location) throw new Error('Redirect response missing Location header')
558
-
559
- response.body.cancel('redirect')
560
- const newUrl = new URL(location, currentUrl).href
561
- const isGet = response.status === 301 || response.status === 302 || response.status === 303
562
- const newInit: RequestInit = {
563
- ...init,
564
- method: isGet ? 'GET' : req.method,
565
- headers: req.headers,
566
- body: isGet ? undefined : (req.body ?? undefined),
567
- }
568
- const result = await doFetch(newUrl, newInit, redirectCount + 1)
569
- result._redirected = true
570
- return result
571
- }
572
-
573
- if (redirectCount > 0) response._redirected = true
574
- return response
575
- }
576
-
577
- async function fetch(url: string | Request, init: RequestInit = {}): Promise<ResponseImpl> {
578
- return doFetch(url, init, 0)
579
- }
580
-
581
- // ── Global declarations ──
582
-
583
- declare global {
584
- var fetch: (url: string | Request, init?: RequestInit) => Promise<Response>;
585
- }
586
-
587
- // ── Register globals ──
588
-
589
- globalThis.fetch = fetch