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.
- package/README.en.md +3 -0
- package/README.md +2 -0
- package/lib/abort.d.ts +21 -0
- package/lib/assert.d.ts +2 -0
- package/lib/bitmap.d.ts +7 -0
- package/lib/cache-utils.d.ts +6 -0
- package/lib/certs.d.ts +1 -0
- package/lib/dpi.d.ts +9 -0
- package/lib/dumpRects.d.ts +2 -0
- package/lib/exec-log.d.ts +23 -0
- package/lib/fetch.d.ts +4 -0
- package/lib/ffi/bind.d.ts +70 -0
- package/lib/ffi/ctype.d.ts +117 -0
- package/lib/ffi/index.d.ts +2 -0
- package/lib/ffi/struct.d.ts +174 -0
- package/lib/http-common.d.ts +143 -0
- package/lib/http-server.d.ts +59 -0
- package/lib/polyfill.d.ts +22 -0
- package/lib/react-qw/components/Button.d.ts +9 -0
- package/lib/react-qw/components/CheckBox.d.ts +11 -0
- package/lib/react-qw/components/ComboBox.d.ts +11 -0
- package/lib/react-qw/components/DateTimePicker.d.ts +15 -0
- package/lib/react-qw/components/Input.d.ts +15 -0
- package/lib/react-qw/components/Link.d.ts +12 -0
- package/lib/react-qw/components/ListBox.d.ts +13 -0
- package/lib/react-qw/components/ListView.d.ts +37 -0
- package/lib/react-qw/components/PathPicker.d.ts +23 -0
- package/lib/react-qw/components/ProgressBar.d.ts +9 -0
- package/lib/react-qw/components/RadioButton.d.ts +11 -0
- package/lib/react-qw/components/ScrollView.d.ts +9 -0
- package/lib/react-qw/components/Slider.d.ts +12 -0
- package/lib/react-qw/components/Tab.d.ts +14 -0
- package/lib/react-qw/components/Tooltip.d.ts +7 -0
- package/lib/react-qw/components/TreeView.d.ts +18 -0
- package/lib/react-qw/components/index.d.ts +16 -0
- package/lib/react-qw/index.d.ts +24 -0
- package/lib/react-qw/layout.d.ts +29 -0
- package/lib/react-qw/nmhdr.d.ts +1 -0
- package/lib/react-qw/props.d.ts +2 -0
- package/lib/react-qw/reconciler.d.ts +21 -0
- package/lib/stream.d.ts +20 -0
- package/lib/text-codec.d.ts +39 -0
- package/lib/text-measure.d.ts +14 -0
- package/lib/url.d.ts +54 -0
- package/lib/websocket.d.ts +73 -0
- package/lib/windows/{advapi32.ts → advapi32.d.ts} +6 -21
- package/lib/windows/comctl32.d.ts +14 -0
- package/lib/windows/comdlg32.d.ts +4 -0
- package/lib/windows/{gdi32.ts → gdi32.d.ts} +161 -98
- package/lib/windows/{kernel32.ts → kernel32.d.ts} +138 -115
- package/lib/windows/{msimg32.ts → msimg32.d.ts} +11 -14
- package/lib/windows/ntdll.d.ts +5 -0
- package/lib/windows/ole32.d.ts +3 -0
- package/lib/windows/{psapi.ts → psapi.d.ts} +9 -18
- package/lib/windows/{shell32.ts → shell32.d.ts} +25 -24
- package/lib/windows/structs.d.ts +3141 -0
- package/lib/windows/{user32.ts → user32.d.ts} +196 -193
- package/lib/windows/version.d.ts +27 -0
- package/lib/windows/{winspool.ts → winspool.d.ts} +18 -32
- package/package.json +15 -8
- package/qwin-nowasm-x86.exe +0 -0
- package/qwin-nowasm.exe +0 -0
- package/qwin-x86.exe +0 -0
- package/qwin.exe +0 -0
- package/examples/PdfCanvas.js +0 -38
- package/examples/PdfCanvas.tsx +0 -54
- package/examples/execd/main.js +0 -23379
- package/examples/execd/tray.js +0 -192
- package/examples/execd/worker.js +0 -122
- package/examples/pdf_preview2.js +0 -398
- package/examples/pdf_preview2.ts +0 -353
- package/examples/pdf_viewer.js +0 -22515
- package/examples/pdf_viewer.tsx +0 -228
- package/examples/test_gallery.js +0 -18692
- package/examples/test_gallery.tsx +0 -291
- package/examples/test_react_lazy.js +0 -18560
- package/examples/test_react_lazy.tsx +0 -65
- package/examples/test_react_listbox.js +0 -18562
- package/examples/test_react_listbox.tsx +0 -73
- package/examples/test_root_window.js +0 -18532
- package/examples/test_root_window.tsx +0 -41
- package/examples/test_tab.js +0 -18563
- package/examples/test_tab.tsx +0 -77
- package/examples/tray_demo.js +0 -86
- package/examples/tray_demo.tsx +0 -89
- package/lib/abort.ts +0 -69
- package/lib/assert.ts +0 -7
- package/lib/bitmap.ts +0 -39
- package/lib/cache-utils.ts +0 -40
- package/lib/certs.ts +0 -258
- package/lib/dpi.ts +0 -31
- package/lib/dumpRects.ts +0 -34
- package/lib/exec-log.ts +0 -30
- package/lib/fetch.ts +0 -589
- package/lib/ffi/bind.ts +0 -540
- package/lib/ffi/ctype.ts +0 -371
- package/lib/ffi/index.ts +0 -6
- package/lib/ffi/struct.ts +0 -780
- package/lib/http-common.ts +0 -441
- package/lib/http-server.ts +0 -604
- package/lib/polyfill.ts +0 -171
- package/lib/react-qw/components/index.ts +0 -17
- package/lib/react-qw/index.ts +0 -96
- package/lib/react-qw/layout.ts +0 -111
- package/lib/react-qw/nmhdr.ts +0 -7
- package/lib/react-qw/props.ts +0 -41
- package/lib/react-qw/reconciler.ts +0 -379
- package/lib/stream.ts +0 -22
- package/lib/text-codec.ts +0 -171
- package/lib/text-measure.ts +0 -44
- package/lib/url.ts +0 -201
- package/lib/websocket.ts +0 -600
- package/lib/windows/comctl32.ts +0 -20
- package/lib/windows/comdlg32.ts +0 -10
- package/lib/windows/ntdll.ts +0 -11
- package/lib/windows/ole32.ts +0 -9
- package/lib/windows/structs.ts +0 -807
- package/lib/windows/version.ts +0 -35
- package/test/add.wasm +0 -0
- package/test/bench_ffi_callraw.js +0 -235
- package/test/bench_ffi_callraw.ts +0 -235
- package/test/bench_ffi_overhead.js +0 -139
- package/test/bench_ffi_overhead.ts +0 -141
- package/test/bench_sendmessage.js +0 -104
- package/test/bench_sendmessage.ts +0 -100
- package/test/bench_sendmessage_nonnull.js +0 -187
- package/test/bench_sendmessage_nonnull.ts +0 -188
- package/test/complex.wasm +0 -0
- package/test/complex_imports.wasm +0 -0
- package/test/debug/http_server_e2e.js +0 -31
- package/test/debug/minwasm.js +0 -52
- package/test/debug/mupdf_debug.js +0 -86
- package/test/debug/wasm.js +0 -122
- package/test/debug/wasm_export_debug.js +0 -54
- package/test/debug/wasm_names.js +0 -36
- package/test/example.pdf +0 -0
- package/test/global_imports.wasm +0 -0
- package/test/import_func.wasm +0 -0
- package/test/imports.wasm +0 -0
- package/test/mupdf-cdn.js +0 -40
- package/test/mupdf-cdn.ts +0 -43
- package/test/run.js +0 -111
- package/test/run.ts +0 -116
- package/test/sjlj.wasm +0 -0
- package/test/test_basic.js +0 -7
- package/test/test_basic.ts +0 -9
- package/test/test_basic_fetch.js +0 -63
- package/test/test_basic_fetch.ts +0 -59
- package/test/test_brotli.js +0 -48
- package/test/test_brotli.ts +0 -52
- package/test/test_fetch_cache.js +0 -169
- package/test/test_fetch_cache.ts +0 -187
- package/test/test_ffi.js +0 -157
- package/test/test_ffi.ts +0 -170
- package/test/test_ffi_abi.js +0 -290
- package/test/test_ffi_abi.ts +0 -274
- package/test/test_ffi_array.js +0 -116
- package/test/test_ffi_array.ts +0 -141
- package/test/test_ffi_bind.js +0 -656
- package/test/test_ffi_bind.ts +0 -670
- package/test/test_ffi_struct.js +0 -779
- package/test/test_ffi_struct.ts +0 -828
- package/test/test_ffi_struct_layout.js +0 -495
- package/test/test_ffi_struct_layout.ts +0 -543
- package/test/test_ffi_user32.js +0 -103
- package/test/test_ffi_user32.ts +0 -113
- package/test/test_ffi_windows.js +0 -172
- package/test/test_ffi_windows.ts +0 -187
- package/test/test_files.js +0 -102
- package/test/test_files.ts +0 -121
- package/test/test_frame_encoding.js +0 -128
- package/test/test_frame_encoding.ts +0 -132
- package/test/test_gdi_draw.js +0 -681
- package/test/test_gdi_draw.ts +0 -725
- package/test/test_helper.js +0 -91
- package/test/test_helper.ts +0 -88
- package/test/test_http_import.js +0 -96
- package/test/test_http_import.ts +0 -93
- package/test/test_http_server.js +0 -214
- package/test/test_http_server.ts +0 -211
- package/test/test_import_dir.js +0 -4
- package/test/test_import_dir.ts +0 -5
- package/test/test_ipv6.js +0 -134
- package/test/test_ipv6.ts +0 -130
- package/test/test_jsx.js +0 -1314
- package/test/test_meta_url.js +0 -3
- package/test/test_meta_url.ts +0 -4
- package/test/test_mupdf_render.js +0 -51
- package/test/test_mupdf_render.ts +0 -55
- package/test/test_mupdf_twice.js +0 -62
- package/test/test_mupdf_twice.ts +0 -65
- package/test/test_mupdf_wasm.js +0 -39
- package/test/test_mupdf_wasm.ts +0 -32
- package/test/test_net_event.js +0 -57
- package/test/test_net_event.ts +0 -56
- package/test/test_net_fetch.js +0 -384
- package/test/test_net_fetch.ts +0 -373
- package/test/test_net_websocket.js +0 -180
- package/test/test_net_websocket.ts +0 -164
- package/test/test_polyfill.js +0 -144
- package/test/test_polyfill.ts +0 -159
- package/test/test_printer.js +0 -137
- package/test/test_printer.ts +0 -148
- package/test/test_proc.js +0 -141
- package/test/test_proc.ts +0 -163
- package/test/test_react_complex.js +0 -18678
- package/test/test_react_counter.js +0 -18555
- package/test/test_react_flex.js +0 -18871
- package/test/test_react_hidden.js +0 -18560
- package/test/test_react_listview_link.js +0 -18590
- package/test/test_react_notify.js +0 -18797
- package/test/test_react_render.js +0 -18550
- package/test/test_registry.js +0 -74
- package/test/test_registry.ts +0 -95
- package/test/test_sysinfo.js +0 -122
- package/test/test_sysinfo.ts +0 -141
- package/test/test_url.js +0 -173
- package/test/test_url.ts +0 -183
- package/test/test_wasm_basic.js +0 -85
- package/test/test_wasm_basic.ts +0 -73
- package/test/test_wasm_import_global.js +0 -41
- package/test/test_wasm_import_global.ts +0 -45
- package/test/test_wasm_sjlj.js +0 -145
- package/test/test_wasm_sjlj.ts +0 -125
- package/test/test_wasm_types.js +0 -97
- package/test/test_wasm_types.ts +0 -124
- package/test/test_wolfssl_read.js +0 -203
- package/test/test_wolfssl_read.ts +0 -184
- package/test/test_worker.js +0 -21
- package/test/test_worker.ts +0 -24
- package/test/test_worker_concurrent.js +0 -45
- package/test/test_worker_concurrent.ts +0 -56
- package/test/test_worker_data_url.js +0 -46
- package/test/test_worker_data_url.ts +0 -51
- package/test/test_worker_fetch.js +0 -26
- package/test/test_worker_fetch.ts +0 -27
- package/test/test_worker_net.js +0 -69
- package/test/test_worker_net.ts +0 -66
- package/test/test_worker_wasm_concurrent.js +0 -44
- package/test/test_worker_wasm_concurrent.ts +0 -45
- package/test/types.wasm +0 -0
- package/test/worker_fetch.js +0 -13
- package/test/worker_fetch.ts +0 -13
- package/test/worker_helper.js +0 -1
- package/test/worker_helper.ts +0 -1
- package/test/worker_http.js +0 -18
- package/test/worker_http.ts +0 -20
- package/test/worker_import_lib.js +0 -12
- package/test/worker_import_lib.ts +0 -13
- package/test/worker_module.js +0 -16
- package/test/worker_module.ts +0 -17
- package/test/worker_wasm_concurrent.js +0 -22
- package/test/worker_wasm_concurrent.ts +0 -22
package/lib/http-server.ts
DELETED
|
@@ -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
|
-
}
|