quickwin 1.0.2 → 1.1.1

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 (78) hide show
  1. package/README.en.md +204 -204
  2. package/README.md +203 -203
  3. package/examples/PdfCanvas.tsx +83 -83
  4. package/examples/exec_server.ts +35 -35
  5. package/examples/pdf_preview2.ts +375 -375
  6. package/examples/pdf_viewer.tsx +279 -279
  7. package/examples/test_gallery.tsx +285 -285
  8. package/examples/test_react_lazy.tsx +63 -63
  9. package/examples/test_react_listbox.tsx +71 -71
  10. package/examples/test_root_window.tsx +41 -41
  11. package/examples/test_tab.tsx +75 -75
  12. package/examples/tray_demo.tsx +81 -81
  13. package/lib/assert.ts +6 -6
  14. package/lib/cache-utils.ts +40 -40
  15. package/lib/dumpRects.ts +25 -25
  16. package/lib/fetch.ts +502 -502
  17. package/lib/http-common.ts +399 -399
  18. package/lib/http-server.ts +424 -424
  19. package/lib/polyfill.ts +171 -171
  20. package/lib/react-qw/components/index.ts +17 -17
  21. package/lib/react-qw/index.ts +90 -90
  22. package/lib/react-qw/jsx.d.ts +34 -34
  23. package/lib/react-qw/layout.ts +111 -111
  24. package/lib/react-qw/props.ts +36 -36
  25. package/lib/react-qw/reconciler.ts +373 -373
  26. package/lib/stream.ts +21 -21
  27. package/lib/text-codec.ts +171 -171
  28. package/lib/text-measure.ts +54 -54
  29. package/lib/url.ts +201 -201
  30. package/lib/vendor/web-streams/ponyfill.d.mts +829 -829
  31. package/lib/vendor/web-streams/ponyfill.mjs +8 -8
  32. package/lib/websocket.js +8 -0
  33. package/lib/websocket.ts +563 -555
  34. package/package.json +50 -44
  35. package/qwin-nowasm-x86.exe +0 -0
  36. package/qwin-nowasm.exe +0 -0
  37. package/qwin-x86.exe +0 -0
  38. package/qwin.exe +0 -0
  39. package/test/run.ts +96 -96
  40. package/test/test_basic.ts +9 -9
  41. package/test/test_basic_fetch.ts +33 -33
  42. package/test/test_brotli.ts +52 -52
  43. package/test/test_fetch_cache.ts +187 -187
  44. package/test/test_ffi.ts +174 -174
  45. package/test/test_frame_encoding.ts +132 -132
  46. package/test/test_helper.ts +80 -80
  47. package/test/test_http_import.ts +74 -74
  48. package/test/test_http_server.ts +136 -136
  49. package/test/test_import_dir.ts +5 -5
  50. package/test/test_ipv6.ts +129 -129
  51. package/test/test_meta_url.ts +4 -4
  52. package/test/test_mupdf_render.ts +74 -74
  53. package/test/test_mupdf_twice.ts +81 -81
  54. package/test/test_mupdf_wasm.ts +30 -30
  55. package/test/test_net_event.ts +55 -55
  56. package/test/test_net_fetch.ts +238 -238
  57. package/test/test_net_websocket.ts +164 -164
  58. package/test/test_polyfill.ts +159 -159
  59. package/test/test_url.ts +183 -183
  60. package/test/test_wasm_basic.ts +73 -73
  61. package/test/test_wasm_import_global.ts +45 -45
  62. package/test/test_wasm_sjlj.ts +125 -125
  63. package/test/test_wasm_types.ts +124 -124
  64. package/test/test_worker.ts +24 -24
  65. package/test/test_worker_fetch.ts +27 -27
  66. package/test/test_worker_net.ts +54 -54
  67. package/test/test_worker_wasm_concurrent.ts +45 -45
  68. package/test/worker_fetch.ts +13 -13
  69. package/test/worker_helper.ts +1 -1
  70. package/test/worker_http.ts +20 -20
  71. package/test/worker_import_lib.ts +13 -13
  72. package/test/worker_module.ts +17 -17
  73. package/test/worker_wasm_concurrent.ts +22 -22
  74. package/tsconfig.json +27 -27
  75. package/vendor/mupdf-wasm/mupdf-wasm.d.ts +571 -571
  76. package/vendor/mupdf-wasm/mupdf-wasm.js +2749 -2749
  77. package/vendor/mupdf-wasm/mupdf.d.ts +939 -939
  78. package/vendor/mupdf-wasm/mupdf.js +3317 -3317
@@ -1,424 +1,424 @@
1
- import './polyfill.js'
2
- import * as sock from 'sock'
3
- import { HeadersImpl, RequestImpl, ResponseImpl, _readStream } from './http-common.js'
4
-
5
- type SockHandle = import('sock').SockHandle
6
-
7
- // ── Public API ──
8
-
9
- export type ServerHandler = (req: Request) => Promise<Response>
10
-
11
- // ── Internal types ──
12
-
13
- interface NetEvent { lNetworkEvents: number; iErrorCode: number[] }
14
-
15
- interface ParsedHeader {
16
- method: string
17
- target: string
18
- version: string
19
- headers: Record<string, string>
20
- chunked: boolean
21
- contentLength: number
22
- }
23
-
24
- interface PendingReq {
25
- parsed: ParsedHeader
26
- bodyStart: number
27
- }
28
-
29
- interface ActiveReq {
30
- req: RequestImpl
31
- }
32
-
33
- interface Conn {
34
- sock: SockHandle
35
- parts: Uint8Array[]
36
- pending: PendingReq | null
37
- current: ActiveReq | null
38
- outQueue: Uint8Array | null
39
- closed: boolean
40
- closeAfterFlush: boolean
41
- keepAlive: boolean
42
- owner: HttpServer
43
- }
44
-
45
- const STATUS_TEXT: Record<number, string> = {
46
- 100: 'Continue', 101: 'Switching Protocols',
47
- 200: 'OK', 201: 'Created', 202: 'Accepted', 204: 'No Content',
48
- 301: 'Moved Permanently', 302: 'Found', 304: 'Not Modified',
49
- 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found',
50
- 405: 'Method Not Allowed', 408: 'Request Timeout', 413: 'Payload Too Large',
51
- 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable',
52
- }
53
-
54
- // ── Byte helpers ──
55
-
56
- function concatU8(parts: Uint8Array[]): Uint8Array {
57
- let total = 0
58
- for (const p of parts) total += p.length
59
- const out = new Uint8Array(total)
60
- let off = 0
61
- for (const p of parts) { out.set(p, off); off += p.length }
62
- return out
63
- }
64
-
65
- function toArrayBuffer(u8: Uint8Array): ArrayBuffer {
66
- return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer
67
- }
68
-
69
- function findCrLfCrLf(buf: Uint8Array): number {
70
- const n = buf.length - 3
71
- for (let i = 0; i < n; i++) {
72
- if (buf[i] === 13 && buf[i + 1] === 10 && buf[i + 2] === 13 && buf[i + 3] === 10) return i
73
- }
74
- return -1
75
- }
76
-
77
- /** Decode a chunked-transfer body starting at `start`. Returns the decoded
78
- * data and the offset just past the terminating CRLF, or null if the buffer
79
- * does not yet contain the complete chunked body. */
80
- function decodeChunked(buf: Uint8Array, start: number): { data: Uint8Array; end: number } | null {
81
- let pos = start
82
- const parts: Uint8Array[] = []
83
- const decoder = new TextDecoder('utf-8')
84
- while (pos < buf.length) {
85
- let eol = pos
86
- while (eol < buf.length && buf[eol] !== 13) eol++
87
- if (eol >= buf.length || eol + 1 >= buf.length || buf[eol + 1] !== 10) return null
88
- const sizeLine = decoder.decode(buf.subarray(pos, eol)).trim()
89
- const semi = sizeLine.indexOf(';')
90
- const hex = (semi >= 0 ? sizeLine.slice(0, semi) : sizeLine).trim()
91
- const size = parseInt(hex, 16)
92
- if (isNaN(size) || size < 0) return null
93
- pos = eol + 2
94
- if (size === 0) {
95
- while (true) {
96
- if (pos >= buf.length) return null
97
- if (buf[pos] === 13) {
98
- if (pos + 1 >= buf.length || buf[pos + 1] !== 10) return null
99
- pos += 2
100
- return { data: concatU8(parts), end: pos }
101
- }
102
- let t = pos
103
- while (t < buf.length && buf[t] !== 13) t++
104
- if (t >= buf.length || t + 1 >= buf.length || buf[t + 1] !== 10) return null
105
- pos = t + 2
106
- }
107
- }
108
- if (pos + size > buf.length) return null
109
- parts.push(buf.subarray(pos, pos + size))
110
- pos += size
111
- if (pos + 1 >= buf.length || buf[pos] !== 13 || buf[pos + 1] !== 10) return null
112
- pos += 2
113
- }
114
- return null
115
- }
116
-
117
- // ── Parsing ──
118
-
119
- function parseHeader(headerStr: string): ParsedHeader | null {
120
- const lines = headerStr.split('\r\n')
121
- const first = lines[0]
122
- if (!first) return null
123
- const sp1 = first.indexOf(' ')
124
- const sp2 = first.lastIndexOf(' ')
125
- if (sp1 <= 0 || sp2 <= sp1) return null
126
- const method = first.slice(0, sp1)
127
- const target = first.slice(sp1 + 1, sp2)
128
- const version = first.slice(sp2 + 1).trim()
129
-
130
- const headers: Record<string, string> = {}
131
- for (let i = 1; i < lines.length; i++) {
132
- const line = lines[i]
133
- if (!line) continue
134
- const ci = line.indexOf(':')
135
- if (ci <= 0) continue
136
- const name = line.slice(0, ci).trim().toLowerCase()
137
- const value = line.slice(ci + 1).trim()
138
- if (headers[name] !== undefined) headers[name] = headers[name] + ', ' + value
139
- else headers[name] = value
140
- }
141
-
142
- const te = (headers['transfer-encoding'] || '').toLowerCase()
143
- const chunked = te.indexOf('chunked') >= 0
144
- const contentLength = parseInt(headers['content-length'] || '0', 10) || 0
145
- return { method, target, version, headers, chunked, contentLength }
146
- }
147
-
148
- // ── Server ──
149
-
150
- export class HttpServer {
151
- readonly handler: ServerHandler
152
- private serverSock: SockHandle | null = null
153
- readonly conns = new Map<SockHandle, Conn>()
154
- private closed = false
155
-
156
- constructor(handler: ServerHandler) {
157
- this.handler = handler
158
- }
159
-
160
- /** Actual bound address, or null if not listening. */
161
- address(): { addr: string; port: number } | null {
162
- if (this.serverSock === null) return null
163
- return sock.getsockname(this.serverSock)
164
- }
165
-
166
- /** Bind and start listening. Returns 0 on success, -1 on failure. */
167
- listen(port: number, host?: string, backlog?: number): number {
168
- if (this.serverSock !== null) return -1
169
- const s = sock.socket()
170
- if (s < 0) return -1
171
- if (sock.bind(s, host ?? null, port) !== 0) { sock.closesocket(s); return -1 }
172
- if (sock.listen(s, backlog ?? 64) !== 0) { sock.closesocket(s); return -1 }
173
- this.serverSock = s
174
- sock.set_on_event(s, (event: NetEvent) => {
175
- if (event.lNetworkEvents & sock.FdEvent.FD_ACCEPT) this.acceptLoop()
176
- })
177
- return 0
178
- }
179
-
180
- close(): void {
181
- if (this.closed) return
182
- this.closed = true
183
- for (const c of Array.from(this.conns.values())) closeConn(c)
184
- this.conns.clear()
185
- if (this.serverSock !== null) { sock.closesocket(this.serverSock); this.serverSock = null }
186
- }
187
-
188
- private acceptLoop(): void {
189
- if (this.closed || this.serverSock === null) return
190
- while (true) {
191
- const ac = sock.accept(this.serverSock)
192
- if (!ac) break
193
- const c: Conn = {
194
- sock: ac.handle,
195
- parts: [],
196
- pending: null,
197
- current: null,
198
- outQueue: null,
199
- closed: false,
200
- closeAfterFlush: false,
201
- keepAlive: true,
202
- owner: this,
203
- }
204
- this.conns.set(c.sock, c)
205
- sock.set_on_event(c.sock, (event: NetEvent) => this.onConnEvent(c, event))
206
- }
207
- }
208
-
209
- private onConnEvent(c: Conn, event: NetEvent): void {
210
- if (c.closed) return
211
- if (event.lNetworkEvents & sock.FdEvent.FD_READ) onRead(c)
212
- if (event.lNetworkEvents & sock.FdEvent.FD_WRITE) flushQueue(c)
213
- if (event.lNetworkEvents & sock.FdEvent.FD_CLOSE) {
214
- onRead(c)
215
- if (!c.current && !c.pending) closeConn(c)
216
- }
217
- }
218
- }
219
-
220
- export function createServer(handler: ServerHandler): HttpServer {
221
- return new HttpServer(handler)
222
- }
223
-
224
- // ── Connection I/O ──
225
-
226
- function onRead(c: Conn): void {
227
- if (c.closed) return
228
- while (true) {
229
- const d = sock.recv(c.sock, 8192)
230
- if (!d) break
231
- c.parts.push(new Uint8Array(d))
232
- }
233
- if (!c.closed) tryProcess(c)
234
- }
235
-
236
- function queueSend(c: Conn, data: Uint8Array): void {
237
- if (c.closed || data.length === 0) return
238
- if (c.outQueue && c.outQueue.length > 0) {
239
- c.outQueue = concatU8([c.outQueue, data])
240
- return
241
- }
242
- const ret = sock.send(c.sock, toArrayBuffer(data))
243
- if (ret < 0) {
244
- c.outQueue = data
245
- return
246
- }
247
- if (ret < data.length) {
248
- c.outQueue = data.subarray(ret)
249
- return
250
- }
251
- if (c.closeAfterFlush) closeConn(c)
252
- }
253
-
254
- function flushQueue(c: Conn): void {
255
- if (c.closed) return
256
- if (!c.outQueue || c.outQueue.length === 0) {
257
- if (c.closeAfterFlush) closeConn(c)
258
- return
259
- }
260
- const q = c.outQueue
261
- const ret = sock.send(c.sock, toArrayBuffer(q))
262
- if (ret < 0) return
263
- if (ret < q.length) {
264
- c.outQueue = q.subarray(ret)
265
- return
266
- }
267
- c.outQueue = null
268
- if (c.closeAfterFlush) closeConn(c)
269
- }
270
-
271
- function closeConn(c: Conn): void {
272
- if (c.closed) return
273
- c.closed = true
274
- if (c.sock >= 0) sock.closesocket(c.sock)
275
- c.owner.conns.delete(c.sock)
276
- }
277
-
278
- // ── Request state machine ──
279
-
280
- function whole(c: Conn): Uint8Array {
281
- return concatU8(c.parts)
282
- }
283
-
284
- function trimParts(c: Conn, buf: Uint8Array, consumed: number): void {
285
- if (consumed >= buf.length) {
286
- c.parts = []
287
- } else {
288
- c.parts = [buf.subarray(consumed)]
289
- }
290
- }
291
-
292
- function tryProcess(c: Conn): void {
293
- if (c.closed || c.current) return
294
- const buf = whole(c)
295
-
296
- if (c.pending) {
297
- const bodyStart = c.pending.bodyStart
298
- const parsed = c.pending.parsed
299
- let body: Uint8Array
300
- let end: number
301
- if (parsed.chunked) {
302
- const dec = decodeChunked(buf, bodyStart)
303
- if (!dec) return
304
- body = dec.data
305
- end = dec.end
306
- } else {
307
- const needed = parsed.contentLength
308
- if (bodyStart + needed > buf.length) return
309
- body = buf.subarray(bodyStart, bodyStart + needed)
310
- end = bodyStart + needed
311
- }
312
- c.pending = null
313
- dispatchRequest(c, parsed, body, end)
314
- return
315
- }
316
-
317
- const headerEnd = findCrLfCrLf(buf)
318
- if (headerEnd < 0) return
319
- const absEnd = headerEnd + 4
320
- const headerStr = new TextDecoder('utf-8').decode(buf.subarray(0, absEnd))
321
- const parsed = parseHeader(headerStr)
322
- if (!parsed) { closeConn(c); return }
323
-
324
- const bodyStart = absEnd
325
- if (parsed.chunked) {
326
- const dec = decodeChunked(buf, bodyStart)
327
- if (!dec) {
328
- c.pending = { parsed, bodyStart }
329
- return
330
- }
331
- dispatchRequest(c, parsed, dec.data, dec.end)
332
- } else if (parsed.contentLength > 0) {
333
- if (bodyStart + parsed.contentLength > buf.length) {
334
- c.pending = { parsed, bodyStart }
335
- return
336
- }
337
- dispatchRequest(c, parsed, buf.subarray(bodyStart, bodyStart + parsed.contentLength), bodyStart + parsed.contentLength)
338
- } else {
339
- dispatchRequest(c, parsed, new Uint8Array(0), bodyStart)
340
- }
341
- }
342
-
343
- function dispatchRequest(c: Conn, parsed: ParsedHeader, body: Uint8Array, end: number): void {
344
- const connHeader = (parsed.headers['connection'] || '').toLowerCase()
345
- if (parsed.version === 'HTTP/1.1') c.keepAlive = connHeader.indexOf('close') < 0
346
- else c.keepAlive = connHeader.indexOf('keep-alive') >= 0
347
-
348
- let host = parsed.headers['host']
349
- if (!host) {
350
- const a = c.owner.address()
351
- const addrStr = a ? (a.addr.includes(':') ? `[${a.addr}]` : a.addr) : 'localhost'
352
- host = a ? addrStr + ':' + a.port : 'localhost'
353
- }
354
-
355
- const isAbsolute = parsed.target.startsWith('http://') || parsed.target.startsWith('https://')
356
- const url = isAbsolute ? parsed.target : 'http://' + host + parsed.target
357
-
358
- const req = new RequestImpl(url, {
359
- method: parsed.method,
360
- headers: new HeadersImpl(parsed.headers),
361
- body: body.length > 0 ? body : undefined,
362
- })
363
-
364
- c.current = { req }
365
- trimParts(c, whole(c), end)
366
- void handleRequest(c)
367
- }
368
-
369
- async function handleRequest(c: Conn): Promise<void> {
370
- const req = c.current?.req
371
- if (!req) { c.current = null; return }
372
-
373
- let res: Response
374
- try {
375
- const result = await c.owner.handler(req)
376
- if (result instanceof ResponseImpl) {
377
- res = result
378
- } else {
379
- console.error('[http-server] handler must return a Response')
380
- res = new ResponseImpl('Internal Server Error', { status: 500 })
381
- }
382
- } catch (e) {
383
- console.error('[http-server] handler error:', e)
384
- res = new ResponseImpl('Internal Server Error', { status: 500 })
385
- }
386
-
387
- if (c.closed) return
388
- await sendResponse(c, req.method, res)
389
- if (c.closed) return
390
-
391
- c.current = null
392
- if (!c.closeAfterFlush) tryProcess(c)
393
- }
394
-
395
- async function sendResponse(c: Conn, method: string, res: Response): Promise<void> {
396
- const status = res.status
397
- const statusText = res.statusText || STATUS_TEXT[status] || 'OK'
398
- const headers = new HeadersImpl(res.headers)
399
-
400
- let body: Uint8Array
401
- try {
402
- body = await _readStream(res.body)
403
- } catch {
404
- body = new Uint8Array(0)
405
- }
406
- if (c.closed) return
407
-
408
- const headOnly = method === 'HEAD'
409
- const bodyless = (status >= 100 && status < 200) || status === 204 || status === 304
410
-
411
- if (!headers.has('content-length') && !bodyless) headers.set('Content-Length', String(body.length))
412
- if (!headers.has('connection')) headers.set('Connection', c.keepAlive ? 'keep-alive' : 'close')
413
-
414
- const connHdr = (headers.get('connection') || '').toLowerCase()
415
- if (!c.keepAlive || connHdr === 'close') c.closeAfterFlush = true
416
-
417
- let head = 'HTTP/1.1 ' + status + ' ' + statusText + '\r\n'
418
- for (const [name, value] of headers) head += name + ': ' + value + '\r\n'
419
- head += '\r\n'
420
-
421
- const headBytes = new TextEncoder().encode(head)
422
- const payload = headOnly || body.length === 0 ? headBytes : concatU8([headBytes, body])
423
- queueSend(c, payload)
424
- }
1
+ import './polyfill.js'
2
+ import * as sock from 'sock'
3
+ import { HeadersImpl, RequestImpl, ResponseImpl, _readStream } from './http-common.js'
4
+
5
+ type SockHandle = import('sock').SockHandle
6
+
7
+ // ── Public API ──
8
+
9
+ export type ServerHandler = (req: Request) => Promise<Response>
10
+
11
+ // ── Internal types ──
12
+
13
+ interface NetEvent { lNetworkEvents: number; iErrorCode: number[] }
14
+
15
+ interface ParsedHeader {
16
+ method: string
17
+ target: string
18
+ version: string
19
+ headers: Record<string, string>
20
+ chunked: boolean
21
+ contentLength: number
22
+ }
23
+
24
+ interface PendingReq {
25
+ parsed: ParsedHeader
26
+ bodyStart: number
27
+ }
28
+
29
+ interface ActiveReq {
30
+ req: RequestImpl
31
+ }
32
+
33
+ interface Conn {
34
+ sock: SockHandle
35
+ parts: Uint8Array[]
36
+ pending: PendingReq | null
37
+ current: ActiveReq | null
38
+ outQueue: Uint8Array | null
39
+ closed: boolean
40
+ closeAfterFlush: boolean
41
+ keepAlive: boolean
42
+ owner: HttpServer
43
+ }
44
+
45
+ const STATUS_TEXT: Record<number, string> = {
46
+ 100: 'Continue', 101: 'Switching Protocols',
47
+ 200: 'OK', 201: 'Created', 202: 'Accepted', 204: 'No Content',
48
+ 301: 'Moved Permanently', 302: 'Found', 304: 'Not Modified',
49
+ 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found',
50
+ 405: 'Method Not Allowed', 408: 'Request Timeout', 413: 'Payload Too Large',
51
+ 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable',
52
+ }
53
+
54
+ // ── Byte helpers ──
55
+
56
+ function concatU8(parts: Uint8Array[]): Uint8Array {
57
+ let total = 0
58
+ for (const p of parts) total += p.length
59
+ const out = new Uint8Array(total)
60
+ let off = 0
61
+ for (const p of parts) { out.set(p, off); off += p.length }
62
+ return out
63
+ }
64
+
65
+ function toArrayBuffer(u8: Uint8Array): ArrayBuffer {
66
+ return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer
67
+ }
68
+
69
+ function findCrLfCrLf(buf: Uint8Array): number {
70
+ const n = buf.length - 3
71
+ for (let i = 0; i < n; i++) {
72
+ if (buf[i] === 13 && buf[i + 1] === 10 && buf[i + 2] === 13 && buf[i + 3] === 10) return i
73
+ }
74
+ return -1
75
+ }
76
+
77
+ /** Decode a chunked-transfer body starting at `start`. Returns the decoded
78
+ * data and the offset just past the terminating CRLF, or null if the buffer
79
+ * does not yet contain the complete chunked body. */
80
+ function decodeChunked(buf: Uint8Array, start: number): { data: Uint8Array; end: number } | null {
81
+ let pos = start
82
+ const parts: Uint8Array[] = []
83
+ const decoder = new TextDecoder('utf-8')
84
+ while (pos < buf.length) {
85
+ let eol = pos
86
+ while (eol < buf.length && buf[eol] !== 13) eol++
87
+ if (eol >= buf.length || eol + 1 >= buf.length || buf[eol + 1] !== 10) return null
88
+ const sizeLine = decoder.decode(buf.subarray(pos, eol)).trim()
89
+ const semi = sizeLine.indexOf(';')
90
+ const hex = (semi >= 0 ? sizeLine.slice(0, semi) : sizeLine).trim()
91
+ const size = parseInt(hex, 16)
92
+ if (isNaN(size) || size < 0) return null
93
+ pos = eol + 2
94
+ if (size === 0) {
95
+ while (true) {
96
+ if (pos >= buf.length) return null
97
+ if (buf[pos] === 13) {
98
+ if (pos + 1 >= buf.length || buf[pos + 1] !== 10) return null
99
+ pos += 2
100
+ return { data: concatU8(parts), end: pos }
101
+ }
102
+ let t = pos
103
+ while (t < buf.length && buf[t] !== 13) t++
104
+ if (t >= buf.length || t + 1 >= buf.length || buf[t + 1] !== 10) return null
105
+ pos = t + 2
106
+ }
107
+ }
108
+ if (pos + size > buf.length) return null
109
+ parts.push(buf.subarray(pos, pos + size))
110
+ pos += size
111
+ if (pos + 1 >= buf.length || buf[pos] !== 13 || buf[pos + 1] !== 10) return null
112
+ pos += 2
113
+ }
114
+ return null
115
+ }
116
+
117
+ // ── Parsing ──
118
+
119
+ function parseHeader(headerStr: string): ParsedHeader | null {
120
+ const lines = headerStr.split('\r\n')
121
+ const first = lines[0]
122
+ if (!first) return null
123
+ const sp1 = first.indexOf(' ')
124
+ const sp2 = first.lastIndexOf(' ')
125
+ if (sp1 <= 0 || sp2 <= sp1) return null
126
+ const method = first.slice(0, sp1)
127
+ const target = first.slice(sp1 + 1, sp2)
128
+ const version = first.slice(sp2 + 1).trim()
129
+
130
+ const headers: Record<string, string> = {}
131
+ for (let i = 1; i < lines.length; i++) {
132
+ const line = lines[i]
133
+ if (!line) continue
134
+ const ci = line.indexOf(':')
135
+ if (ci <= 0) continue
136
+ const name = line.slice(0, ci).trim().toLowerCase()
137
+ const value = line.slice(ci + 1).trim()
138
+ if (headers[name] !== undefined) headers[name] = headers[name] + ', ' + value
139
+ else headers[name] = value
140
+ }
141
+
142
+ const te = (headers['transfer-encoding'] || '').toLowerCase()
143
+ const chunked = te.indexOf('chunked') >= 0
144
+ const contentLength = parseInt(headers['content-length'] || '0', 10) || 0
145
+ return { method, target, version, headers, chunked, contentLength }
146
+ }
147
+
148
+ // ── Server ──
149
+
150
+ export class HttpServer {
151
+ readonly handler: ServerHandler
152
+ private serverSock: SockHandle | null = null
153
+ readonly conns = new Map<SockHandle, Conn>()
154
+ private closed = false
155
+
156
+ constructor(handler: ServerHandler) {
157
+ this.handler = handler
158
+ }
159
+
160
+ /** Actual bound address, or null if not listening. */
161
+ address(): { addr: string; port: number } | null {
162
+ if (this.serverSock === null) return null
163
+ return sock.getsockname(this.serverSock)
164
+ }
165
+
166
+ /** Bind and start listening. Returns 0 on success, -1 on failure. */
167
+ listen(port: number, host?: string, backlog?: number): number {
168
+ if (this.serverSock !== null) return -1
169
+ const s = sock.socket()
170
+ if (s < 0) return -1
171
+ if (sock.bind(s, host ?? null, port) !== 0) { sock.closesocket(s); return -1 }
172
+ if (sock.listen(s, backlog ?? 64) !== 0) { sock.closesocket(s); return -1 }
173
+ this.serverSock = s
174
+ sock.set_on_event(s, (event: NetEvent) => {
175
+ if (event.lNetworkEvents & sock.FdEvent.FD_ACCEPT) this.acceptLoop()
176
+ })
177
+ return 0
178
+ }
179
+
180
+ close(): void {
181
+ if (this.closed) return
182
+ this.closed = true
183
+ for (const c of Array.from(this.conns.values())) closeConn(c)
184
+ this.conns.clear()
185
+ if (this.serverSock !== null) { sock.closesocket(this.serverSock); this.serverSock = null }
186
+ }
187
+
188
+ private acceptLoop(): void {
189
+ if (this.closed || this.serverSock === null) return
190
+ while (true) {
191
+ const ac = sock.accept(this.serverSock)
192
+ if (!ac) break
193
+ const c: Conn = {
194
+ sock: ac.handle,
195
+ parts: [],
196
+ pending: null,
197
+ current: null,
198
+ outQueue: null,
199
+ closed: false,
200
+ closeAfterFlush: false,
201
+ keepAlive: true,
202
+ owner: this,
203
+ }
204
+ this.conns.set(c.sock, c)
205
+ sock.set_on_event(c.sock, (event: NetEvent) => this.onConnEvent(c, event))
206
+ }
207
+ }
208
+
209
+ private onConnEvent(c: Conn, event: NetEvent): void {
210
+ if (c.closed) return
211
+ if (event.lNetworkEvents & sock.FdEvent.FD_READ) onRead(c)
212
+ if (event.lNetworkEvents & sock.FdEvent.FD_WRITE) flushQueue(c)
213
+ if (event.lNetworkEvents & sock.FdEvent.FD_CLOSE) {
214
+ onRead(c)
215
+ if (!c.current && !c.pending) closeConn(c)
216
+ }
217
+ }
218
+ }
219
+
220
+ export function createServer(handler: ServerHandler): HttpServer {
221
+ return new HttpServer(handler)
222
+ }
223
+
224
+ // ── Connection I/O ──
225
+
226
+ function onRead(c: Conn): void {
227
+ if (c.closed) return
228
+ while (true) {
229
+ const d = sock.recv(c.sock, 8192)
230
+ if (!d) break
231
+ c.parts.push(new Uint8Array(d))
232
+ }
233
+ if (!c.closed) tryProcess(c)
234
+ }
235
+
236
+ function queueSend(c: Conn, data: Uint8Array): void {
237
+ if (c.closed || data.length === 0) return
238
+ if (c.outQueue && c.outQueue.length > 0) {
239
+ c.outQueue = concatU8([c.outQueue, data])
240
+ return
241
+ }
242
+ const ret = sock.send(c.sock, toArrayBuffer(data))
243
+ if (ret < 0) {
244
+ c.outQueue = data
245
+ return
246
+ }
247
+ if (ret < data.length) {
248
+ c.outQueue = data.subarray(ret)
249
+ return
250
+ }
251
+ if (c.closeAfterFlush) closeConn(c)
252
+ }
253
+
254
+ function flushQueue(c: Conn): void {
255
+ if (c.closed) return
256
+ if (!c.outQueue || c.outQueue.length === 0) {
257
+ if (c.closeAfterFlush) closeConn(c)
258
+ return
259
+ }
260
+ const q = c.outQueue
261
+ const ret = sock.send(c.sock, toArrayBuffer(q))
262
+ if (ret < 0) return
263
+ if (ret < q.length) {
264
+ c.outQueue = q.subarray(ret)
265
+ return
266
+ }
267
+ c.outQueue = null
268
+ if (c.closeAfterFlush) closeConn(c)
269
+ }
270
+
271
+ function closeConn(c: Conn): void {
272
+ if (c.closed) return
273
+ c.closed = true
274
+ if (c.sock >= 0) sock.closesocket(c.sock)
275
+ c.owner.conns.delete(c.sock)
276
+ }
277
+
278
+ // ── Request state machine ──
279
+
280
+ function whole(c: Conn): Uint8Array {
281
+ return concatU8(c.parts)
282
+ }
283
+
284
+ function trimParts(c: Conn, buf: Uint8Array, consumed: number): void {
285
+ if (consumed >= buf.length) {
286
+ c.parts = []
287
+ } else {
288
+ c.parts = [buf.subarray(consumed)]
289
+ }
290
+ }
291
+
292
+ function tryProcess(c: Conn): void {
293
+ if (c.closed || c.current) return
294
+ const buf = whole(c)
295
+
296
+ if (c.pending) {
297
+ const bodyStart = c.pending.bodyStart
298
+ const parsed = c.pending.parsed
299
+ let body: Uint8Array
300
+ let end: number
301
+ if (parsed.chunked) {
302
+ const dec = decodeChunked(buf, bodyStart)
303
+ if (!dec) return
304
+ body = dec.data
305
+ end = dec.end
306
+ } else {
307
+ const needed = parsed.contentLength
308
+ if (bodyStart + needed > buf.length) return
309
+ body = buf.subarray(bodyStart, bodyStart + needed)
310
+ end = bodyStart + needed
311
+ }
312
+ c.pending = null
313
+ dispatchRequest(c, parsed, body, end)
314
+ return
315
+ }
316
+
317
+ const headerEnd = findCrLfCrLf(buf)
318
+ if (headerEnd < 0) return
319
+ const absEnd = headerEnd + 4
320
+ const headerStr = new TextDecoder('utf-8').decode(buf.subarray(0, absEnd))
321
+ const parsed = parseHeader(headerStr)
322
+ if (!parsed) { closeConn(c); return }
323
+
324
+ const bodyStart = absEnd
325
+ if (parsed.chunked) {
326
+ const dec = decodeChunked(buf, bodyStart)
327
+ if (!dec) {
328
+ c.pending = { parsed, bodyStart }
329
+ return
330
+ }
331
+ dispatchRequest(c, parsed, dec.data, dec.end)
332
+ } else if (parsed.contentLength > 0) {
333
+ if (bodyStart + parsed.contentLength > buf.length) {
334
+ c.pending = { parsed, bodyStart }
335
+ return
336
+ }
337
+ dispatchRequest(c, parsed, buf.subarray(bodyStart, bodyStart + parsed.contentLength), bodyStart + parsed.contentLength)
338
+ } else {
339
+ dispatchRequest(c, parsed, new Uint8Array(0), bodyStart)
340
+ }
341
+ }
342
+
343
+ function dispatchRequest(c: Conn, parsed: ParsedHeader, body: Uint8Array, end: number): void {
344
+ const connHeader = (parsed.headers['connection'] || '').toLowerCase()
345
+ if (parsed.version === 'HTTP/1.1') c.keepAlive = connHeader.indexOf('close') < 0
346
+ else c.keepAlive = connHeader.indexOf('keep-alive') >= 0
347
+
348
+ let host = parsed.headers['host']
349
+ if (!host) {
350
+ const a = c.owner.address()
351
+ const addrStr = a ? (a.addr.includes(':') ? `[${a.addr}]` : a.addr) : 'localhost'
352
+ host = a ? addrStr + ':' + a.port : 'localhost'
353
+ }
354
+
355
+ const isAbsolute = parsed.target.startsWith('http://') || parsed.target.startsWith('https://')
356
+ const url = isAbsolute ? parsed.target : 'http://' + host + parsed.target
357
+
358
+ const req = new RequestImpl(url, {
359
+ method: parsed.method,
360
+ headers: new HeadersImpl(parsed.headers),
361
+ body: body.length > 0 ? body : undefined,
362
+ })
363
+
364
+ c.current = { req }
365
+ trimParts(c, whole(c), end)
366
+ void handleRequest(c)
367
+ }
368
+
369
+ async function handleRequest(c: Conn): Promise<void> {
370
+ const req = c.current?.req
371
+ if (!req) { c.current = null; return }
372
+
373
+ let res: Response
374
+ try {
375
+ const result = await c.owner.handler(req)
376
+ if (result instanceof ResponseImpl) {
377
+ res = result
378
+ } else {
379
+ console.error('[http-server] handler must return a Response')
380
+ res = new ResponseImpl('Internal Server Error', { status: 500 })
381
+ }
382
+ } catch (e) {
383
+ console.error('[http-server] handler error:', e)
384
+ res = new ResponseImpl('Internal Server Error', { status: 500 })
385
+ }
386
+
387
+ if (c.closed) return
388
+ await sendResponse(c, req.method, res)
389
+ if (c.closed) return
390
+
391
+ c.current = null
392
+ if (!c.closeAfterFlush) tryProcess(c)
393
+ }
394
+
395
+ async function sendResponse(c: Conn, method: string, res: Response): Promise<void> {
396
+ const status = res.status
397
+ const statusText = res.statusText || STATUS_TEXT[status] || 'OK'
398
+ const headers = new HeadersImpl(res.headers)
399
+
400
+ let body: Uint8Array
401
+ try {
402
+ body = await _readStream(res.body)
403
+ } catch {
404
+ body = new Uint8Array(0)
405
+ }
406
+ if (c.closed) return
407
+
408
+ const headOnly = method === 'HEAD'
409
+ const bodyless = (status >= 100 && status < 200) || status === 204 || status === 304
410
+
411
+ if (!headers.has('content-length') && !bodyless) headers.set('Content-Length', String(body.length))
412
+ if (!headers.has('connection')) headers.set('Connection', c.keepAlive ? 'keep-alive' : 'close')
413
+
414
+ const connHdr = (headers.get('connection') || '').toLowerCase()
415
+ if (!c.keepAlive || connHdr === 'close') c.closeAfterFlush = true
416
+
417
+ let head = 'HTTP/1.1 ' + status + ' ' + statusText + '\r\n'
418
+ for (const [name, value] of headers) head += name + ': ' + value + '\r\n'
419
+ head += '\r\n'
420
+
421
+ const headBytes = new TextEncoder().encode(head)
422
+ const payload = headOnly || body.length === 0 ? headBytes : concatU8([headBytes, body])
423
+ queueSend(c, payload)
424
+ }