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