quickwin 2026.5.2-3.145209
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.md +6 -0
- package/examples/pdf_preview.js +440 -0
- package/examples/pdf_preview.ts +470 -0
- package/examples/preact_demo.js +35 -0
- package/examples/preact_demo.tsx +49 -0
- package/examples/tray_demo.js +75 -0
- package/examples/tray_demo.tsx +79 -0
- package/lib/fetch.js +746 -0
- package/lib/fetch.ts +811 -0
- package/lib/polyfill.js +500 -0
- package/lib/polyfill.ts +454 -0
- package/lib/preact/hooks.js +287 -0
- package/lib/preact/hooks.ts +330 -0
- package/lib/preact/jsx-runtime.js +1 -0
- package/lib/preact/jsx-runtime.ts +2 -0
- package/lib/preact/jsx.d.ts +36 -0
- package/lib/preact/layout.js +153 -0
- package/lib/preact/layout.ts +183 -0
- package/lib/preact/preact.js +54 -0
- package/lib/preact/preact.ts +133 -0
- package/lib/preact/props.js +99 -0
- package/lib/preact/props.ts +119 -0
- package/lib/preact/render.js +320 -0
- package/lib/preact/render.ts +353 -0
- package/lib/websocket.js +540 -0
- package/lib/websocket.ts +574 -0
- package/package.json +32 -0
- package/quickwin.d.ts +657 -0
- package/test/add.wasm +0 -0
- package/test/complex.wasm +0 -0
- package/test/complex_imports.wasm +0 -0
- package/test/global_imports.wasm +0 -0
- package/test/import_func.wasm +0 -0
- package/test/imports.wasm +0 -0
- package/test/run.js +86 -0
- package/test/run.ts +90 -0
- package/test/sjlj.wasm +0 -0
- package/test/test_basic.js +7 -0
- package/test/test_basic.ts +9 -0
- package/test/test_brotli.js +48 -0
- package/test/test_brotli.ts +52 -0
- package/test/test_fetch_cache.js +131 -0
- package/test/test_fetch_cache.ts +141 -0
- package/test/test_ffi.js +157 -0
- package/test/test_ffi.ts +174 -0
- package/test/test_frame_encoding.js +128 -0
- package/test/test_frame_encoding.ts +132 -0
- package/test/test_helper.js +84 -0
- package/test/test_helper.ts +80 -0
- package/test/test_http_import.js +78 -0
- package/test/test_http_import.ts +74 -0
- package/test/test_mupdf_render.js +69 -0
- package/test/test_mupdf_render.ts +74 -0
- package/test/test_mupdf_twice.js +77 -0
- package/test/test_mupdf_twice.ts +81 -0
- package/test/test_mupdf_wasm.js +33 -0
- package/test/test_mupdf_wasm.ts +30 -0
- package/test/test_net_event.js +63 -0
- package/test/test_net_event.ts +59 -0
- package/test/test_net_fetch.js +153 -0
- package/test/test_net_fetch.ts +131 -0
- package/test/test_net_websocket.js +158 -0
- package/test/test_net_websocket.ts +144 -0
- package/test/test_polyfill.js +58 -0
- package/test/test_polyfill.ts +60 -0
- package/test/test_url.js +173 -0
- package/test/test_url.ts +183 -0
- package/test/test_wasm_basic.js +82 -0
- package/test/test_wasm_basic.ts +70 -0
- package/test/test_wasm_import_global.js +41 -0
- package/test/test_wasm_import_global.ts +39 -0
- package/test/test_wasm_sjlj.js +153 -0
- package/test/test_wasm_sjlj.ts +134 -0
- package/test/test_wasm_types.js +96 -0
- package/test/test_wasm_types.ts +108 -0
- package/test/types.wasm +0 -0
- package/tsconfig.json +18 -0
- package/vendor/mupdf-wasm/mupdf-wasm.d.ts +571 -0
- package/vendor/mupdf-wasm/mupdf-wasm.js +2749 -0
- package/vendor/mupdf-wasm/mupdf-wasm.wasm +0 -0
- package/vendor/mupdf-wasm/mupdf.d.ts +939 -0
- package/vendor/mupdf-wasm/mupdf.js +3317 -0
- package/win-mingw64.exe +0 -0
package/lib/websocket.ts
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import '../lib/polyfill.js'
|
|
2
|
+
import * as sock from 'sock'
|
|
3
|
+
import * as wolfssl from 'wolfssl'
|
|
4
|
+
import * as os from 'os'
|
|
5
|
+
|
|
6
|
+
// ── Constants ──
|
|
7
|
+
|
|
8
|
+
const MAGIC_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
|
9
|
+
|
|
10
|
+
enum State {
|
|
11
|
+
CONNECTING = 0,
|
|
12
|
+
OPEN = 1,
|
|
13
|
+
CLOSING = 2,
|
|
14
|
+
CLOSED = 3,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
enum Opcode {
|
|
18
|
+
CONTINUATION = 0x0,
|
|
19
|
+
TEXT = 0x1,
|
|
20
|
+
BINARY = 0x2,
|
|
21
|
+
CLOSE = 0x8,
|
|
22
|
+
PING = 0x9,
|
|
23
|
+
PONG = 0xA,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── Random key generation ──
|
|
27
|
+
|
|
28
|
+
function generateKey(): string {
|
|
29
|
+
const bytes = new Uint8Array(16)
|
|
30
|
+
crypto.getRandomValues(bytes)
|
|
31
|
+
return btoa(String.fromCharCode(...bytes))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function computeAccept(key: string): Promise<string> {
|
|
35
|
+
const data = new TextEncoder().encode(key + MAGIC_GUID)
|
|
36
|
+
const hash = await crypto.subtle.digest('SHA-1', data)
|
|
37
|
+
const hashBytes = new Uint8Array(hash)
|
|
38
|
+
return btoa(String.fromCharCode(...hashBytes))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Frame helpers ──
|
|
42
|
+
|
|
43
|
+
function createFrame(opcode: number, payload: Uint8Array): ArrayBuffer {
|
|
44
|
+
const maskingKey = new Uint8Array(4)
|
|
45
|
+
for (let i = 0; i < 4; i++) maskingKey[i] = (Math.random() * 256) | 0
|
|
46
|
+
|
|
47
|
+
let headerSize: number
|
|
48
|
+
if (payload.length < 126) {
|
|
49
|
+
headerSize = 6
|
|
50
|
+
} else if (payload.length < 65536) {
|
|
51
|
+
headerSize = 8
|
|
52
|
+
} else {
|
|
53
|
+
headerSize = 14
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const buf = new ArrayBuffer(headerSize + payload.length)
|
|
57
|
+
const view = new Uint8Array(buf)
|
|
58
|
+
let off = 0
|
|
59
|
+
|
|
60
|
+
view[off++] = 0x80 | opcode
|
|
61
|
+
view[off++] = 0x80 | (payload.length < 126 ? payload.length :
|
|
62
|
+
payload.length < 65536 ? 126 : 127)
|
|
63
|
+
|
|
64
|
+
if (payload.length >= 126 && payload.length < 65536) {
|
|
65
|
+
view[off++] = (payload.length >> 8) & 0xFF
|
|
66
|
+
view[off++] = payload.length & 0xFF
|
|
67
|
+
} else if (payload.length >= 65536) {
|
|
68
|
+
const len = payload.length
|
|
69
|
+
const hi = Math.floor(len / 0x100000000) >>> 0
|
|
70
|
+
const lo = len >>> 0
|
|
71
|
+
view[off++] = (hi >> 24) & 0xFF
|
|
72
|
+
view[off++] = (hi >> 16) & 0xFF
|
|
73
|
+
view[off++] = (hi >> 8) & 0xFF
|
|
74
|
+
view[off++] = hi & 0xFF
|
|
75
|
+
view[off++] = (lo >> 24) & 0xFF
|
|
76
|
+
view[off++] = (lo >> 16) & 0xFF
|
|
77
|
+
view[off++] = (lo >> 8) & 0xFF
|
|
78
|
+
view[off++] = lo & 0xFF
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
view[off++] = maskingKey[0]
|
|
82
|
+
view[off++] = maskingKey[1]
|
|
83
|
+
view[off++] = maskingKey[2]
|
|
84
|
+
view[off++] = maskingKey[3]
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < payload.length; i++) {
|
|
87
|
+
view[off++] = payload[i] ^ maskingKey[i % 4]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return buf
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createCloseFrame(code: number, reason: string): ArrayBuffer {
|
|
94
|
+
const reasonBytes = new Uint8Array(reason.length + 2)
|
|
95
|
+
reasonBytes[0] = (code >> 8) & 0xFF
|
|
96
|
+
reasonBytes[1] = code & 0xFF
|
|
97
|
+
for (let i = 0; i < reason.length; i++) {
|
|
98
|
+
reasonBytes[i + 2] = reason.charCodeAt(i) & 0xFF
|
|
99
|
+
}
|
|
100
|
+
return createFrame(Opcode.CLOSE, reasonBytes)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Frame parser (returns null if incomplete) ──
|
|
104
|
+
|
|
105
|
+
interface ParsedFrame {
|
|
106
|
+
opcode: number
|
|
107
|
+
payload: Uint8Array
|
|
108
|
+
fin: boolean
|
|
109
|
+
totalLen: number
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function tryParseFrame(buffer: Uint8Array): ParsedFrame | null {
|
|
113
|
+
if (buffer.length < 2) return null
|
|
114
|
+
|
|
115
|
+
const b0 = buffer[0]
|
|
116
|
+
const b1 = buffer[1]
|
|
117
|
+
const fin = (b0 & 0x80) !== 0
|
|
118
|
+
const opcode = b0 & 0x0F
|
|
119
|
+
const masked = (b1 & 0x80) !== 0
|
|
120
|
+
let payloadLen = b1 & 0x7F
|
|
121
|
+
let offset = 2
|
|
122
|
+
|
|
123
|
+
if (payloadLen === 126) {
|
|
124
|
+
if (buffer.length < 4) return null
|
|
125
|
+
payloadLen = (buffer[2] << 8) | buffer[3]
|
|
126
|
+
offset = 4
|
|
127
|
+
} else if (payloadLen === 127) {
|
|
128
|
+
if (buffer.length < 10) return null
|
|
129
|
+
let hi = 0, lo = 0
|
|
130
|
+
for (let i = 0; i < 4; i++) hi = (hi * 256 + buffer[offset + i]) >>> 0
|
|
131
|
+
for (let i = 4; i < 8; i++) lo = (lo * 256 + buffer[offset + i]) >>> 0
|
|
132
|
+
payloadLen = hi * 0x100000000 + lo
|
|
133
|
+
offset = 10
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (masked) offset += 4
|
|
137
|
+
|
|
138
|
+
const totalLen = offset + payloadLen
|
|
139
|
+
if (buffer.length < totalLen) return null
|
|
140
|
+
|
|
141
|
+
let payload: Uint8Array
|
|
142
|
+
if (masked) {
|
|
143
|
+
const maskKey = buffer.slice(offset - 4, offset)
|
|
144
|
+
payload = new Uint8Array(payloadLen)
|
|
145
|
+
for (let i = 0; i < payloadLen; i++) {
|
|
146
|
+
payload[i] = buffer[offset + i] ^ maskKey[i % 4]
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
payload = buffer.slice(offset, offset + payloadLen)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { opcode, payload, fin, totalLen }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── WebSocket class ──
|
|
156
|
+
|
|
157
|
+
interface WSEventMap {
|
|
158
|
+
open: Event
|
|
159
|
+
message: MessageEvent
|
|
160
|
+
close: CloseEvent
|
|
161
|
+
error: Event
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
class WebSocket {
|
|
165
|
+
readonly url: string
|
|
166
|
+
readyState: number = State.CONNECTING
|
|
167
|
+
onopen: ((event: Event) => void) | null = null
|
|
168
|
+
onclose: ((event: CloseEvent) => void) | null = null
|
|
169
|
+
onerror: ((event: Event) => void) | null = null
|
|
170
|
+
onmessage: ((event: MessageEvent) => void) | null = null
|
|
171
|
+
|
|
172
|
+
private _sock: number | null = null
|
|
173
|
+
private _ssl: number | null = null
|
|
174
|
+
private _ctx: number | null = null
|
|
175
|
+
private _readBuffer: Uint8Array = new Uint8Array(0)
|
|
176
|
+
private _state: State = State.CONNECTING
|
|
177
|
+
private _resolveOpen: (() => void) | null = null
|
|
178
|
+
private _processingHandshake: boolean = false
|
|
179
|
+
static readonly CONNECTING = State.CONNECTING
|
|
180
|
+
static readonly OPEN = State.OPEN
|
|
181
|
+
static readonly CLOSING = State.CLOSING
|
|
182
|
+
static readonly CLOSED = State.CLOSED
|
|
183
|
+
|
|
184
|
+
private _closeCode: number = 1005
|
|
185
|
+
private _closeReason: string = ''
|
|
186
|
+
private _requestKey: string = ''
|
|
187
|
+
|
|
188
|
+
constructor(url: string) {
|
|
189
|
+
this.url = url
|
|
190
|
+
this._connect()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
send(data: string | ArrayBuffer | Uint8Array): void {
|
|
194
|
+
if (this._state !== State.OPEN) return
|
|
195
|
+
|
|
196
|
+
let payload: Uint8Array
|
|
197
|
+
if (typeof data === 'string') {
|
|
198
|
+
const bytes: number[] = []
|
|
199
|
+
for (let i = 0; i < data.length; i++) {
|
|
200
|
+
const c = data.charCodeAt(i)
|
|
201
|
+
if (c < 0x80) {
|
|
202
|
+
bytes.push(c)
|
|
203
|
+
} else if (c < 0x800) {
|
|
204
|
+
bytes.push(0xC0 | (c >> 6), 0x80 | (c & 0x3F))
|
|
205
|
+
} else {
|
|
206
|
+
bytes.push(0xE0 | (c >> 12), 0x80 | ((c >> 6) & 0x3F), 0x80 | (c & 0x3F))
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
payload = new Uint8Array(bytes)
|
|
210
|
+
const frame = createFrame(Opcode.TEXT, payload)
|
|
211
|
+
this._sendRaw(frame)
|
|
212
|
+
} else if (data instanceof ArrayBuffer) {
|
|
213
|
+
payload = new Uint8Array(data)
|
|
214
|
+
const frame = createFrame(Opcode.BINARY, payload)
|
|
215
|
+
this._sendRaw(frame)
|
|
216
|
+
} else if (data instanceof Uint8Array) {
|
|
217
|
+
const frame = createFrame(Opcode.BINARY, data)
|
|
218
|
+
this._sendRaw(frame)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
close(code?: number, reason?: string): void {
|
|
223
|
+
if (this._state === State.CLOSED || this._state === State.CLOSING) return
|
|
224
|
+
this._state = State.CLOSING
|
|
225
|
+
|
|
226
|
+
const closeCode = code || 1000
|
|
227
|
+
const closeReason = reason || ''
|
|
228
|
+
this._closeCode = closeCode
|
|
229
|
+
this._closeReason = closeReason
|
|
230
|
+
const frame = createCloseFrame(closeCode, closeReason)
|
|
231
|
+
this._sendRaw(frame)
|
|
232
|
+
|
|
233
|
+
this._cleanup()
|
|
234
|
+
this._setState(State.CLOSED)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private _connect(): void {
|
|
238
|
+
let url: URL
|
|
239
|
+
try { url = new URL(this.url) } catch {
|
|
240
|
+
const self = this
|
|
241
|
+
os.setTimeout(() => {
|
|
242
|
+
self._fireError(new Error('Invalid WebSocket URL: ' + self.url))
|
|
243
|
+
self._setState(State.CLOSED)
|
|
244
|
+
}, 0)
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
const host = url.hostname
|
|
248
|
+
const isWSS = url.protocol === 'wss:'
|
|
249
|
+
const port = url.port ? parseInt(url.port, 10) : (isWSS ? 443 : 80)
|
|
250
|
+
const path = url.pathname
|
|
251
|
+
|
|
252
|
+
const requestKey = generateKey()
|
|
253
|
+
this._requestKey = requestKey
|
|
254
|
+
|
|
255
|
+
const request = (
|
|
256
|
+
'GET ' + path + ' HTTP/1.1\r\n' +
|
|
257
|
+
'Host: ' + host + ':' + port + '\r\n' +
|
|
258
|
+
'Upgrade: websocket\r\n' +
|
|
259
|
+
'Connection: Upgrade\r\n' +
|
|
260
|
+
'Sec-WebSocket-Key: ' + requestKey + '\r\n' +
|
|
261
|
+
'Sec-WebSocket-Version: 13\r\n' +
|
|
262
|
+
'\r\n'
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
let state: 'resolve' | 'connect' | 'handshake' | 'request' | 'open' = 'resolve'
|
|
266
|
+
let headerBuffer = ''
|
|
267
|
+
let resolved = false
|
|
268
|
+
|
|
269
|
+
const cleanup = (): void => {
|
|
270
|
+
if (this._ssl) { wolfssl.wolfSSL_free(this._ssl); this._ssl = null }
|
|
271
|
+
if (this._ctx) { wolfssl.wolfSSL_CTX_free(this._ctx); this._ctx = null }
|
|
272
|
+
if (this._sock) { sock.closesocket(this._sock); this._sock = null }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const doError = (err: Error): void => {
|
|
276
|
+
if (resolved) return
|
|
277
|
+
resolved = true
|
|
278
|
+
cleanup()
|
|
279
|
+
this._fireError(err)
|
|
280
|
+
if (this._state !== State.CLOSED) this._setState(State.CLOSED)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this._sock = sock.socket()
|
|
284
|
+
if (!this._sock) {
|
|
285
|
+
const self = this
|
|
286
|
+
os.setTimeout(() => {
|
|
287
|
+
self._fireError(new Error('Failed to create socket'))
|
|
288
|
+
if (self._state !== State.CLOSED) self._setState(State.CLOSED)
|
|
289
|
+
}, 0)
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
const fd = this._sock
|
|
293
|
+
|
|
294
|
+
sock.set_on_event(fd, async (event: { lNetworkEvents: number; iErrorCode: number[] }) => {
|
|
295
|
+
if (this._processingHandshake) return
|
|
296
|
+
if (resolved && state === 'open') {
|
|
297
|
+
if (event.lNetworkEvents & sock.FD_READ) {
|
|
298
|
+
this._onData()
|
|
299
|
+
}
|
|
300
|
+
if (event.lNetworkEvents & sock.FD_CLOSE) {
|
|
301
|
+
this._cleanup()
|
|
302
|
+
this._setState(State.CLOSED)
|
|
303
|
+
}
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (event.lNetworkEvents & sock.FD_CONNECT) {
|
|
308
|
+
const errCode = event.iErrorCode[0]
|
|
309
|
+
if (errCode !== 0) {
|
|
310
|
+
doError(new Error('Connection failed: ' + errCode))
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
if (isWSS) {
|
|
314
|
+
const method = wolfssl.wolfTLSv1_2_client_method()
|
|
315
|
+
this._ctx = wolfssl.wolfSSL_CTX_new(method)
|
|
316
|
+
wolfssl.wolfSSL_CTX_set_verify(this._ctx, wolfssl.SSL_VERIFY_NONE)
|
|
317
|
+
this._ssl = wolfssl.wolfSSL_new(this._ctx)
|
|
318
|
+
if (!this._ssl) {
|
|
319
|
+
doError(new Error('SSL_new failed'))
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
wolfssl.wolfSSL_set_fd(this._ssl, sock.get_fd(fd))
|
|
323
|
+
const sniHost = host
|
|
324
|
+
if (sniHost) wolfssl.wolfSSL_UseSNI(this._ssl, wolfssl.WOLFSSL_SNI_HOST_NAME, sniHost)
|
|
325
|
+
state = 'handshake'
|
|
326
|
+
} else {
|
|
327
|
+
this._sendRawStr(request)
|
|
328
|
+
state = 'request'
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if ((event.lNetworkEvents & sock.FD_READ) || (event.lNetworkEvents & sock.FD_WRITE)) {
|
|
333
|
+
if (state === 'handshake') {
|
|
334
|
+
if (!this._ssl) { doError(new Error('TLS not initialized')); return }
|
|
335
|
+
const ret = wolfssl.wolfSSL_connect(this._ssl)
|
|
336
|
+
if (ret === wolfssl.SSL_SUCCESS) {
|
|
337
|
+
this._sendRawStr(request)
|
|
338
|
+
state = 'request'
|
|
339
|
+
} else {
|
|
340
|
+
const err = wolfssl.wolfSSL_get_error(this._ssl, ret)
|
|
341
|
+
if (err !== wolfssl.WOLFSSL_ERROR_WANT_READ &&
|
|
342
|
+
err !== wolfssl.WOLFSSL_ERROR_WANT_WRITE) {
|
|
343
|
+
doError(new Error('TLS handshake failed: ' + err))
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
else if (state === 'request') {
|
|
348
|
+
while (true) {
|
|
349
|
+
let data: ArrayBuffer | null
|
|
350
|
+
if (isWSS && this._ssl) {
|
|
351
|
+
data = wolfssl.wolfSSL_read(this._ssl, 8192)
|
|
352
|
+
} else if (this._sock) {
|
|
353
|
+
data = sock.recv(this._sock, 8192)
|
|
354
|
+
} else { break }
|
|
355
|
+
if (!data || data.byteLength === 0) break
|
|
356
|
+
headerBuffer += _ab2str(data)
|
|
357
|
+
const headerEnd = headerBuffer.indexOf('\r\n\r\n')
|
|
358
|
+
if (headerEnd >= 0) {
|
|
359
|
+
const headerPart = headerBuffer.slice(0, headerEnd)
|
|
360
|
+
const lines = headerPart.split('\r\n')
|
|
361
|
+
const statusLine = lines[0]
|
|
362
|
+
const statusParts = statusLine.split(' ')
|
|
363
|
+
const statusCode = parseInt(statusParts[1], 10)
|
|
364
|
+
if (statusCode !== 101) {
|
|
365
|
+
doError(new Error('WebSocket handshake failed: HTTP ' + statusCode))
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let acceptHeader = ''
|
|
370
|
+
for (let i = 1; i < lines.length; i++) {
|
|
371
|
+
const line = lines[i]
|
|
372
|
+
const colonIdx2 = line.indexOf(':')
|
|
373
|
+
if (colonIdx2 >= 0) {
|
|
374
|
+
const name = line.slice(0, colonIdx2).toLowerCase().trim()
|
|
375
|
+
if (name === 'sec-websocket-accept') {
|
|
376
|
+
acceptHeader = line.slice(colonIdx2 + 1).trim()
|
|
377
|
+
break
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (!acceptHeader) {
|
|
382
|
+
doError(new Error('WebSocket handshake missing Sec-WebSocket-Accept'))
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
this._processingHandshake = true
|
|
387
|
+
const expectedAccept = await computeAccept(requestKey)
|
|
388
|
+
if (acceptHeader !== expectedAccept) {
|
|
389
|
+
doError(new Error('WebSocket handshake invalid Sec-WebSocket-Accept'))
|
|
390
|
+
this._processingHandshake = false
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
state = 'open'
|
|
395
|
+
resolved = true
|
|
396
|
+
this._processingHandshake = false
|
|
397
|
+
this._setState(State.OPEN)
|
|
398
|
+
break
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (event.lNetworkEvents & sock.FD_CLOSE) {
|
|
405
|
+
if (state !== 'open') {
|
|
406
|
+
doError(new Error('Connection closed'))
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
const ip = sock.resolve(host)
|
|
412
|
+
if (!ip) {
|
|
413
|
+
const self = this
|
|
414
|
+
os.setTimeout(() => {
|
|
415
|
+
self._fireError(new Error('DNS resolution failed for: ' + host))
|
|
416
|
+
if (self._state !== State.CLOSED) self._setState(State.CLOSED)
|
|
417
|
+
}, 0)
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
sock.connect(fd, ip, port)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private _sendRaw(buf: ArrayBuffer): void {
|
|
424
|
+
if (!this._sock) return
|
|
425
|
+
if (this._ssl) {
|
|
426
|
+
wolfssl.wolfSSL_write(this._ssl, buf)
|
|
427
|
+
} else {
|
|
428
|
+
sock.send(this._sock, buf)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private _sendRawStr(str: string): void {
|
|
433
|
+
const buf = new ArrayBuffer(str.length)
|
|
434
|
+
const view = new Uint8Array(buf)
|
|
435
|
+
for (let i = 0; i < str.length; i++) view[i] = str.charCodeAt(i) & 0xFF
|
|
436
|
+
this._sendRaw(buf)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private _onData(): void {
|
|
440
|
+
while (true) {
|
|
441
|
+
let data: ArrayBuffer | null
|
|
442
|
+
if (this._ssl) {
|
|
443
|
+
data = wolfssl.wolfSSL_read(this._ssl, 8192)
|
|
444
|
+
} else if (this._sock) {
|
|
445
|
+
data = sock.recv(this._sock, 8192)
|
|
446
|
+
} else { break }
|
|
447
|
+
if (!data || data.byteLength === 0) break
|
|
448
|
+
|
|
449
|
+
const newBuf = new Uint8Array(this._readBuffer.length + data.byteLength)
|
|
450
|
+
newBuf.set(this._readBuffer)
|
|
451
|
+
newBuf.set(new Uint8Array(data), this._readBuffer.length)
|
|
452
|
+
this._readBuffer = newBuf
|
|
453
|
+
|
|
454
|
+
while (true) {
|
|
455
|
+
const frame = tryParseFrame(this._readBuffer)
|
|
456
|
+
if (!frame) break
|
|
457
|
+
|
|
458
|
+
this._readBuffer = this._readBuffer.slice(frame.totalLen)
|
|
459
|
+
|
|
460
|
+
if (frame.opcode === Opcode.TEXT || frame.opcode === Opcode.BINARY) {
|
|
461
|
+
const event = new MessageEvent('message', { data: frame.opcode === Opcode.TEXT ? _ab2str(frame.payload.slice(0).buffer) : frame.payload.slice(0).buffer })
|
|
462
|
+
this._dispatchEvent('message', event)
|
|
463
|
+
} else if (frame.opcode === Opcode.PING) {
|
|
464
|
+
const pong = createFrame(Opcode.PONG, frame.payload)
|
|
465
|
+
this._sendRaw(pong)
|
|
466
|
+
} else if (frame.opcode === Opcode.PONG) {
|
|
467
|
+
} else if (frame.opcode === Opcode.CLOSE) {
|
|
468
|
+
if (frame.payload.length >= 2) {
|
|
469
|
+
this._closeCode = (frame.payload[0] << 8) | frame.payload[1]
|
|
470
|
+
this._closeReason = _ab2str(frame.payload.slice(2).buffer)
|
|
471
|
+
}
|
|
472
|
+
this._cleanup()
|
|
473
|
+
this._setState(State.CLOSED)
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private _setState(state: State): void {
|
|
481
|
+
if (this._state === state) return
|
|
482
|
+
this.readyState = state
|
|
483
|
+
this._state = state
|
|
484
|
+
|
|
485
|
+
if (state === State.OPEN) {
|
|
486
|
+
this._dispatchEvent('open', new Event('open'))
|
|
487
|
+
} else if (state === State.CLOSED) {
|
|
488
|
+
const event = new CloseEvent('close', { code: this._closeCode, reason: this._closeReason, wasClean: true })
|
|
489
|
+
this._dispatchEvent('close', event)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private _dispatchEvent(type: string, event: Event | MessageEvent | CloseEvent): void {
|
|
494
|
+
if (type === 'open' && this.onopen) this.onopen(event as Event)
|
|
495
|
+
else if (type === 'close' && this.onclose) this.onclose(event as CloseEvent)
|
|
496
|
+
else if (type === 'error' && this.onerror) this.onerror(event as Event)
|
|
497
|
+
else if (type === 'message' && this.onmessage) this.onmessage(event as MessageEvent)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private _fireError(err: Error): void {
|
|
501
|
+
this._dispatchEvent('error', new Event('error'))
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private _cleanup(): void {
|
|
505
|
+
if (this._ssl) { wolfssl.wolfSSL_free(this._ssl); this._ssl = null }
|
|
506
|
+
if (this._ctx) { wolfssl.wolfSSL_CTX_free(this._ctx); this._ctx = null }
|
|
507
|
+
if (this._sock) { sock.closesocket(this._sock); this._sock = null }
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ── Event/MessageEvent/CloseEvent polyfills ──
|
|
512
|
+
|
|
513
|
+
class Event {
|
|
514
|
+
readonly type: string
|
|
515
|
+
constructor(type: string, _init?: any) { this.type = type }
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
class MessageEvent {
|
|
519
|
+
readonly type: string
|
|
520
|
+
readonly data: any
|
|
521
|
+
constructor(type: string, init: { data: any }) {
|
|
522
|
+
this.type = type
|
|
523
|
+
this.data = init.data
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
class CloseEvent {
|
|
528
|
+
readonly type: string
|
|
529
|
+
readonly code: number
|
|
530
|
+
readonly reason: string
|
|
531
|
+
readonly wasClean: boolean
|
|
532
|
+
constructor(type: string, init: { code?: number; reason?: string; wasClean?: boolean }) {
|
|
533
|
+
this.type = type
|
|
534
|
+
this.code = init.code || 0
|
|
535
|
+
this.reason = init.reason || ''
|
|
536
|
+
this.wasClean = init.wasClean || false
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ── Utility ──
|
|
541
|
+
|
|
542
|
+
function _ab2str(buf: ArrayBuffer): string {
|
|
543
|
+
const view = new Uint8Array(buf)
|
|
544
|
+
let str = ''
|
|
545
|
+
for (let i = 0; i < view.length; i++) str += String.fromCharCode(view[i])
|
|
546
|
+
return str
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ── Global declarations ──
|
|
550
|
+
|
|
551
|
+
declare global {
|
|
552
|
+
interface WebSocket {
|
|
553
|
+
url: string
|
|
554
|
+
readyState: number
|
|
555
|
+
onopen: ((event: Event) => void) | null
|
|
556
|
+
onclose: ((event: CloseEvent) => void) | null
|
|
557
|
+
onerror: ((event: Event) => void) | null
|
|
558
|
+
onmessage: ((event: MessageEvent) => void) | null
|
|
559
|
+
send(data: string | ArrayBuffer | Uint8Array): void
|
|
560
|
+
close(code?: number, reason?: string): void
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
var WebSocket: {
|
|
564
|
+
new(url: string): WebSocket
|
|
565
|
+
readonly CONNECTING: number
|
|
566
|
+
readonly OPEN: number
|
|
567
|
+
readonly CLOSING: number
|
|
568
|
+
readonly CLOSED: number
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ── Register globals ──
|
|
573
|
+
|
|
574
|
+
globalThis.WebSocket = WebSocket
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "quickwin",
|
|
3
|
+
"version": "2026.5.2-3.145209",
|
|
4
|
+
"description": "QuickJS Win32 runtime — GUI, networking, WASM, FFI, and more",
|
|
5
|
+
"bin": {
|
|
6
|
+
"quickwin": "win-mingw64.exe"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"win-mingw64.exe",
|
|
10
|
+
"lib/",
|
|
11
|
+
"test/",
|
|
12
|
+
"examples/",
|
|
13
|
+
"vendor/",
|
|
14
|
+
"quickwin.d.ts",
|
|
15
|
+
"tsconfig.json",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"os": [
|
|
19
|
+
"win32"
|
|
20
|
+
],
|
|
21
|
+
"cpu": [
|
|
22
|
+
"x64"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/anomalyco/quickwin.git"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@typescript/native-preview": "^7.0.0-dev.20260426.1"
|
|
31
|
+
}
|
|
32
|
+
}
|