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