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.
Files changed (143) hide show
  1. package/README.en.md +1 -2
  2. package/README.md +19 -23
  3. package/examples/PdfCanvas.js +8 -37
  4. package/examples/PdfCanvas.tsx +11 -41
  5. package/examples/exec_server.js +20054 -1423
  6. package/examples/exec_server.tsx +489 -0
  7. package/examples/exec_server_worker.js +383 -33
  8. package/examples/exec_server_worker.ts +408 -30
  9. package/examples/pdf_preview2.js +25 -82
  10. package/examples/pdf_preview2.ts +27 -84
  11. package/examples/pdf_viewer.js +1318 -1178
  12. package/examples/pdf_viewer.tsx +16 -67
  13. package/examples/setres.js +22 -47
  14. package/examples/setres.ts +23 -57
  15. package/examples/test_gallery.js +1313 -1074
  16. package/examples/test_react_lazy.js +1284 -1056
  17. package/examples/test_react_listbox.js +1283 -1055
  18. package/examples/test_root_window.js +1279 -1051
  19. package/examples/test_tab.js +1284 -1056
  20. package/lib/abort.js +70 -0
  21. package/lib/abort.ts +69 -0
  22. package/lib/certs.js +214 -0
  23. package/lib/certs.ts +224 -0
  24. package/lib/exec-log.js +15 -0
  25. package/lib/exec-log.ts +30 -0
  26. package/lib/fetch.js +37 -4
  27. package/lib/fetch.ts +35 -4
  28. package/lib/ffi/bind.js +352 -0
  29. package/lib/ffi/bind.ts +442 -0
  30. package/lib/ffi/ctype.js +168 -0
  31. package/lib/ffi/ctype.ts +331 -0
  32. package/lib/ffi/index.js +6 -0
  33. package/lib/ffi/index.ts +6 -0
  34. package/lib/ffi/struct.js +412 -0
  35. package/lib/ffi/struct.ts +664 -0
  36. package/lib/http-common.js +43 -5
  37. package/lib/http-common.ts +48 -6
  38. package/lib/http-server.js +187 -29
  39. package/lib/http-server.ts +177 -26
  40. package/lib/react-qw/components/DateTimePicker.js +18 -37
  41. package/lib/react-qw/components/Input.js +3 -10
  42. package/lib/react-qw/components/Link.js +6 -19
  43. package/lib/react-qw/components/ListView.js +49 -154
  44. package/lib/react-qw/components/PathPicker.js +28 -88
  45. package/lib/react-qw/components/ScrollView.js +15 -8
  46. package/lib/react-qw/components/Slider.js +2 -1
  47. package/lib/react-qw/components/Tab.js +6 -22
  48. package/lib/react-qw/components/Tooltip.js +10 -21
  49. package/lib/react-qw/components/TreeView.js +8 -31
  50. package/lib/react-qw/index.js +4 -2
  51. package/lib/react-qw/index.ts +4 -2
  52. package/lib/react-qw/nmhdr.js +3 -24
  53. package/lib/react-qw/nmhdr.ts +5 -30
  54. package/lib/react-qw/props.js +7 -2
  55. package/lib/react-qw/props.ts +7 -3
  56. package/lib/react-qw/reconciler.js +23 -13
  57. package/lib/react-qw/reconciler.ts +22 -13
  58. package/lib/text-measure.js +18 -23
  59. package/lib/text-measure.ts +20 -25
  60. package/lib/websocket.js +12 -2
  61. package/lib/websocket.ts +15 -2
  62. package/lib/windows/comctl32.js +18 -0
  63. package/lib/windows/comctl32.ts +20 -0
  64. package/lib/windows/comdlg32.js +7 -0
  65. package/lib/windows/comdlg32.ts +9 -0
  66. package/lib/windows/gdi32.js +90 -0
  67. package/lib/windows/gdi32.ts +97 -0
  68. package/lib/windows/kernel32.js +66 -0
  69. package/lib/windows/kernel32.ts +76 -0
  70. package/lib/windows/ntdll.js +8 -0
  71. package/lib/windows/ntdll.ts +10 -0
  72. package/lib/windows/ole32.js +7 -0
  73. package/lib/windows/ole32.ts +9 -0
  74. package/lib/windows/shell32.js +11 -0
  75. package/lib/windows/shell32.ts +14 -0
  76. package/lib/windows/structs.js +384 -0
  77. package/lib/windows/structs.ts +422 -0
  78. package/lib/windows/user32.js +469 -0
  79. package/lib/windows/user32.ts +502 -0
  80. package/package.json +1 -1
  81. package/quickwin.d.ts +22 -37
  82. package/quickwin_const.d.ts +1 -0
  83. package/qwin-nowasm-x86.exe +0 -0
  84. package/qwin-nowasm.exe +0 -0
  85. package/qwin-x86.exe +0 -0
  86. package/qwin.exe +0 -0
  87. package/test/bench_ffi_callraw.js +236 -0
  88. package/test/bench_ffi_callraw.ts +236 -0
  89. package/test/bench_ffi_overhead.js +139 -0
  90. package/test/bench_ffi_overhead.ts +141 -0
  91. package/test/bench_sendmessage.js +104 -0
  92. package/test/bench_sendmessage.ts +100 -0
  93. package/test/bench_sendmessage_nonnull.js +187 -0
  94. package/test/bench_sendmessage_nonnull.ts +188 -0
  95. package/test/run.js +12 -4
  96. package/test/run.ts +13 -5
  97. package/test/test_basic_fetch.js +8 -0
  98. package/test/test_basic_fetch.ts +9 -0
  99. package/test/test_ffi.js +25 -43
  100. package/test/test_ffi.ts +33 -52
  101. package/test/test_ffi_abi.js +235 -0
  102. package/test/test_ffi_abi.ts +218 -0
  103. package/test/test_ffi_bind.js +538 -8
  104. package/test/test_ffi_bind.ts +559 -8
  105. package/test/test_ffi_struct.js +632 -88
  106. package/test/test_ffi_struct.ts +666 -91
  107. package/test/test_ffi_struct_layout.js +495 -0
  108. package/test/test_ffi_struct_layout.ts +543 -0
  109. package/test/test_ffi_user32.js +103 -0
  110. package/test/test_ffi_user32.ts +113 -0
  111. package/test/test_ffi_windows.js +101 -0
  112. package/test/test_ffi_windows.ts +113 -0
  113. package/test/test_helper.js +8 -1
  114. package/test/test_helper.ts +9 -1
  115. package/test/test_http_import.js +18 -0
  116. package/test/test_http_import.ts +19 -0
  117. package/test/test_http_server.js +73 -1
  118. package/test/test_http_server.ts +75 -1
  119. package/test/test_ipv6.js +2 -2
  120. package/test/test_ipv6.ts +2 -2
  121. package/test/test_net_fetch.js +70 -8
  122. package/test/test_net_fetch.ts +69 -7
  123. package/test/test_net_websocket.js +1 -1
  124. package/test/test_net_websocket.ts +1 -1
  125. package/test/test_react_complex.js +1294 -1066
  126. package/test/test_react_counter.js +1284 -1056
  127. package/test/test_react_flex.js +1304 -1069
  128. package/test/test_react_hidden.js +1287 -1059
  129. package/test/test_react_listview_link.js +1284 -1056
  130. package/test/test_react_notify.js +18534 -0
  131. package/test/test_react_render.js +1284 -1056
  132. package/test/test_wolfssl_read.js +3 -2
  133. package/test/test_wolfssl_read.ts +3 -1
  134. package/test/test_worker_net.js +35 -17
  135. package/test/test_worker_net.ts +27 -15
  136. package/tsconfig.json +1 -1
  137. package/examples/exec_server.ts +0 -80
  138. package/lib/ffi-bind.js +0 -85
  139. package/lib/ffi-bind.ts +0 -170
  140. package/lib/ffi-struct.js +0 -224
  141. package/lib/ffi-struct.ts +0 -239
  142. package/lib/win-common-structs.js +0 -39
  143. package/lib/win-common-structs.ts +0 -41
@@ -193,7 +193,8 @@ export class RequestImpl {
193
193
  credentials = 'same-origin';
194
194
  referrer = 'about:client';
195
195
  referrerPolicy = '';
196
- signal = null;
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
- this._body = body != null ? _toReadableStream(body) : new ReadableStream({
300
- start(ctrl) { ctrl.close(); }
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;
@@ -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 = 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
- this._body = body != null ? _toReadableStream(body) : new ReadableStream<Uint8Array>({
300
- start(ctrl) { ctrl.close() }
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: Event) => void) | null;
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 {}
@@ -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
- if (!c.current && !c.pending)
198
- closeConn(c);
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.outQueue = data;
228
- return;
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
- if (c.closeAfterFlush)
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
- await sendResponse(c, req.method, res);
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
- let head = 'HTTP/1.1 ' + status + ' ' + statusText + '\r\n';
410
- for (const [name, value] of headers)
411
- head += name + ': ' + value + '\r\n';
412
- head += '\r\n';
413
- const headBytes = new TextEncoder().encode(head);
414
- const payload = headOnly || body.length === 0 ? headBytes : concatU8([headBytes, body]);
415
- queueSend(c, payload);
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
  }