quickwin 1.3.0 → 2.0.0
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 +1 -2
- package/README.md +19 -23
- package/examples/PdfCanvas.js +8 -37
- package/examples/PdfCanvas.tsx +11 -41
- package/examples/exec_server.js +20054 -1423
- package/examples/exec_server.tsx +489 -0
- package/examples/exec_server_worker.js +383 -33
- package/examples/exec_server_worker.ts +408 -30
- package/examples/pdf_preview2.js +25 -82
- package/examples/pdf_preview2.ts +27 -84
- package/examples/pdf_viewer.js +1318 -1178
- package/examples/pdf_viewer.tsx +16 -67
- package/examples/setres.js +22 -47
- package/examples/setres.ts +23 -57
- package/examples/test_gallery.js +1313 -1074
- package/examples/test_react_lazy.js +1284 -1056
- package/examples/test_react_listbox.js +1283 -1055
- package/examples/test_root_window.js +1279 -1051
- package/examples/test_tab.js +1284 -1056
- package/lib/abort.js +70 -0
- package/lib/abort.ts +69 -0
- package/lib/certs.js +214 -0
- package/lib/certs.ts +224 -0
- package/lib/exec-log.js +15 -0
- package/lib/exec-log.ts +30 -0
- package/lib/fetch.js +37 -4
- package/lib/fetch.ts +35 -4
- package/lib/ffi/bind.js +352 -0
- package/lib/ffi/bind.ts +442 -0
- package/lib/ffi/ctype.js +168 -0
- package/lib/ffi/ctype.ts +331 -0
- package/lib/ffi/index.js +6 -0
- package/lib/ffi/index.ts +6 -0
- package/lib/ffi/struct.js +412 -0
- package/lib/ffi/struct.ts +664 -0
- package/lib/http-common.js +43 -5
- package/lib/http-common.ts +48 -6
- package/lib/http-server.js +187 -29
- package/lib/http-server.ts +177 -26
- package/lib/react-qw/components/DateTimePicker.js +18 -37
- package/lib/react-qw/components/Input.js +3 -10
- package/lib/react-qw/components/Link.js +6 -19
- package/lib/react-qw/components/ListView.js +49 -154
- package/lib/react-qw/components/PathPicker.js +28 -88
- package/lib/react-qw/components/ScrollView.js +15 -8
- package/lib/react-qw/components/Slider.js +2 -1
- package/lib/react-qw/components/Tab.js +6 -22
- package/lib/react-qw/components/Tooltip.js +10 -21
- package/lib/react-qw/components/TreeView.js +8 -31
- package/lib/react-qw/index.js +4 -2
- package/lib/react-qw/index.ts +4 -2
- package/lib/react-qw/nmhdr.js +3 -24
- package/lib/react-qw/nmhdr.ts +5 -30
- package/lib/react-qw/props.js +7 -2
- package/lib/react-qw/props.ts +7 -3
- package/lib/react-qw/reconciler.js +23 -13
- package/lib/react-qw/reconciler.ts +22 -13
- package/lib/text-measure.js +18 -23
- package/lib/text-measure.ts +20 -25
- package/lib/websocket.js +12 -2
- package/lib/websocket.ts +15 -2
- package/lib/windows/comctl32.js +18 -0
- package/lib/windows/comctl32.ts +20 -0
- package/lib/windows/comdlg32.js +7 -0
- package/lib/windows/comdlg32.ts +9 -0
- package/lib/windows/gdi32.js +90 -0
- package/lib/windows/gdi32.ts +97 -0
- package/lib/windows/kernel32.js +66 -0
- package/lib/windows/kernel32.ts +76 -0
- package/lib/windows/ntdll.js +8 -0
- package/lib/windows/ntdll.ts +10 -0
- package/lib/windows/ole32.js +7 -0
- package/lib/windows/ole32.ts +9 -0
- package/lib/windows/shell32.js +11 -0
- package/lib/windows/shell32.ts +14 -0
- package/lib/windows/structs.js +384 -0
- package/lib/windows/structs.ts +422 -0
- package/lib/windows/user32.js +469 -0
- package/lib/windows/user32.ts +502 -0
- package/package.json +1 -1
- package/quickwin.d.ts +22 -37
- package/quickwin_const.d.ts +1 -0
- 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/test/bench_ffi_callraw.js +236 -0
- package/test/bench_ffi_callraw.ts +236 -0
- package/test/bench_ffi_overhead.js +139 -0
- package/test/bench_ffi_overhead.ts +141 -0
- package/test/bench_sendmessage.js +104 -0
- package/test/bench_sendmessage.ts +100 -0
- package/test/bench_sendmessage_nonnull.js +187 -0
- package/test/bench_sendmessage_nonnull.ts +188 -0
- package/test/run.js +12 -4
- package/test/run.ts +13 -5
- package/test/test_basic_fetch.js +8 -0
- package/test/test_basic_fetch.ts +9 -0
- package/test/test_ffi.js +25 -43
- package/test/test_ffi.ts +33 -52
- package/test/test_ffi_abi.js +235 -0
- package/test/test_ffi_abi.ts +218 -0
- package/test/test_ffi_bind.js +538 -8
- package/test/test_ffi_bind.ts +559 -8
- package/test/test_ffi_struct.js +632 -88
- package/test/test_ffi_struct.ts +666 -91
- package/test/test_ffi_struct_layout.js +495 -0
- package/test/test_ffi_struct_layout.ts +543 -0
- package/test/test_ffi_user32.js +103 -0
- package/test/test_ffi_user32.ts +113 -0
- package/test/test_ffi_windows.js +101 -0
- package/test/test_ffi_windows.ts +113 -0
- package/test/test_helper.js +8 -1
- package/test/test_helper.ts +9 -1
- package/test/test_http_import.js +18 -0
- package/test/test_http_import.ts +19 -0
- package/test/test_http_server.js +73 -1
- package/test/test_http_server.ts +75 -1
- package/test/test_ipv6.js +2 -2
- package/test/test_ipv6.ts +2 -2
- package/test/test_net_fetch.js +70 -8
- package/test/test_net_fetch.ts +69 -7
- package/test/test_net_websocket.js +1 -1
- package/test/test_net_websocket.ts +1 -1
- package/test/test_react_complex.js +1294 -1066
- package/test/test_react_counter.js +1284 -1056
- package/test/test_react_flex.js +1304 -1069
- package/test/test_react_hidden.js +1287 -1059
- package/test/test_react_listview_link.js +1284 -1056
- package/test/test_react_notify.js +18534 -0
- package/test/test_react_render.js +1284 -1056
- package/test/test_wolfssl_read.js +3 -2
- package/test/test_wolfssl_read.ts +3 -1
- package/test/test_worker_net.js +35 -17
- package/test/test_worker_net.ts +27 -15
- package/tsconfig.json +1 -1
- package/examples/exec_server.ts +0 -80
- package/lib/ffi-bind.js +0 -85
- package/lib/ffi-bind.ts +0 -170
- package/lib/ffi-struct.js +0 -224
- package/lib/ffi-struct.ts +0 -239
- package/lib/win-common-structs.js +0 -39
- package/lib/win-common-structs.ts +0 -41
package/lib/http-common.js
CHANGED
|
@@ -193,7 +193,8 @@ export class RequestImpl {
|
|
|
193
193
|
credentials = 'same-origin';
|
|
194
194
|
referrer = 'about:client';
|
|
195
195
|
referrerPolicy = '';
|
|
196
|
-
signal
|
|
196
|
+
signal;
|
|
197
|
+
rejectUnauthorized;
|
|
197
198
|
destination = '';
|
|
198
199
|
get body() { return this._body; }
|
|
199
200
|
get bodyUsed() {
|
|
@@ -214,6 +215,9 @@ export class RequestImpl {
|
|
|
214
215
|
this.redirect = init.redirect || input.redirect;
|
|
215
216
|
this.timeout = init.timeout || input.timeout;
|
|
216
217
|
this.maxRedirects = init.maxRedirects || input.maxRedirects;
|
|
218
|
+
this.rejectUnauthorized = init.rejectUnauthorized === undefined
|
|
219
|
+
? input.rejectUnauthorized
|
|
220
|
+
: init.rejectUnauthorized;
|
|
217
221
|
}
|
|
218
222
|
else {
|
|
219
223
|
this.url = input;
|
|
@@ -223,7 +227,11 @@ export class RequestImpl {
|
|
|
223
227
|
this.redirect = init.redirect || 'follow';
|
|
224
228
|
this.timeout = init.timeout || 30000;
|
|
225
229
|
this.maxRedirects = init.maxRedirects || 5;
|
|
230
|
+
this.rejectUnauthorized = init.rejectUnauthorized ?? true;
|
|
226
231
|
}
|
|
232
|
+
this.signal = init.signal !== undefined
|
|
233
|
+
? init.signal
|
|
234
|
+
: (input instanceof RequestImpl ? input.signal : null);
|
|
227
235
|
}
|
|
228
236
|
async arrayBuffer() {
|
|
229
237
|
if (!this.body)
|
|
@@ -259,6 +267,7 @@ export class RequestImpl {
|
|
|
259
267
|
redirect: this.redirect,
|
|
260
268
|
timeout: this.timeout,
|
|
261
269
|
maxRedirects: this.maxRedirects,
|
|
270
|
+
signal: this.signal,
|
|
262
271
|
});
|
|
263
272
|
return req;
|
|
264
273
|
}
|
|
@@ -268,6 +277,7 @@ export class RequestImpl {
|
|
|
268
277
|
redirect: this.redirect,
|
|
269
278
|
timeout: this.timeout,
|
|
270
279
|
maxRedirects: this.maxRedirects,
|
|
280
|
+
signal: this.signal,
|
|
271
281
|
});
|
|
272
282
|
}
|
|
273
283
|
}
|
|
@@ -282,8 +292,12 @@ export class ResponseImpl {
|
|
|
282
292
|
_url = '';
|
|
283
293
|
_body;
|
|
284
294
|
_bodyConsumed = false;
|
|
295
|
+
_trailers;
|
|
285
296
|
get body() { return this._body; }
|
|
286
297
|
get bodyUsed() { return this._bodyConsumed || this._body.locked; }
|
|
298
|
+
/** Trailing headers to emit after a chunked body (Promise resolves to
|
|
299
|
+
* a HeadersInit), when a `Trailer` declaration header is present. */
|
|
300
|
+
get trailers() { return this._trailers; }
|
|
287
301
|
get headers() { return this._headers; }
|
|
288
302
|
get redirected() { return this._redirected; }
|
|
289
303
|
get type() { return this._type; }
|
|
@@ -295,10 +309,34 @@ export class ResponseImpl {
|
|
|
295
309
|
this.status = status;
|
|
296
310
|
this.statusText = statusText;
|
|
297
311
|
this._headers = headers;
|
|
312
|
+
this._trailers = init?.trailers;
|
|
298
313
|
this.ok = status >= 200 && status < 300;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
314
|
+
if (body instanceof ReadableStream) {
|
|
315
|
+
this._body = body;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
let bytes;
|
|
319
|
+
if (body == null) {
|
|
320
|
+
bytes = new Uint8Array(0);
|
|
321
|
+
}
|
|
322
|
+
else if (typeof body === 'string') {
|
|
323
|
+
bytes = new TextEncoder().encode(body);
|
|
324
|
+
}
|
|
325
|
+
else if (body instanceof ArrayBuffer) {
|
|
326
|
+
bytes = new Uint8Array(body);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
bytes = new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
330
|
+
}
|
|
331
|
+
if (!headers.has('content-length'))
|
|
332
|
+
headers.set('Content-Length', String(bytes.byteLength));
|
|
333
|
+
this._body = new ReadableStream({
|
|
334
|
+
start(ctrl) {
|
|
335
|
+
ctrl.enqueue(bytes);
|
|
336
|
+
ctrl.close();
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
302
340
|
}
|
|
303
341
|
async text() {
|
|
304
342
|
if (this.bodyUsed)
|
|
@@ -328,7 +366,7 @@ export class ResponseImpl {
|
|
|
328
366
|
throw new TypeError('Body already used');
|
|
329
367
|
const [branch1, branch2] = this._body.tee();
|
|
330
368
|
this._body = branch1;
|
|
331
|
-
return new ResponseImpl(branch2, { status: this.status, statusText: this.statusText, headers: this._headers });
|
|
369
|
+
return new ResponseImpl(branch2, { status: this.status, statusText: this.statusText, headers: this._headers, trailers: this._trailers });
|
|
332
370
|
}
|
|
333
371
|
_applyDecompressedBody(stream, _body, newHeaders) {
|
|
334
372
|
this._bodyConsumed = false;
|
package/lib/http-common.ts
CHANGED
|
@@ -188,7 +188,8 @@ export class RequestImpl {
|
|
|
188
188
|
readonly credentials: string = 'same-origin'
|
|
189
189
|
readonly referrer: string = 'about:client'
|
|
190
190
|
readonly referrerPolicy: string = ''
|
|
191
|
-
readonly signal: AbortSignal | null
|
|
191
|
+
readonly signal: AbortSignal | null
|
|
192
|
+
readonly rejectUnauthorized: boolean
|
|
192
193
|
readonly destination: string = ''
|
|
193
194
|
|
|
194
195
|
get body(): ReadableStream<Uint8Array> | null { return this._body }
|
|
@@ -211,6 +212,9 @@ export class RequestImpl {
|
|
|
211
212
|
this.redirect = init.redirect || input.redirect
|
|
212
213
|
this.timeout = init.timeout || input.timeout
|
|
213
214
|
this.maxRedirects = init.maxRedirects || input.maxRedirects
|
|
215
|
+
this.rejectUnauthorized = init.rejectUnauthorized === undefined
|
|
216
|
+
? input.rejectUnauthorized
|
|
217
|
+
: init.rejectUnauthorized
|
|
214
218
|
} else {
|
|
215
219
|
this.url = input
|
|
216
220
|
this.method = init.method || 'GET'
|
|
@@ -219,7 +223,11 @@ export class RequestImpl {
|
|
|
219
223
|
this.redirect = init.redirect || 'follow'
|
|
220
224
|
this.timeout = init.timeout || 30000
|
|
221
225
|
this.maxRedirects = init.maxRedirects || 5
|
|
226
|
+
this.rejectUnauthorized = init.rejectUnauthorized ?? true
|
|
222
227
|
}
|
|
228
|
+
this.signal = init.signal !== undefined
|
|
229
|
+
? init.signal
|
|
230
|
+
: (input instanceof RequestImpl ? input.signal : null)
|
|
223
231
|
}
|
|
224
232
|
|
|
225
233
|
async arrayBuffer(): Promise<ArrayBuffer> {
|
|
@@ -254,6 +262,7 @@ export class RequestImpl {
|
|
|
254
262
|
redirect: this.redirect,
|
|
255
263
|
timeout: this.timeout,
|
|
256
264
|
maxRedirects: this.maxRedirects,
|
|
265
|
+
signal: this.signal,
|
|
257
266
|
})
|
|
258
267
|
return req
|
|
259
268
|
}
|
|
@@ -263,6 +272,7 @@ export class RequestImpl {
|
|
|
263
272
|
redirect: this.redirect,
|
|
264
273
|
timeout: this.timeout,
|
|
265
274
|
maxRedirects: this.maxRedirects,
|
|
275
|
+
signal: this.signal,
|
|
266
276
|
})
|
|
267
277
|
}
|
|
268
278
|
}
|
|
@@ -280,9 +290,13 @@ export class ResponseImpl {
|
|
|
280
290
|
_url: string = ''
|
|
281
291
|
private _body: ReadableStream<Uint8Array>
|
|
282
292
|
private _bodyConsumed: boolean = false
|
|
293
|
+
private _trailers: Promise<HeadersInit> | undefined
|
|
283
294
|
|
|
284
295
|
get body(): ReadableStream<Uint8Array> { return this._body }
|
|
285
296
|
get bodyUsed(): boolean { return this._bodyConsumed || this._body.locked }
|
|
297
|
+
/** Trailing headers to emit after a chunked body (Promise resolves to
|
|
298
|
+
* a HeadersInit), when a `Trailer` declaration header is present. */
|
|
299
|
+
get trailers(): Promise<HeadersInit> | undefined { return this._trailers }
|
|
286
300
|
get headers(): HeadersImpl { return this._headers }
|
|
287
301
|
get redirected(): boolean { return this._redirected }
|
|
288
302
|
get type(): ResponseType { return this._type }
|
|
@@ -295,10 +309,29 @@ export class ResponseImpl {
|
|
|
295
309
|
this.status = status
|
|
296
310
|
this.statusText = statusText
|
|
297
311
|
this._headers = headers
|
|
312
|
+
this._trailers = init?.trailers
|
|
298
313
|
this.ok = status >= 200 && status < 300
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
314
|
+
if (body instanceof ReadableStream) {
|
|
315
|
+
this._body = body
|
|
316
|
+
} else {
|
|
317
|
+
let bytes: Uint8Array
|
|
318
|
+
if (body == null) {
|
|
319
|
+
bytes = new Uint8Array(0)
|
|
320
|
+
} else if (typeof body === 'string') {
|
|
321
|
+
bytes = new TextEncoder().encode(body)
|
|
322
|
+
} else if (body instanceof ArrayBuffer) {
|
|
323
|
+
bytes = new Uint8Array(body)
|
|
324
|
+
} else {
|
|
325
|
+
bytes = new Uint8Array(body.buffer, body.byteOffset, body.byteLength)
|
|
326
|
+
}
|
|
327
|
+
if (!headers.has('content-length')) headers.set('Content-Length', String(bytes.byteLength))
|
|
328
|
+
this._body = new ReadableStream<Uint8Array>({
|
|
329
|
+
start(ctrl) {
|
|
330
|
+
ctrl.enqueue(bytes)
|
|
331
|
+
ctrl.close()
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
}
|
|
302
335
|
}
|
|
303
336
|
|
|
304
337
|
async text(): Promise<string> {
|
|
@@ -329,7 +362,7 @@ export class ResponseImpl {
|
|
|
329
362
|
if (this.bodyUsed) throw new TypeError('Body already used')
|
|
330
363
|
const [branch1, branch2] = this._body.tee()
|
|
331
364
|
this._body = branch1
|
|
332
|
-
return new ResponseImpl(branch2, { status: this.status, statusText: this.statusText, headers: this._headers })
|
|
365
|
+
return new ResponseImpl(branch2, { status: this.status, statusText: this.statusText, headers: this._headers, trailers: this._trailers })
|
|
333
366
|
}
|
|
334
367
|
|
|
335
368
|
_applyDecompressedBody(stream: ReadableStream<Uint8Array>, _body: ArrayBuffer, newHeaders: HeadersImpl): void {
|
|
@@ -353,7 +386,9 @@ declare global {
|
|
|
353
386
|
interface AbortSignal {
|
|
354
387
|
readonly aborted: boolean;
|
|
355
388
|
readonly reason: unknown;
|
|
356
|
-
onabort: ((event:
|
|
389
|
+
onabort: ((event: { type: 'abort'; target: unknown }) => void) | null;
|
|
390
|
+
addEventListener(type: 'abort', listener: (e: { type: 'abort'; target: unknown }) => void): void;
|
|
391
|
+
removeEventListener(type: 'abort', listener: (e: { type: 'abort'; target: unknown }) => void): void;
|
|
357
392
|
throwIfAborted(): void;
|
|
358
393
|
}
|
|
359
394
|
|
|
@@ -373,12 +408,19 @@ declare global {
|
|
|
373
408
|
window?: null;
|
|
374
409
|
timeout?: number;
|
|
375
410
|
maxRedirects?: number;
|
|
411
|
+
/** Non-standard: when false, TLS certificate verification is skipped
|
|
412
|
+
* (mirrors Node's https.request rejectUnauthorized option). */
|
|
413
|
+
rejectUnauthorized?: boolean;
|
|
376
414
|
}
|
|
377
415
|
|
|
378
416
|
interface ResponseInit {
|
|
379
417
|
status?: number;
|
|
380
418
|
statusText?: string;
|
|
381
419
|
headers?: HeadersInit;
|
|
420
|
+
/** Non-standard: trailing headers, resolved after the body streams.
|
|
421
|
+
* http-server emits them (Node addTrailers semantics) when a `Trailer`
|
|
422
|
+
* declaration header is present and the body is chunk-framed. */
|
|
423
|
+
trailers?: Promise<HeadersInit>;
|
|
382
424
|
}
|
|
383
425
|
|
|
384
426
|
interface Headers extends HeadersImpl {}
|
package/lib/http-server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import './polyfill.js';
|
|
2
2
|
import * as sock from 'sock';
|
|
3
3
|
import { HeadersImpl, RequestImpl, ResponseImpl, _readStream } from './http-common.js';
|
|
4
|
+
import { AbortController } from './abort.js';
|
|
4
5
|
const STATUS_TEXT = {
|
|
5
6
|
100: 'Continue', 101: 'Switching Protocols',
|
|
6
7
|
200: 'OK', 201: 'Created', 202: 'Accepted', 204: 'No Content',
|
|
@@ -33,6 +34,22 @@ function findCrLfCrLf(buf) {
|
|
|
33
34
|
}
|
|
34
35
|
return -1;
|
|
35
36
|
}
|
|
37
|
+
/** Flatten a `HeadersInit` into `[name, value]` pairs, preserving the
|
|
38
|
+
* casing the caller provided (Node `addTrailers` semantics). */
|
|
39
|
+
function headersInitToPairs(init) {
|
|
40
|
+
if (init instanceof HeadersImpl) {
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const [name, value] of init)
|
|
43
|
+
out.push([name, value]);
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(init))
|
|
47
|
+
return init.map(([name, value]) => [name, String(value)]);
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const name of Object.keys(init))
|
|
50
|
+
out.push([name, String(init[name])]);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
36
53
|
/** Decode a chunked-transfer body starting at `start`. Returns the decoded
|
|
37
54
|
* data and the offset just past the terminating CRLF, or null if the buffer
|
|
38
55
|
* does not yet contain the complete chunked body. */
|
|
@@ -176,10 +193,12 @@ export class HttpServer {
|
|
|
176
193
|
pending: null,
|
|
177
194
|
current: null,
|
|
178
195
|
outQueue: null,
|
|
196
|
+
resumeRead: null,
|
|
179
197
|
closed: false,
|
|
180
198
|
closeAfterFlush: false,
|
|
181
199
|
keepAlive: true,
|
|
182
200
|
owner: this,
|
|
201
|
+
ac: null,
|
|
183
202
|
};
|
|
184
203
|
this.conns.set(c.sock, c);
|
|
185
204
|
sock.set_on_event(c.sock, (event) => this.onConnEvent(c, event));
|
|
@@ -194,8 +213,8 @@ export class HttpServer {
|
|
|
194
213
|
flushQueue(c);
|
|
195
214
|
if (event.lNetworkEvents & 32 /* sock.FdEvent.FD_CLOSE */) {
|
|
196
215
|
onRead(c);
|
|
197
|
-
|
|
198
|
-
|
|
216
|
+
abortConn(c);
|
|
217
|
+
closeConn(c);
|
|
199
218
|
}
|
|
200
219
|
}
|
|
201
220
|
}
|
|
@@ -215,24 +234,28 @@ function onRead(c) {
|
|
|
215
234
|
if (!c.closed)
|
|
216
235
|
tryProcess(c);
|
|
217
236
|
}
|
|
237
|
+
/** Send `data`, or queue it when the kernel send buffer is full.
|
|
238
|
+
* Returns true when the data was fully handed to the kernel, or false
|
|
239
|
+
* when it has been buffered (or partially handed over) and the caller
|
|
240
|
+
* should back off until `flushQueue` drains and calls `c.resumeRead`. */
|
|
218
241
|
function queueSend(c, data) {
|
|
219
242
|
if (c.closed || data.length === 0)
|
|
220
|
-
return;
|
|
243
|
+
return true;
|
|
221
244
|
if (c.outQueue && c.outQueue.length > 0) {
|
|
222
245
|
c.outQueue = concatU8([c.outQueue, data]);
|
|
223
|
-
return;
|
|
246
|
+
return false;
|
|
224
247
|
}
|
|
225
248
|
const ret = sock.send(c.sock, toArrayBuffer(data));
|
|
226
249
|
if (ret < 0) {
|
|
227
|
-
c
|
|
228
|
-
|
|
250
|
+
abortConn(c);
|
|
251
|
+
closeConn(c);
|
|
252
|
+
return true;
|
|
229
253
|
}
|
|
230
254
|
if (ret < data.length) {
|
|
231
255
|
c.outQueue = data.subarray(ret);
|
|
232
|
-
return;
|
|
256
|
+
return false;
|
|
233
257
|
}
|
|
234
|
-
|
|
235
|
-
closeConn(c);
|
|
258
|
+
return true;
|
|
236
259
|
}
|
|
237
260
|
function flushQueue(c) {
|
|
238
261
|
if (c.closed)
|
|
@@ -240,12 +263,17 @@ function flushQueue(c) {
|
|
|
240
263
|
if (!c.outQueue || c.outQueue.length === 0) {
|
|
241
264
|
if (c.closeAfterFlush)
|
|
242
265
|
closeConn(c);
|
|
266
|
+
else
|
|
267
|
+
resumeReader(c);
|
|
243
268
|
return;
|
|
244
269
|
}
|
|
245
270
|
const q = c.outQueue;
|
|
246
271
|
const ret = sock.send(c.sock, toArrayBuffer(q));
|
|
247
|
-
if (ret < 0)
|
|
272
|
+
if (ret < 0) {
|
|
273
|
+
abortConn(c);
|
|
274
|
+
closeConn(c);
|
|
248
275
|
return;
|
|
276
|
+
}
|
|
249
277
|
if (ret < q.length) {
|
|
250
278
|
c.outQueue = q.subarray(ret);
|
|
251
279
|
return;
|
|
@@ -253,15 +281,101 @@ function flushQueue(c) {
|
|
|
253
281
|
c.outQueue = null;
|
|
254
282
|
if (c.closeAfterFlush)
|
|
255
283
|
closeConn(c);
|
|
284
|
+
else
|
|
285
|
+
resumeReader(c);
|
|
256
286
|
}
|
|
257
287
|
function closeConn(c) {
|
|
258
288
|
if (c.closed)
|
|
259
289
|
return;
|
|
260
290
|
c.closed = true;
|
|
291
|
+
resumeReader(c);
|
|
261
292
|
if (c.sock >= 0)
|
|
262
293
|
sock.closesocket(c.sock);
|
|
263
294
|
c.owner.conns.delete(c.sock);
|
|
264
295
|
}
|
|
296
|
+
/** Abort the in-flight request (notifying its handler via `req.signal`).
|
|
297
|
+
* Safe to call when idle or already aborted: the controller is cleared so
|
|
298
|
+
* a fresh request on this connection gets a fresh signal. */
|
|
299
|
+
function abortConn(c) {
|
|
300
|
+
const ac = c.ac;
|
|
301
|
+
c.ac = null;
|
|
302
|
+
if (ac)
|
|
303
|
+
ac.abort();
|
|
304
|
+
}
|
|
305
|
+
/** Resolve the pending back-pressure awaiter, if any. Called whenever the
|
|
306
|
+
* send queue drains or the connection closes. */
|
|
307
|
+
function resumeReader(c) {
|
|
308
|
+
const r = c.resumeRead;
|
|
309
|
+
c.resumeRead = null;
|
|
310
|
+
if (r)
|
|
311
|
+
r();
|
|
312
|
+
}
|
|
313
|
+
/** Close the connection once a response is fully flushed, when the
|
|
314
|
+
* handler or client asked for `Connection: close`. */
|
|
315
|
+
function finishResponse(c) {
|
|
316
|
+
if (c.closeAfterFlush && !c.outQueue)
|
|
317
|
+
closeConn(c);
|
|
318
|
+
}
|
|
319
|
+
/** Frame `data` as a single chunked-transfer block: `<hex size>\r\n<data>\r\n`. */
|
|
320
|
+
function encodeChunkFrame(data, enc) {
|
|
321
|
+
return concatU8([enc.encode(data.byteLength.toString(16) + '\r\n'), data, enc.encode('\r\n')]);
|
|
322
|
+
}
|
|
323
|
+
/** Read `body` one chunk at a time and send it out. When the kernel send
|
|
324
|
+
* buffer fills up (`queueSend` returns false) we pause on `c.resumeRead`,
|
|
325
|
+
* which `flushQueue` resumes once the socket drains — backpressure. */
|
|
326
|
+
async function writeStreamingBody(c, body, chunked, trailer) {
|
|
327
|
+
if (c.closed || body == null)
|
|
328
|
+
return;
|
|
329
|
+
const reader = body.getReader();
|
|
330
|
+
const enc = new TextEncoder();
|
|
331
|
+
try {
|
|
332
|
+
while (!c.closed) {
|
|
333
|
+
const { done, value } = await reader.read();
|
|
334
|
+
if (done)
|
|
335
|
+
break;
|
|
336
|
+
if (value && value.byteLength > 0) {
|
|
337
|
+
const frame = chunked ? encodeChunkFrame(value, enc) : value;
|
|
338
|
+
if (!queueSend(c, frame)) {
|
|
339
|
+
await new Promise((resolve) => { c.resumeRead = resolve; });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (c.closed) {
|
|
344
|
+
// 客户端断开(FD_CLOSE / send 失败):取消响应体,让 producer 感知
|
|
345
|
+
//(exec_server 借此 kill 子进程),而不是干等它自然结束。
|
|
346
|
+
try {
|
|
347
|
+
await reader.cancel();
|
|
348
|
+
}
|
|
349
|
+
catch (e) {
|
|
350
|
+
console.error('[http-server] body cancel error:', e);
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (chunked) {
|
|
355
|
+
let terminator = '0\r\n\r\n';
|
|
356
|
+
if (trailer) {
|
|
357
|
+
try {
|
|
358
|
+
const pairs = headersInitToPairs(await trailer);
|
|
359
|
+
if (pairs.length > 0) {
|
|
360
|
+
let s = '0\r\n';
|
|
361
|
+
for (const [name, value] of pairs)
|
|
362
|
+
s += name + ': ' + value + '\r\n';
|
|
363
|
+
s += '\r\n';
|
|
364
|
+
terminator = s;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch (e) {
|
|
368
|
+
console.error('[http-server] trailers promise rejected:', e);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
queueSend(c, enc.encode(terminator));
|
|
372
|
+
}
|
|
373
|
+
finishResponse(c);
|
|
374
|
+
}
|
|
375
|
+
finally {
|
|
376
|
+
reader.releaseLock();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
265
379
|
// ── Request state machine ──
|
|
266
380
|
function whole(c) {
|
|
267
381
|
return concatU8(c.parts);
|
|
@@ -345,11 +459,14 @@ function dispatchRequest(c, parsed, body, end) {
|
|
|
345
459
|
}
|
|
346
460
|
const isAbsolute = parsed.target.startsWith('http://') || parsed.target.startsWith('https://');
|
|
347
461
|
const url = isAbsolute ? parsed.target : 'http://' + host + parsed.target;
|
|
462
|
+
const ac = new AbortController();
|
|
348
463
|
const req = new RequestImpl(url, {
|
|
349
464
|
method: parsed.method,
|
|
350
465
|
headers: new HeadersImpl(parsed.headers),
|
|
351
466
|
body: body.length > 0 ? body : undefined,
|
|
467
|
+
signal: ac.signal,
|
|
352
468
|
});
|
|
469
|
+
c.ac = ac;
|
|
353
470
|
c.current = { req };
|
|
354
471
|
trimParts(c, whole(c), end);
|
|
355
472
|
void handleRequest(c);
|
|
@@ -377,40 +494,81 @@ async function handleRequest(c) {
|
|
|
377
494
|
}
|
|
378
495
|
if (c.closed)
|
|
379
496
|
return;
|
|
380
|
-
|
|
497
|
+
try {
|
|
498
|
+
await sendResponse(c, req.method, res);
|
|
499
|
+
}
|
|
500
|
+
catch (e) {
|
|
501
|
+
console.error('[http-server] response send error:', e);
|
|
502
|
+
if (!c.closed)
|
|
503
|
+
closeConn(c);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
381
506
|
if (c.closed)
|
|
382
507
|
return;
|
|
383
508
|
c.current = null;
|
|
384
509
|
if (!c.closeAfterFlush)
|
|
385
510
|
tryProcess(c);
|
|
386
511
|
}
|
|
512
|
+
function buildHead(status, statusText, headers) {
|
|
513
|
+
let head = 'HTTP/1.1 ' + status + ' ' + statusText + '\r\n';
|
|
514
|
+
for (const [name, value] of headers)
|
|
515
|
+
head += name + ': ' + value + '\r\n';
|
|
516
|
+
head += '\r\n';
|
|
517
|
+
return new TextEncoder().encode(head);
|
|
518
|
+
}
|
|
387
519
|
async function sendResponse(c, method, res) {
|
|
388
520
|
const status = res.status;
|
|
389
521
|
const statusText = res.statusText || STATUS_TEXT[status] || 'OK';
|
|
390
522
|
const headers = new HeadersImpl(res.headers);
|
|
391
|
-
let body;
|
|
392
|
-
try {
|
|
393
|
-
body = await _readStream(res.body);
|
|
394
|
-
}
|
|
395
|
-
catch {
|
|
396
|
-
body = new Uint8Array(0);
|
|
397
|
-
}
|
|
398
|
-
if (c.closed)
|
|
399
|
-
return;
|
|
400
523
|
const headOnly = method === 'HEAD';
|
|
401
524
|
const bodyless = (status >= 100 && status < 200) || status === 204 || status === 304;
|
|
402
|
-
if (!headers.has('content-length') && !bodyless)
|
|
403
|
-
headers.set('Content-Length', String(body.length));
|
|
404
525
|
if (!headers.has('connection'))
|
|
405
526
|
headers.set('Connection', c.keepAlive ? 'keep-alive' : 'close');
|
|
406
527
|
const connHdr = (headers.get('connection') || '').toLowerCase();
|
|
407
528
|
if (!c.keepAlive || connHdr === 'close')
|
|
408
529
|
c.closeAfterFlush = true;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
530
|
+
if (bodyless) {
|
|
531
|
+
headers.delete('Transfer-Encoding');
|
|
532
|
+
headers.delete('Content-Length');
|
|
533
|
+
headers.delete('Trailer');
|
|
534
|
+
queueSend(c, buildHead(status, statusText, headers));
|
|
535
|
+
finishResponse(c);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (headOnly) {
|
|
539
|
+
headers.delete('Transfer-Encoding');
|
|
540
|
+
headers.delete('Trailer');
|
|
541
|
+
if (!headers.has('content-length')) {
|
|
542
|
+
try {
|
|
543
|
+
const full = await _readStream(res.body);
|
|
544
|
+
headers.set('Content-Length', String(full.length));
|
|
545
|
+
}
|
|
546
|
+
catch { /* leave length unspecified */ }
|
|
547
|
+
}
|
|
548
|
+
if (c.closed)
|
|
549
|
+
return;
|
|
550
|
+
queueSend(c, buildHead(status, statusText, headers));
|
|
551
|
+
finishResponse(c);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
// Body is streamed out chunk by chunk. A declared Content-Length sends
|
|
555
|
+
// the bytes as-is; otherwise we use chunked framing.
|
|
556
|
+
const chunked = (headers.get('transfer-encoding') || '').toLowerCase() === 'chunked' ||
|
|
557
|
+
!headers.has('content-length');
|
|
558
|
+
// Trailing headers are only emitted when the body is chunk-framed and a
|
|
559
|
+
// `Trailer` declaration header is present (Node addTrailers semantics).
|
|
560
|
+
const trailerDeclared = (headers.get('trailer') || '').trim().length > 0;
|
|
561
|
+
if (chunked) {
|
|
562
|
+
headers.delete('Content-Length');
|
|
563
|
+
if (!headers.has('transfer-encoding'))
|
|
564
|
+
headers.set('Transfer-Encoding', 'chunked');
|
|
565
|
+
if (!trailerDeclared)
|
|
566
|
+
headers.delete('Trailer');
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
headers.delete('Transfer-Encoding');
|
|
570
|
+
headers.delete('Trailer');
|
|
571
|
+
}
|
|
572
|
+
queueSend(c, buildHead(status, statusText, headers));
|
|
573
|
+
await writeStreamingBody(c, res.body, chunked, (chunked && trailerDeclared) ? res.trailers : undefined);
|
|
416
574
|
}
|