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
@@ -1,604 +0,0 @@
1
- import './polyfill.js'
2
- import * as sock from 'sock'
3
- import { HeadersImpl, RequestImpl, ResponseImpl, _readStream } from './http-common.js'
4
- import { AbortController } from './abort.js'
5
- import { FdEvent } from '../constants'
6
-
7
- type SockHandle = import('sock').SockHandle
8
-
9
- // ── Public API ──
10
-
11
- export type ServerHandler = (req: Request) => Promise<Response>
12
-
13
- // ── Internal types ──
14
-
15
- interface NetEvent { lNetworkEvents: number; iErrorCode: number[] }
16
-
17
- interface ParsedHeader {
18
- method: string
19
- target: string
20
- version: string
21
- headers: Record<string, string>
22
- chunked: boolean
23
- contentLength: number
24
- }
25
-
26
- interface PendingReq {
27
- parsed: ParsedHeader
28
- bodyStart: number
29
- }
30
-
31
- interface ActiveReq {
32
- req: RequestImpl
33
- }
34
-
35
- interface Conn {
36
- sock: SockHandle
37
- parts: Uint8Array[]
38
- /** Cached total length of `parts` — lets the pending-body wait check
39
- * "have we received enough yet" in O(1) instead of re-concatenating
40
- * every received byte on each read event (O(n²) for large bodies). */
41
- partsLen: number
42
- pending: PendingReq | null
43
- current: ActiveReq | null
44
- outQueue: Uint8Array | null
45
- resumeRead: (() => void) | null
46
- closed: boolean
47
- closeAfterFlush: boolean
48
- keepAlive: boolean
49
- owner: HttpServer
50
- /** AbortController for the request currently served on this connection,
51
- * aborted when the client disconnects mid-response. */
52
- ac: AbortController | null
53
- }
54
-
55
- const STATUS_TEXT: Record<number, string> = {
56
- 100: 'Continue', 101: 'Switching Protocols',
57
- 200: 'OK', 201: 'Created', 202: 'Accepted', 204: 'No Content',
58
- 301: 'Moved Permanently', 302: 'Found', 304: 'Not Modified',
59
- 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found',
60
- 405: 'Method Not Allowed', 408: 'Request Timeout', 413: 'Payload Too Large',
61
- 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable',
62
- }
63
-
64
- // ── Byte helpers ──
65
-
66
- function concatU8(parts: Uint8Array[]): Uint8Array {
67
- let total = 0
68
- for (const p of parts) total += p.length
69
- const out = new Uint8Array(total)
70
- let off = 0
71
- for (const p of parts) { out.set(p, off); off += p.length }
72
- return out
73
- }
74
-
75
- function toArrayBuffer(u8: Uint8Array): ArrayBuffer {
76
- // 已经是完整 buffer(byteOffset=0、无尾部余量)时零复制返回——发送大
77
- // body 的主路径,避免每片 send 前白拷一份。视图场景仍 slice 出独立副本
78
- //(FFI 持有的 buffer 生命周期不受 view 约束)。
79
- if (u8.byteOffset === 0 && u8.byteLength === u8.buffer.byteLength) return u8.buffer as ArrayBuffer
80
- return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer
81
- }
82
-
83
- function findCrLfCrLf(buf: Uint8Array): number {
84
- const n = buf.length - 3
85
- for (let i = 0; i < n; i++) {
86
- if (buf[i] === 13 && buf[i + 1] === 10 && buf[i + 2] === 13 && buf[i + 3] === 10) return i
87
- }
88
- return -1
89
- }
90
-
91
- /** Flatten a `HeadersInit` into `[name, value]` pairs, preserving the
92
- * casing the caller provided (Node `addTrailers` semantics). */
93
- function headersInitToPairs(init: HeadersInit): [string, string][] {
94
- if (init instanceof HeadersImpl) {
95
- const out: [string, string][] = []
96
- for (const [name, value] of init) out.push([name, value])
97
- return out
98
- }
99
- if (Array.isArray(init)) return init.map(([name, value]) => [name, String(value)])
100
- const out: [string, string][] = []
101
- for (const name of Object.keys(init)) out.push([name, String((init as Record<string, string>)[name])])
102
- return out
103
- }
104
-
105
- /** Decode a chunked-transfer body starting at `start`. Returns the decoded
106
- * data and the offset just past the terminating CRLF, or null if the buffer
107
- * does not yet contain the complete chunked body. */
108
- function decodeChunked(buf: Uint8Array, start: number): { data: Uint8Array; end: number } | null {
109
- let pos = start
110
- const parts: Uint8Array[] = []
111
- const decoder = new TextDecoder('utf-8')
112
- while (pos < buf.length) {
113
- let eol = pos
114
- while (eol < buf.length && buf[eol] !== 13) eol++
115
- if (eol >= buf.length || eol + 1 >= buf.length || buf[eol + 1] !== 10) return null
116
- const sizeLine = decoder.decode(buf.subarray(pos, eol)).trim()
117
- const semi = sizeLine.indexOf(';')
118
- const hex = (semi >= 0 ? sizeLine.slice(0, semi) : sizeLine).trim()
119
- const size = parseInt(hex, 16)
120
- if (isNaN(size) || size < 0) return null
121
- pos = eol + 2
122
- if (size === 0) {
123
- while (true) {
124
- if (pos >= buf.length) return null
125
- if (buf[pos] === 13) {
126
- if (pos + 1 >= buf.length || buf[pos + 1] !== 10) return null
127
- pos += 2
128
- return { data: concatU8(parts), end: pos }
129
- }
130
- let t = pos
131
- while (t < buf.length && buf[t] !== 13) t++
132
- if (t >= buf.length || t + 1 >= buf.length || buf[t + 1] !== 10) return null
133
- pos = t + 2
134
- }
135
- }
136
- if (pos + size > buf.length) return null
137
- parts.push(buf.subarray(pos, pos + size))
138
- pos += size
139
- if (pos + 1 >= buf.length || buf[pos] !== 13 || buf[pos + 1] !== 10) return null
140
- pos += 2
141
- }
142
- return null
143
- }
144
-
145
- // ── Parsing ──
146
-
147
- function parseHeader(headerStr: string): ParsedHeader | null {
148
- const lines = headerStr.split('\r\n')
149
- const first = lines[0]
150
- if (!first) return null
151
- const sp1 = first.indexOf(' ')
152
- const sp2 = first.lastIndexOf(' ')
153
- if (sp1 <= 0 || sp2 <= sp1) return null
154
- const method = first.slice(0, sp1)
155
- const target = first.slice(sp1 + 1, sp2)
156
- const version = first.slice(sp2 + 1).trim()
157
-
158
- const headers: Record<string, string> = {}
159
- for (let i = 1; i < lines.length; i++) {
160
- const line = lines[i]
161
- if (!line) continue
162
- const ci = line.indexOf(':')
163
- if (ci <= 0) continue
164
- const name = line.slice(0, ci).trim().toLowerCase()
165
- const value = line.slice(ci + 1).trim()
166
- if (headers[name] !== undefined) headers[name] = headers[name] + ', ' + value
167
- else headers[name] = value
168
- }
169
-
170
- const te = (headers['transfer-encoding'] || '').toLowerCase()
171
- const chunked = te.indexOf('chunked') >= 0
172
- const contentLength = parseInt(headers['content-length'] || '0', 10) || 0
173
- return { method, target, version, headers, chunked, contentLength }
174
- }
175
-
176
- // ── Server ──
177
-
178
- export class HttpServer {
179
- readonly handler: ServerHandler
180
- private serverSock: SockHandle | null = null
181
- readonly conns = new Map<SockHandle, Conn>()
182
- private closed = false
183
-
184
- constructor(handler: ServerHandler) {
185
- this.handler = handler
186
- }
187
-
188
- /** Actual bound address, or null if not listening. */
189
- address(): { addr: string; port: number } | null {
190
- if (this.serverSock === null) return null
191
- return sock.getsockname(this.serverSock)
192
- }
193
-
194
- /** Bind and start listening. Returns 0 on success, -1 on failure. */
195
- listen(port: number, host?: string, backlog?: number): number {
196
- if (this.serverSock !== null) return -1
197
- const s = sock.socket()
198
- if (s < 0) return -1
199
- if (sock.bind(s, host ?? null, port) !== 0) { sock.closesocket(s); return -1 }
200
- if (sock.listen(s, backlog ?? 64) !== 0) { sock.closesocket(s); return -1 }
201
- this.serverSock = s
202
- sock.set_on_event(s, (event: NetEvent) => {
203
- if (event.lNetworkEvents & FdEvent.FD_ACCEPT) this.acceptLoop()
204
- })
205
- return 0
206
- }
207
-
208
- close(): void {
209
- if (this.closed) return
210
- this.closed = true
211
- for (const c of Array.from(this.conns.values())) closeConn(c)
212
- this.conns.clear()
213
- if (this.serverSock !== null) { sock.closesocket(this.serverSock); this.serverSock = null }
214
- }
215
-
216
- private acceptLoop(): void {
217
- if (this.closed || this.serverSock === null) return
218
- while (true) {
219
- const ac = sock.accept(this.serverSock)
220
- if (!ac) break
221
- const c: Conn = {
222
- sock: ac.handle,
223
- parts: [],
224
- partsLen: 0,
225
- pending: null,
226
- current: null,
227
- outQueue: null,
228
- resumeRead: null,
229
- closed: false,
230
- closeAfterFlush: false,
231
- keepAlive: true,
232
- owner: this,
233
- ac: null,
234
- }
235
- this.conns.set(c.sock, c)
236
- sock.set_on_event(c.sock, (event: NetEvent) => this.onConnEvent(c, event))
237
- }
238
- }
239
-
240
- private onConnEvent(c: Conn, event: NetEvent): void {
241
- if (c.closed) return
242
- if (event.lNetworkEvents & FdEvent.FD_READ) onRead(c)
243
- if (event.lNetworkEvents & FdEvent.FD_WRITE) flushQueue(c)
244
- if (event.lNetworkEvents & FdEvent.FD_CLOSE) {
245
- onRead(c)
246
- abortConn(c)
247
- closeConn(c)
248
- }
249
- }
250
- }
251
-
252
- export function createServer(handler: ServerHandler): HttpServer {
253
- return new HttpServer(handler)
254
- }
255
-
256
- // ── Connection I/O ──
257
-
258
- function onRead(c: Conn): void {
259
- if (c.closed) return
260
- while (true) {
261
- const d = sock.recv(c.sock, 8192)
262
- if (!d) break
263
- const u8 = new Uint8Array(d)
264
- c.parts.push(u8)
265
- c.partsLen += u8.length
266
- }
267
- if (!c.closed) tryProcess(c)
268
- }
269
-
270
- /** Send `data`, or queue it when the kernel send buffer is full.
271
- * Returns true when the data was fully handed to the kernel, or false
272
- * when it has been buffered (or partially handed over) and the caller
273
- * should back off until `flushQueue` drains and calls `c.resumeRead`. */
274
- function queueSend(c: Conn, data: Uint8Array): boolean {
275
- if (c.closed || data.length === 0) return true
276
- if (c.outQueue && c.outQueue.length > 0) {
277
- c.outQueue = concatU8([c.outQueue, data])
278
- return false
279
- }
280
- const ret = sock.send(c.sock, toArrayBuffer(data))
281
- if (ret < 0) {
282
- abortConn(c)
283
- closeConn(c)
284
- return true
285
- }
286
- if (ret < data.length) {
287
- c.outQueue = data.subarray(ret)
288
- return false
289
- }
290
- return true
291
- }
292
-
293
- function flushQueue(c: Conn): void {
294
- if (c.closed) return
295
- if (!c.outQueue || c.outQueue.length === 0) {
296
- if (c.closeAfterFlush) closeConn(c)
297
- else resumeReader(c)
298
- return
299
- }
300
- const q = c.outQueue
301
- const ret = sock.send(c.sock, toArrayBuffer(q))
302
- if (ret < 0) {
303
- abortConn(c)
304
- closeConn(c)
305
- return
306
- }
307
- if (ret < q.length) {
308
- c.outQueue = q.subarray(ret)
309
- return
310
- }
311
- c.outQueue = null
312
- if (c.closeAfterFlush) closeConn(c)
313
- else resumeReader(c)
314
- }
315
-
316
- function closeConn(c: Conn): void {
317
- if (c.closed) return
318
- c.closed = true
319
- resumeReader(c)
320
- if (c.sock >= 0) sock.closesocket(c.sock)
321
- c.owner.conns.delete(c.sock)
322
- }
323
-
324
- /** Abort the in-flight request (notifying its handler via `req.signal`).
325
- * Safe to call when idle or already aborted: the controller is cleared so
326
- * a fresh request on this connection gets a fresh signal. */
327
- function abortConn(c: Conn): void {
328
- const ac = c.ac
329
- c.ac = null
330
- if (ac) ac.abort()
331
- }
332
-
333
- /** Resolve the pending back-pressure awaiter, if any. Called whenever the
334
- * send queue drains or the connection closes. */
335
- function resumeReader(c: Conn): void {
336
- const r = c.resumeRead
337
- c.resumeRead = null
338
- if (r) r()
339
- }
340
-
341
- /** Close the connection once a response is fully flushed, when the
342
- * handler or client asked for `Connection: close`. */
343
- function finishResponse(c: Conn): void {
344
- if (c.closeAfterFlush && !c.outQueue) closeConn(c)
345
- }
346
-
347
- /** Frame `data` as a single chunked-transfer block: `<hex size>\r\n<data>\r\n`. */
348
- function encodeChunkFrame(data: Uint8Array, enc: TextEncoder): Uint8Array {
349
- return concatU8([enc.encode(data.byteLength.toString(16) + '\r\n'), data, enc.encode('\r\n')])
350
- }
351
-
352
- /** Read `body` one chunk at a time and send it out. When the kernel send
353
- * buffer fills up (`queueSend` returns false) we pause on `c.resumeRead`,
354
- * which `flushQueue` resumes once the socket drains — backpressure. */
355
- async function writeStreamingBody(c: Conn, body: ReadableStream<Uint8Array> | null, chunked: boolean, trailer: Promise<HeadersInit> | undefined): Promise<void> {
356
- if (c.closed || body == null) return
357
- const reader = body.getReader()
358
- const enc = new TextEncoder()
359
- try {
360
- while (!c.closed) {
361
- const { done, value } = await reader.read()
362
- if (done) break
363
- if (value && value.byteLength > 0) {
364
- // 切成 ≤64KB 再入 queueSend:单发 200MB 级大 chunk 时
365
- // toArrayBuffer 会全量 slice 复制,partial drain 又每次复制
366
- // 剩余(O(n²)),XP 32 位 1GB 上直接把进程水位顶穿导致断连;
367
- // 小片让每次复制/发送恒为常数大小,backpressure 语义不变。
368
- const MAX_SEND = 65536
369
- for (let off = 0; off < value.byteLength; off += MAX_SEND) {
370
- const piece = value.byteLength <= MAX_SEND ? value : value.subarray(off, Math.min(off + MAX_SEND, value.byteLength))
371
- const frame = chunked ? encodeChunkFrame(piece, enc) : piece
372
- if (!queueSend(c, frame)) {
373
- await new Promise<void>((resolve) => { c.resumeRead = resolve })
374
- }
375
- if (c.closed) break
376
- }
377
- }
378
- }
379
- if (c.closed) {
380
- // 客户端断开(FD_CLOSE / send 失败):取消响应体,让 producer 感知
381
- //(execd 借此 kill 子进程),而不是干等它自然结束。
382
- try { await reader.cancel() } catch (e) {
383
- console.error('[http-server] body cancel error:', e)
384
- }
385
- return
386
- }
387
- if (chunked) {
388
- let terminator = '0\r\n\r\n'
389
- if (trailer) {
390
- try {
391
- const pairs = headersInitToPairs(await trailer)
392
- if (pairs.length > 0) {
393
- let s = '0\r\n'
394
- for (const [name, value] of pairs) s += name + ': ' + value + '\r\n'
395
- s += '\r\n'
396
- terminator = s
397
- }
398
- } catch (e) {
399
- console.error('[http-server] trailers promise rejected:', e)
400
- }
401
- }
402
- queueSend(c, enc.encode(terminator))
403
- }
404
- finishResponse(c)
405
- } finally {
406
- reader.releaseLock()
407
- }
408
- }
409
-
410
- // ── Request state machine ──
411
-
412
- function whole(c: Conn): Uint8Array {
413
- return concatU8(c.parts)
414
- }
415
-
416
- function trimParts(c: Conn, buf: Uint8Array, consumed: number): void {
417
- if (consumed >= buf.length) {
418
- c.parts = []
419
- c.partsLen = 0
420
- } else {
421
- c.parts = [buf.subarray(consumed)]
422
- c.partsLen = buf.length - consumed
423
- }
424
- }
425
-
426
- function tryProcess(c: Conn): void {
427
- if (c.closed || c.current) return
428
-
429
- if (c.pending) {
430
- const bodyStart = c.pending.bodyStart
431
- const parsed = c.pending.parsed
432
- if (!parsed.chunked) {
433
- // O(1) length check while the body is still arriving — only
434
- // concatenate once, when the last byte is in.
435
- const end = bodyStart + parsed.contentLength
436
- if (end > c.partsLen) return
437
- const buf = whole(c)
438
- c.pending = null
439
- dispatchRequest(c, parsed, buf.subarray(bodyStart, end), end, buf)
440
- return
441
- }
442
- // chunked: no total length up front, so a decode attempt still needs
443
- // the concatenated view (re-tried per read event as before).
444
- const buf = whole(c)
445
- const dec = decodeChunked(buf, bodyStart)
446
- if (!dec) return
447
- c.pending = null
448
- dispatchRequest(c, parsed, dec.data, dec.end, buf)
449
- return
450
- }
451
-
452
- // Header phase: bodies are checked against `partsLen` above, so a full
453
- // concatenation is only needed here — once per request, not per read.
454
- const buf = whole(c)
455
- const headerEnd = findCrLfCrLf(buf)
456
- if (headerEnd < 0) return
457
- const absEnd = headerEnd + 4
458
- const headerStr = new TextDecoder('utf-8').decode(buf.subarray(0, absEnd))
459
- const parsed = parseHeader(headerStr)
460
- if (!parsed) { closeConn(c); return }
461
-
462
- const bodyStart = absEnd
463
- if (parsed.chunked) {
464
- const dec = decodeChunked(buf, bodyStart)
465
- if (!dec) {
466
- c.pending = { parsed, bodyStart }
467
- return
468
- }
469
- dispatchRequest(c, parsed, dec.data, dec.end, buf)
470
- } else if (parsed.contentLength > 0) {
471
- if (bodyStart + parsed.contentLength > buf.length) {
472
- c.pending = { parsed, bodyStart }
473
- return
474
- }
475
- dispatchRequest(c, parsed, buf.subarray(bodyStart, bodyStart + parsed.contentLength), bodyStart + parsed.contentLength, buf)
476
- } else {
477
- dispatchRequest(c, parsed, new Uint8Array(0), bodyStart, buf)
478
- }
479
- }
480
-
481
- function dispatchRequest(c: Conn, parsed: ParsedHeader, body: Uint8Array, end: number, wholeBuf?: Uint8Array): void {
482
- const connHeader = (parsed.headers['connection'] || '').toLowerCase()
483
- if (parsed.version === 'HTTP/1.1') c.keepAlive = connHeader.indexOf('close') < 0
484
- else c.keepAlive = connHeader.indexOf('keep-alive') >= 0
485
-
486
- let host = parsed.headers['host']
487
- if (!host) {
488
- const a = c.owner.address()
489
- const addrStr = a ? (a.addr.includes(':') ? `[${a.addr}]` : a.addr) : 'localhost'
490
- host = a ? addrStr + ':' + a.port : 'localhost'
491
- }
492
-
493
- const isAbsolute = parsed.target.startsWith('http://') || parsed.target.startsWith('https://')
494
- const url = isAbsolute ? parsed.target : 'http://' + host + parsed.target
495
-
496
- const ac = new AbortController()
497
- const req = new RequestImpl(url, {
498
- method: parsed.method,
499
- headers: new HeadersImpl(parsed.headers),
500
- body: body.length > 0 ? body : undefined,
501
- signal: ac.signal,
502
- })
503
-
504
- c.ac = ac
505
- c.current = { req }
506
- // `wholeBuf` is the concatenation the caller already produced — reuse it
507
- // instead of concatenating the very same parts a second time.
508
- trimParts(c, wholeBuf ?? whole(c), end)
509
- void handleRequest(c)
510
- }
511
-
512
- async function handleRequest(c: Conn): Promise<void> {
513
- const req = c.current?.req
514
- if (!req) { c.current = null; return }
515
-
516
- let res: Response
517
- try {
518
- const result = await c.owner.handler(req)
519
- if (result instanceof ResponseImpl) {
520
- res = result
521
- } else {
522
- console.error('[http-server] handler must return a Response')
523
- res = new ResponseImpl('Internal Server Error', { status: 500 })
524
- }
525
- } catch (e) {
526
- console.error('[http-server] handler error:', e)
527
- res = new ResponseImpl('Internal Server Error', { status: 500 })
528
- }
529
-
530
- if (c.closed) return
531
- try {
532
- await sendResponse(c, req.method, res)
533
- } catch (e) {
534
- console.error('[http-server] response send error:', e)
535
- if (!c.closed) closeConn(c)
536
- return
537
- }
538
- if (c.closed) return
539
-
540
- c.current = null
541
- if (!c.closeAfterFlush) tryProcess(c)
542
- }
543
-
544
- function buildHead(status: number, statusText: string, headers: HeadersImpl): Uint8Array {
545
- let head = 'HTTP/1.1 ' + status + ' ' + statusText + '\r\n'
546
- for (const [name, value] of headers) head += name + ': ' + value + '\r\n'
547
- head += '\r\n'
548
- return new TextEncoder().encode(head)
549
- }
550
-
551
- async function sendResponse(c: Conn, method: string, res: Response): Promise<void> {
552
- const status = res.status
553
- const statusText = res.statusText || STATUS_TEXT[status] || 'OK'
554
- const headers = new HeadersImpl(res.headers)
555
-
556
- const headOnly = method === 'HEAD'
557
- const bodyless = (status >= 100 && status < 200) || status === 204 || status === 304
558
-
559
- if (!headers.has('connection')) headers.set('Connection', c.keepAlive ? 'keep-alive' : 'close')
560
- const connHdr = (headers.get('connection') || '').toLowerCase()
561
- if (!c.keepAlive || connHdr === 'close') c.closeAfterFlush = true
562
-
563
- if (bodyless) {
564
- headers.delete('Transfer-Encoding')
565
- headers.delete('Content-Length')
566
- headers.delete('Trailer')
567
- queueSend(c, buildHead(status, statusText, headers))
568
- finishResponse(c)
569
- return
570
- }
571
-
572
- if (headOnly) {
573
- headers.delete('Transfer-Encoding')
574
- headers.delete('Trailer')
575
- if (!headers.has('content-length')) {
576
- try {
577
- const full = await _readStream(res.body)
578
- headers.set('Content-Length', String(full.length))
579
- } catch { /* leave length unspecified */ }
580
- }
581
- if (c.closed) return
582
- queueSend(c, buildHead(status, statusText, headers))
583
- finishResponse(c)
584
- return
585
- }
586
-
587
- // Body is streamed out chunk by chunk. A declared Content-Length sends
588
- // the bytes as-is; otherwise we use chunked framing.
589
- const chunked = (headers.get('transfer-encoding') || '').toLowerCase() === 'chunked' ||
590
- !headers.has('content-length')
591
- // Trailing headers are only emitted when the body is chunk-framed and a
592
- // `Trailer` declaration header is present (Node addTrailers semantics).
593
- const trailerDeclared = (headers.get('trailer') || '').trim().length > 0
594
- if (chunked) {
595
- headers.delete('Content-Length')
596
- if (!headers.has('transfer-encoding')) headers.set('Transfer-Encoding', 'chunked')
597
- if (!trailerDeclared) headers.delete('Trailer')
598
- } else {
599
- headers.delete('Transfer-Encoding')
600
- headers.delete('Trailer')
601
- }
602
- queueSend(c, buildHead(status, statusText, headers))
603
- await writeStreamingBody(c, res.body, chunked, (chunked && trailerDeclared) ? res.trailers : undefined)
604
- }