quickwin 2.3.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/constants.ts CHANGED
@@ -961,6 +961,13 @@ export const enum ProcessCreationFlag {
961
961
  NO_WINDOW = 134217728, // 0x8000000
962
962
  }
963
963
 
964
+ // File
965
+ export const enum MoveFileFlag {
966
+ REPLACE_EXISTING = 1, // 0x1
967
+ COPY_ALLOWED = 2, // 0x2
968
+ WRITE_THROUGH = 8, // 0x8
969
+ }
970
+
964
971
  // Error codes
965
972
  export const enum ErrorCode {
966
973
  FILE_NOT_FOUND = 2, // 0x2
package/enum_map.d.ts CHANGED
@@ -108,6 +108,7 @@ interface EnumMap {
108
108
  IDI: import('./constants').IDI
109
109
  DrawTextFlag: import('./constants').DrawTextFlag
110
110
  ProcessCreationFlag: import('./constants').ProcessCreationFlag
111
+ MoveFileFlag: import('./constants').MoveFileFlag
111
112
  ErrorCode: import('./constants').ErrorCode
112
113
  PowerBroadcast: import('./constants').PowerBroadcast
113
114
  DevMode: import('./constants').DevMode
@@ -18535,6 +18535,8 @@ function concatU8(parts) {
18535
18535
  return out;
18536
18536
  }
18537
18537
  function toArrayBuffer(u8) {
18538
+ if (u8.byteOffset === 0 && u8.byteLength === u8.buffer.byteLength)
18539
+ return u8.buffer;
18538
18540
  return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength);
18539
18541
  }
18540
18542
  function findCrLfCrLf(buf) {
@@ -18694,6 +18696,7 @@ var HttpServer = class {
18694
18696
  const c2 = {
18695
18697
  sock: ac.handle,
18696
18698
  parts: [],
18699
+ partsLen: 0,
18697
18700
  pending: null,
18698
18701
  current: null,
18699
18702
  outQueue: null,
@@ -18732,7 +18735,9 @@ function onRead(c2) {
18732
18735
  const d2 = sock.recv(c2.sock, 8192);
18733
18736
  if (!d2)
18734
18737
  break;
18735
- c2.parts.push(new Uint8Array(d2));
18738
+ const u8 = new Uint8Array(d2);
18739
+ c2.parts.push(u8);
18740
+ c2.partsLen += u8.length;
18736
18741
  }
18737
18742
  if (!c2.closed)
18738
18743
  tryProcess(c2);
@@ -18822,11 +18827,17 @@ async function writeStreamingBody(c2, body, chunked, trailer) {
18822
18827
  if (done)
18823
18828
  break;
18824
18829
  if (value && value.byteLength > 0) {
18825
- const frame = chunked ? encodeChunkFrame(value, enc) : value;
18826
- if (!queueSend(c2, frame)) {
18827
- await new Promise((resolve) => {
18828
- c2.resumeRead = resolve;
18829
- });
18830
+ const MAX_SEND = 65536;
18831
+ for (let off = 0; off < value.byteLength; off += MAX_SEND) {
18832
+ const piece = value.byteLength <= MAX_SEND ? value : value.subarray(off, Math.min(off + MAX_SEND, value.byteLength));
18833
+ const frame = chunked ? encodeChunkFrame(piece, enc) : piece;
18834
+ if (!queueSend(c2, frame)) {
18835
+ await new Promise((resolve) => {
18836
+ c2.resumeRead = resolve;
18837
+ });
18838
+ }
18839
+ if (c2.closed)
18840
+ break;
18830
18841
  }
18831
18842
  }
18832
18843
  }
@@ -18867,36 +18878,36 @@ function whole(c2) {
18867
18878
  function trimParts(c2, buf, consumed) {
18868
18879
  if (consumed >= buf.length) {
18869
18880
  c2.parts = [];
18881
+ c2.partsLen = 0;
18870
18882
  } else {
18871
18883
  c2.parts = [buf.subarray(consumed)];
18884
+ c2.partsLen = buf.length - consumed;
18872
18885
  }
18873
18886
  }
18874
18887
  function tryProcess(c2) {
18875
18888
  if (c2.closed || c2.current)
18876
18889
  return;
18877
- const buf = whole(c2);
18878
18890
  if (c2.pending) {
18879
18891
  const bodyStart2 = c2.pending.bodyStart;
18880
18892
  const parsed2 = c2.pending.parsed;
18881
- let body;
18882
- let end;
18883
- if (parsed2.chunked) {
18884
- const dec = decodeChunked(buf, bodyStart2);
18885
- if (!dec)
18886
- return;
18887
- body = dec.data;
18888
- end = dec.end;
18889
- } else {
18890
- const needed = parsed2.contentLength;
18891
- if (bodyStart2 + needed > buf.length)
18893
+ if (!parsed2.chunked) {
18894
+ const end = bodyStart2 + parsed2.contentLength;
18895
+ if (end > c2.partsLen)
18892
18896
  return;
18893
- body = buf.subarray(bodyStart2, bodyStart2 + needed);
18894
- end = bodyStart2 + needed;
18897
+ const buf3 = whole(c2);
18898
+ c2.pending = null;
18899
+ dispatchRequest(c2, parsed2, buf3.subarray(bodyStart2, end), end, buf3);
18900
+ return;
18895
18901
  }
18902
+ const buf2 = whole(c2);
18903
+ const dec = decodeChunked(buf2, bodyStart2);
18904
+ if (!dec)
18905
+ return;
18896
18906
  c2.pending = null;
18897
- dispatchRequest(c2, parsed2, body, end);
18907
+ dispatchRequest(c2, parsed2, dec.data, dec.end, buf2);
18898
18908
  return;
18899
18909
  }
18910
+ const buf = whole(c2);
18900
18911
  const headerEnd = findCrLfCrLf(buf);
18901
18912
  if (headerEnd < 0)
18902
18913
  return;
@@ -18914,18 +18925,18 @@ function tryProcess(c2) {
18914
18925
  c2.pending = { parsed, bodyStart };
18915
18926
  return;
18916
18927
  }
18917
- dispatchRequest(c2, parsed, dec.data, dec.end);
18928
+ dispatchRequest(c2, parsed, dec.data, dec.end, buf);
18918
18929
  } else if (parsed.contentLength > 0) {
18919
18930
  if (bodyStart + parsed.contentLength > buf.length) {
18920
18931
  c2.pending = { parsed, bodyStart };
18921
18932
  return;
18922
18933
  }
18923
- dispatchRequest(c2, parsed, buf.subarray(bodyStart, bodyStart + parsed.contentLength), bodyStart + parsed.contentLength);
18934
+ dispatchRequest(c2, parsed, buf.subarray(bodyStart, bodyStart + parsed.contentLength), bodyStart + parsed.contentLength, buf);
18924
18935
  } else {
18925
- dispatchRequest(c2, parsed, new Uint8Array(0), bodyStart);
18936
+ dispatchRequest(c2, parsed, new Uint8Array(0), bodyStart, buf);
18926
18937
  }
18927
18938
  }
18928
- function dispatchRequest(c2, parsed, body, end) {
18939
+ function dispatchRequest(c2, parsed, body, end, wholeBuf) {
18929
18940
  const connHeader = (parsed.headers["connection"] || "").toLowerCase();
18930
18941
  if (parsed.version === "HTTP/1.1")
18931
18942
  c2.keepAlive = connHeader.indexOf("close") < 0;
@@ -18948,7 +18959,7 @@ function dispatchRequest(c2, parsed, body, end) {
18948
18959
  });
18949
18960
  c2.ac = ac;
18950
18961
  c2.current = { req };
18951
- trimParts(c2, whole(c2), end);
18962
+ trimParts(c2, wholeBuf ?? whole(c2), end);
18952
18963
  void handleRequest(c2);
18953
18964
  }
18954
18965
  async function handleRequest(c2) {
@@ -22022,10 +22033,6 @@ function createRoot(container) {
22022
22033
  // _build/examples/execd/main.js
22023
22034
  var import_react18 = __toESM(require_react(), 1);
22024
22035
 
22025
- // _build/examples/execd/tray.js
22026
- import * as os6 from "os";
22027
- import * as gui18 from "gui";
22028
-
22029
22036
  // _build/lib/windows/kernel32.js
22030
22037
  var b7 = (name, sig, encoders) => bind2("kernel32.dll", name, sig, encoders);
22031
22038
  var CloseHandle = /* @__PURE__ */ b7("CloseHandle", "<>ptr -> i32");
@@ -22034,9 +22041,13 @@ var GetCurrentProcess = /* @__PURE__ */ b7("GetCurrentProcess", " -> <HPROCESS>p
22034
22041
  var VirtualAllocEx = /* @__PURE__ */ b7("VirtualAllocEx", "<HPROCESS>ptr <>ptr <>ptr u32 u32 -> <>ptr");
22035
22042
  var VirtualFreeEx = /* @__PURE__ */ b7("VirtualFreeEx", "<HPROCESS>ptr <>ptr <>ptr u32 -> i32");
22036
22043
  var ReadProcessMemory = /* @__PURE__ */ b7("ReadProcessMemory", "<HPROCESS>ptr <>ptr <BYTE>ptr <>ptr <>ptr -> i32");
22044
+ var MoveFileEx = /* @__PURE__ */ b7("MoveFileExW", "<WCHAR>ptr! <WCHAR>ptr! u32 -> i32");
22045
+ var GetLastError = /* @__PURE__ */ b7("GetLastError", " -> u32");
22037
22046
  var IsWow64Process = /* @__PURE__ */ b7("IsWow64Process", "<HPROCESS>ptr <BYTE>ptr -> i32");
22038
22047
 
22039
22048
  // _build/examples/execd/tray.js
22049
+ import * as os6 from "os";
22050
+ import * as gui18 from "gui";
22040
22051
  var TB_GETBUTTON = 1047;
22041
22052
  var TB_BUTTONCOUNT = 1048;
22042
22053
  var TB_GETITEMRECT = 1053;
@@ -22744,7 +22755,14 @@ API\uFF08\u914D\u7F6E token \u540E\u9700 Authorization: Bearer <token>\uFF0C\u67
22744
22755
  POST /input body = JSON \u6570\u7EC4\uFF08\u6279\u91CF\u6CE8\u5165\uFF1Bx/y \u7269\u7406\u50CF\u7D20\uFF09\uFF1A
22745
22756
  {"t":"move","x":,"y":} | {"t":"down|up","btn":"left|right|middle|x1|x2","x":,"y":?} |
22746
22757
  {"t":"key","key":"a|enter|0x41","up":true?} | {"t":"sleep","ms":0..5000}
22747
- \u2192 JSON {sent,expected}\uFF1B\u6309 sleep \u5207\u6BB5\uFF0C\u6BB5\u5185\u4E00\u6B21 SendInput \u539F\u5B50\u6CE8\u5165`;
22758
+ \u2192 JSON {sent,expected}\uFF1B\u6309 sleep \u5207\u6BB5\uFF0C\u6BB5\u5185\u4E00\u6B21 SendInput \u539F\u5B50\u6CE8\u5165
22759
+ POST /upload ?path=<\u76EE\u6807\u8DEF\u5F84>&mkdir=0 body=\u539F\u59CB\u5B57\u8282\uFF08\u975E\u7A7A\uFF09\u2192 JSON {bytes,path}
22760
+ \u9ED8\u8BA4\u9010\u5C42\u5EFA\u7236\u76EE\u5F55\uFF08mkdir=0 \u4E25\u683C\u8981\u6C42\u7236\u76EE\u5F55\u5DF2\u5B58\u5728\uFF0C\u9632\u8DEF\u5F84 typo\uFF09\uFF1B
22761
+ \u539F\u5B50\u5199\uFF1A\u5148\u5199 <path>.uploading \u518D MoveFileEx \u8986\u76D6\uFF0C\u770B\u4E0D\u5230\u534A\u622A\u6587\u4EF6\u3002
22762
+ \u65E0 SMB \u73AF\u5883\u7684\u4F20\u6587\u4EF6\u901A\u9053\uFF0C\u8DEF\u5F84\u5168\u653E\u5F00\u30028MB \u5206\u5757\u5199\u76D8\uFF08CRT \u5355\u5757\u5927\u5199
22763
+ \u5728 XP \u7F51\u7EDC\u76D8 ~64MiB \u8FB9\u754C\u4F1A\u88AB\u6574\u5757\u62D2\u7EDD\uFF09\uFF0CXP/win7 \u5B9E\u6D4B 100MB+ OK
22764
+ GET /download ?path=<\u6587\u4EF6\u8DEF\u5F84> \u2192 \u539F\u59CB\u5B57\u8282\u6D41\uFF08application/octet-stream\uFF0CX-Bytes \u5934\uFF1B
22765
+ \u5206\u5757\u8BFB\u5230 EOF \u65E0\u5927\u5C0F\u4E0A\u9650\uFF09\u2014\u2014type \u547D\u4EE4\u7684\u4E8C\u8FDB\u5236\u5B89\u5168\u7248`;
22748
22766
  function usageResponse() {
22749
22767
  return new Response(USAGE, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
22750
22768
  }
@@ -22809,10 +22827,23 @@ function vkOf(key) {
22809
22827
  }
22810
22828
  throw new Error("bad key: " + JSON.stringify(key) + " (a-z/0-9|name|0..0xFFFF)");
22811
22829
  }
22830
+ function mkdirParents(dest) {
22831
+ const segs = dest.replace(/\//g, "\\").split("\\");
22832
+ let acc = "";
22833
+ for (let i2 = 0; i2 < segs.length - 1; i2++) {
22834
+ acc = acc === "" ? segs[i2] : acc + "\\" + segs[i2];
22835
+ if (acc === "" || /^[A-Za-z]:$/.test(acc))
22836
+ continue;
22837
+ try {
22838
+ os7.mkdir(acc);
22839
+ } catch {
22840
+ }
22841
+ }
22842
+ }
22812
22843
  var server = createServer(async (req) => {
22813
22844
  const t0 = Date.now();
22814
22845
  const path = new URL(req.url).pathname;
22815
- const isApi = req.method === "POST" && (path === "/exec" || path === "/set_resolution" || path === "/input") || req.method === "GET" && (path === "/window_list" || path === "/screenshot" || path === "/tray_list");
22846
+ const isApi = req.method === "POST" && (path === "/exec" || path === "/set_resolution" || path === "/input" || path === "/upload") || req.method === "GET" && (path === "/window_list" || path === "/screenshot" || path === "/tray_list" || path === "/download");
22816
22847
  if (!isApi)
22817
22848
  return usageResponse();
22818
22849
  const denied = authCheck(req);
@@ -23031,6 +23062,103 @@ var server = createServer(async (req) => {
23031
23062
  return jsonError(504, String(ex));
23032
23063
  }
23033
23064
  }
23065
+ if (req.method === "POST" && path === "/upload") {
23066
+ const bad = (msg) => {
23067
+ logReq(t0, "POST /upload", 400, msg);
23068
+ return jsonError(400, msg);
23069
+ };
23070
+ const params = new URL(req.url).searchParams;
23071
+ const dest = params.get("path");
23072
+ if (!dest)
23073
+ return bad("path=<\u76EE\u6807\u8DEF\u5F84>");
23074
+ let bytes;
23075
+ try {
23076
+ bytes = new Uint8Array(await req.arrayBuffer());
23077
+ } catch (ex) {
23078
+ return bad("bad body: " + String(ex));
23079
+ }
23080
+ if (bytes.length === 0)
23081
+ return bad("empty body");
23082
+ try {
23083
+ if (params.get("mkdir") !== "0")
23084
+ mkdirParents(dest);
23085
+ const tmp = dest + ".uploading";
23086
+ const f2 = std2.open(tmp, "wb");
23087
+ if (!f2)
23088
+ throw new Error("open for write failed: " + tmp);
23089
+ let w2 = 0;
23090
+ try {
23091
+ const CHUNK = 8 * 1024 * 1024;
23092
+ for (let off = 0; off < bytes.length; off += CHUNK) {
23093
+ const n2 = f2.write(bytes.buffer, off, Math.min(CHUNK, bytes.length - off));
23094
+ if (n2 <= 0)
23095
+ break;
23096
+ w2 += n2;
23097
+ }
23098
+ } finally {
23099
+ f2.close();
23100
+ }
23101
+ if (w2 !== bytes.length)
23102
+ throw new Error("short write: " + w2 + "/" + bytes.length);
23103
+ if (!MoveFileEx(
23104
+ tmp,
23105
+ dest,
23106
+ 1
23107
+ /* MoveFileFlag.REPLACE_EXISTING */
23108
+ ))
23109
+ throw new Error("MoveFileEx failed (GetLastError " + GetLastError() + "): " + tmp + " -> " + dest);
23110
+ } catch (ex) {
23111
+ logReq(t0, "POST /upload", 500, String(ex));
23112
+ return jsonError(500, String(ex));
23113
+ }
23114
+ logReq(t0, "POST /upload", 200, bytes.length + "B -> " + dest);
23115
+ return new Response(JSON.stringify({ bytes: bytes.length, path: dest }), { headers: { "Content-Type": "application/json" } });
23116
+ }
23117
+ if (req.method === "GET" && path === "/download") {
23118
+ const p2 = new URL(req.url).searchParams.get("path");
23119
+ if (!p2) {
23120
+ logReq(t0, "GET /download", 400, "path=<\u6587\u4EF6\u8DEF\u5F84>");
23121
+ return jsonError(400, "path=<\u6587\u4EF6\u8DEF\u5F84>");
23122
+ }
23123
+ let f2 = null;
23124
+ try {
23125
+ f2 = std2.open(p2, "rb");
23126
+ } catch (ex) {
23127
+ logReq(t0, "GET /download", 500, String(ex));
23128
+ return jsonError(500, String(ex));
23129
+ }
23130
+ if (f2 === null) {
23131
+ logReq(t0, "GET /download", 404, p2);
23132
+ return jsonError(404, "cannot open: " + p2);
23133
+ }
23134
+ try {
23135
+ const chunks = [];
23136
+ const buf = new Uint8Array(64 * 1024);
23137
+ let total = 0;
23138
+ for (; ; ) {
23139
+ const n2 = f2.read(buf.buffer, 0, buf.length);
23140
+ if (n2 <= 0)
23141
+ break;
23142
+ chunks.push(n2 === buf.length ? buf.slice() : buf.slice(0, n2));
23143
+ total += n2;
23144
+ }
23145
+ const all = new Uint8Array(total);
23146
+ let off = 0;
23147
+ for (const c2 of chunks) {
23148
+ all.set(c2, off);
23149
+ off += c2.length;
23150
+ }
23151
+ logReq(t0, "GET /download", 200, total + "B <- " + p2);
23152
+ return new Response(all, {
23153
+ headers: { "Content-Type": "application/octet-stream", "X-Bytes": String(total) }
23154
+ });
23155
+ } catch (ex) {
23156
+ logReq(t0, "GET /download", 500, String(ex));
23157
+ return jsonError(500, String(ex));
23158
+ } finally {
23159
+ f2.close();
23160
+ }
23161
+ }
23034
23162
  return usageResponse();
23035
23163
  });
23036
23164
  var rc = server.listen(8080, "0.0.0.0");
@@ -24,6 +24,11 @@ function concatU8(parts) {
24
24
  return out;
25
25
  }
26
26
  function toArrayBuffer(u8) {
27
+ // 已经是完整 buffer(byteOffset=0、无尾部余量)时零复制返回——发送大
28
+ // body 的主路径,避免每片 send 前白拷一份。视图场景仍 slice 出独立副本
29
+ //(FFI 持有的 buffer 生命周期不受 view 约束)。
30
+ if (u8.byteOffset === 0 && u8.byteLength === u8.buffer.byteLength)
31
+ return u8.buffer;
27
32
  return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength);
28
33
  }
29
34
  function findCrLfCrLf(buf) {
@@ -190,6 +195,7 @@ export class HttpServer {
190
195
  const c = {
191
196
  sock: ac.handle,
192
197
  parts: [],
198
+ partsLen: 0,
193
199
  pending: null,
194
200
  current: null,
195
201
  outQueue: null,
@@ -229,7 +235,9 @@ function onRead(c) {
229
235
  const d = sock.recv(c.sock, 8192);
230
236
  if (!d)
231
237
  break;
232
- c.parts.push(new Uint8Array(d));
238
+ const u8 = new Uint8Array(d);
239
+ c.parts.push(u8);
240
+ c.partsLen += u8.length;
233
241
  }
234
242
  if (!c.closed)
235
243
  tryProcess(c);
@@ -334,9 +342,19 @@ async function writeStreamingBody(c, body, chunked, trailer) {
334
342
  if (done)
335
343
  break;
336
344
  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; });
345
+ // 切成 ≤64KB 再入 queueSend:单发 200MB 级大 chunk 时
346
+ // toArrayBuffer 会全量 slice 复制,partial drain 又每次复制
347
+ // 剩余(O(n²)),XP 32 位 1GB 上直接把进程水位顶穿导致断连;
348
+ // 小片让每次复制/发送恒为常数大小,backpressure 语义不变。
349
+ const MAX_SEND = 65536;
350
+ for (let off = 0; off < value.byteLength; off += MAX_SEND) {
351
+ const piece = value.byteLength <= MAX_SEND ? value : value.subarray(off, Math.min(off + MAX_SEND, value.byteLength));
352
+ const frame = chunked ? encodeChunkFrame(piece, enc) : piece;
353
+ if (!queueSend(c, frame)) {
354
+ await new Promise((resolve) => { c.resumeRead = resolve; });
355
+ }
356
+ if (c.closed)
357
+ break;
340
358
  }
341
359
  }
342
360
  }
@@ -383,38 +401,43 @@ function whole(c) {
383
401
  function trimParts(c, buf, consumed) {
384
402
  if (consumed >= buf.length) {
385
403
  c.parts = [];
404
+ c.partsLen = 0;
386
405
  }
387
406
  else {
388
407
  c.parts = [buf.subarray(consumed)];
408
+ c.partsLen = buf.length - consumed;
389
409
  }
390
410
  }
391
411
  function tryProcess(c) {
392
412
  if (c.closed || c.current)
393
413
  return;
394
- const buf = whole(c);
395
414
  if (c.pending) {
396
415
  const bodyStart = c.pending.bodyStart;
397
416
  const parsed = c.pending.parsed;
398
- let body;
399
- let end;
400
- if (parsed.chunked) {
401
- const dec = decodeChunked(buf, bodyStart);
402
- if (!dec)
403
- return;
404
- body = dec.data;
405
- end = dec.end;
406
- }
407
- else {
408
- const needed = parsed.contentLength;
409
- if (bodyStart + needed > buf.length)
417
+ if (!parsed.chunked) {
418
+ // O(1) length check while the body is still arriving — only
419
+ // concatenate once, when the last byte is in.
420
+ const end = bodyStart + parsed.contentLength;
421
+ if (end > c.partsLen)
410
422
  return;
411
- body = buf.subarray(bodyStart, bodyStart + needed);
412
- end = bodyStart + needed;
423
+ const buf = whole(c);
424
+ c.pending = null;
425
+ dispatchRequest(c, parsed, buf.subarray(bodyStart, end), end, buf);
426
+ return;
413
427
  }
428
+ // chunked: no total length up front, so a decode attempt still needs
429
+ // the concatenated view (re-tried per read event as before).
430
+ const buf = whole(c);
431
+ const dec = decodeChunked(buf, bodyStart);
432
+ if (!dec)
433
+ return;
414
434
  c.pending = null;
415
- dispatchRequest(c, parsed, body, end);
435
+ dispatchRequest(c, parsed, dec.data, dec.end, buf);
416
436
  return;
417
437
  }
438
+ // Header phase: bodies are checked against `partsLen` above, so a full
439
+ // concatenation is only needed here — once per request, not per read.
440
+ const buf = whole(c);
418
441
  const headerEnd = findCrLfCrLf(buf);
419
442
  if (headerEnd < 0)
420
443
  return;
@@ -432,20 +455,20 @@ function tryProcess(c) {
432
455
  c.pending = { parsed, bodyStart };
433
456
  return;
434
457
  }
435
- dispatchRequest(c, parsed, dec.data, dec.end);
458
+ dispatchRequest(c, parsed, dec.data, dec.end, buf);
436
459
  }
437
460
  else if (parsed.contentLength > 0) {
438
461
  if (bodyStart + parsed.contentLength > buf.length) {
439
462
  c.pending = { parsed, bodyStart };
440
463
  return;
441
464
  }
442
- dispatchRequest(c, parsed, buf.subarray(bodyStart, bodyStart + parsed.contentLength), bodyStart + parsed.contentLength);
465
+ dispatchRequest(c, parsed, buf.subarray(bodyStart, bodyStart + parsed.contentLength), bodyStart + parsed.contentLength, buf);
443
466
  }
444
467
  else {
445
- dispatchRequest(c, parsed, new Uint8Array(0), bodyStart);
468
+ dispatchRequest(c, parsed, new Uint8Array(0), bodyStart, buf);
446
469
  }
447
470
  }
448
- function dispatchRequest(c, parsed, body, end) {
471
+ function dispatchRequest(c, parsed, body, end, wholeBuf) {
449
472
  const connHeader = (parsed.headers['connection'] || '').toLowerCase();
450
473
  if (parsed.version === 'HTTP/1.1')
451
474
  c.keepAlive = connHeader.indexOf('close') < 0;
@@ -468,7 +491,9 @@ function dispatchRequest(c, parsed, body, end) {
468
491
  });
469
492
  c.ac = ac;
470
493
  c.current = { req };
471
- trimParts(c, whole(c), end);
494
+ // `wholeBuf` is the concatenation the caller already produced — reuse it
495
+ // instead of concatenating the very same parts a second time.
496
+ trimParts(c, wholeBuf ?? whole(c), end);
472
497
  void handleRequest(c);
473
498
  }
474
499
  async function handleRequest(c) {
@@ -35,6 +35,10 @@ interface ActiveReq {
35
35
  interface Conn {
36
36
  sock: SockHandle
37
37
  parts: Uint8Array[]
38
+ /** Cached total length of `parts` — lets the pending-body wait check
39
+ * "have we received enough yet" in O(1) instead of re-concatenating
40
+ * every received byte on each read event (O(n²) for large bodies). */
41
+ partsLen: number
38
42
  pending: PendingReq | null
39
43
  current: ActiveReq | null
40
44
  outQueue: Uint8Array | null
@@ -69,6 +73,10 @@ function concatU8(parts: Uint8Array[]): Uint8Array {
69
73
  }
70
74
 
71
75
  function toArrayBuffer(u8: Uint8Array): ArrayBuffer {
76
+ // 已经是完整 buffer(byteOffset=0、无尾部余量)时零复制返回——发送大
77
+ // body 的主路径,避免每片 send 前白拷一份。视图场景仍 slice 出独立副本
78
+ //(FFI 持有的 buffer 生命周期不受 view 约束)。
79
+ if (u8.byteOffset === 0 && u8.byteLength === u8.buffer.byteLength) return u8.buffer as ArrayBuffer
72
80
  return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer
73
81
  }
74
82
 
@@ -213,6 +221,7 @@ export class HttpServer {
213
221
  const c: Conn = {
214
222
  sock: ac.handle,
215
223
  parts: [],
224
+ partsLen: 0,
216
225
  pending: null,
217
226
  current: null,
218
227
  outQueue: null,
@@ -251,7 +260,9 @@ function onRead(c: Conn): void {
251
260
  while (true) {
252
261
  const d = sock.recv(c.sock, 8192)
253
262
  if (!d) break
254
- c.parts.push(new Uint8Array(d))
263
+ const u8 = new Uint8Array(d)
264
+ c.parts.push(u8)
265
+ c.partsLen += u8.length
255
266
  }
256
267
  if (!c.closed) tryProcess(c)
257
268
  }
@@ -350,9 +361,18 @@ async function writeStreamingBody(c: Conn, body: ReadableStream<Uint8Array> | nu
350
361
  const { done, value } = await reader.read()
351
362
  if (done) break
352
363
  if (value && value.byteLength > 0) {
353
- const frame = chunked ? encodeChunkFrame(value, enc) : value
354
- if (!queueSend(c, frame)) {
355
- await new Promise<void>((resolve) => { c.resumeRead = resolve })
364
+ // 切成 ≤64KB 再入 queueSend:单发 200MB 级大 chunk 时
365
+ // toArrayBuffer 会全量 slice 复制,partial drain 又每次复制
366
+ // 剩余(O(n²)),XP 32 位 1GB 上直接把进程水位顶穿导致断连;
367
+ // 小片让每次复制/发送恒为常数大小,backpressure 语义不变。
368
+ const MAX_SEND = 65536
369
+ for (let off = 0; off < value.byteLength; off += MAX_SEND) {
370
+ const piece = value.byteLength <= MAX_SEND ? value : value.subarray(off, Math.min(off + MAX_SEND, value.byteLength))
371
+ const frame = chunked ? encodeChunkFrame(piece, enc) : piece
372
+ if (!queueSend(c, frame)) {
373
+ await new Promise<void>((resolve) => { c.resumeRead = resolve })
374
+ }
375
+ if (c.closed) break
356
376
  }
357
377
  }
358
378
  }
@@ -396,36 +416,42 @@ function whole(c: Conn): Uint8Array {
396
416
  function trimParts(c: Conn, buf: Uint8Array, consumed: number): void {
397
417
  if (consumed >= buf.length) {
398
418
  c.parts = []
419
+ c.partsLen = 0
399
420
  } else {
400
421
  c.parts = [buf.subarray(consumed)]
422
+ c.partsLen = buf.length - consumed
401
423
  }
402
424
  }
403
425
 
404
426
  function tryProcess(c: Conn): void {
405
427
  if (c.closed || c.current) return
406
- const buf = whole(c)
407
428
 
408
429
  if (c.pending) {
409
430
  const bodyStart = c.pending.bodyStart
410
431
  const parsed = c.pending.parsed
411
- let body: Uint8Array
412
- let end: number
413
- if (parsed.chunked) {
414
- const dec = decodeChunked(buf, bodyStart)
415
- if (!dec) return
416
- body = dec.data
417
- end = dec.end
418
- } else {
419
- const needed = parsed.contentLength
420
- if (bodyStart + needed > buf.length) return
421
- body = buf.subarray(bodyStart, bodyStart + needed)
422
- end = bodyStart + needed
432
+ if (!parsed.chunked) {
433
+ // O(1) length check while the body is still arriving — only
434
+ // concatenate once, when the last byte is in.
435
+ const end = bodyStart + parsed.contentLength
436
+ if (end > c.partsLen) return
437
+ const buf = whole(c)
438
+ c.pending = null
439
+ dispatchRequest(c, parsed, buf.subarray(bodyStart, end), end, buf)
440
+ return
423
441
  }
442
+ // chunked: no total length up front, so a decode attempt still needs
443
+ // the concatenated view (re-tried per read event as before).
444
+ const buf = whole(c)
445
+ const dec = decodeChunked(buf, bodyStart)
446
+ if (!dec) return
424
447
  c.pending = null
425
- dispatchRequest(c, parsed, body, end)
448
+ dispatchRequest(c, parsed, dec.data, dec.end, buf)
426
449
  return
427
450
  }
428
451
 
452
+ // Header phase: bodies are checked against `partsLen` above, so a full
453
+ // concatenation is only needed here — once per request, not per read.
454
+ const buf = whole(c)
429
455
  const headerEnd = findCrLfCrLf(buf)
430
456
  if (headerEnd < 0) return
431
457
  const absEnd = headerEnd + 4
@@ -440,19 +466,19 @@ function tryProcess(c: Conn): void {
440
466
  c.pending = { parsed, bodyStart }
441
467
  return
442
468
  }
443
- dispatchRequest(c, parsed, dec.data, dec.end)
469
+ dispatchRequest(c, parsed, dec.data, dec.end, buf)
444
470
  } else if (parsed.contentLength > 0) {
445
471
  if (bodyStart + parsed.contentLength > buf.length) {
446
472
  c.pending = { parsed, bodyStart }
447
473
  return
448
474
  }
449
- dispatchRequest(c, parsed, buf.subarray(bodyStart, bodyStart + parsed.contentLength), bodyStart + parsed.contentLength)
475
+ dispatchRequest(c, parsed, buf.subarray(bodyStart, bodyStart + parsed.contentLength), bodyStart + parsed.contentLength, buf)
450
476
  } else {
451
- dispatchRequest(c, parsed, new Uint8Array(0), bodyStart)
477
+ dispatchRequest(c, parsed, new Uint8Array(0), bodyStart, buf)
452
478
  }
453
479
  }
454
480
 
455
- function dispatchRequest(c: Conn, parsed: ParsedHeader, body: Uint8Array, end: number): void {
481
+ function dispatchRequest(c: Conn, parsed: ParsedHeader, body: Uint8Array, end: number, wholeBuf?: Uint8Array): void {
456
482
  const connHeader = (parsed.headers['connection'] || '').toLowerCase()
457
483
  if (parsed.version === 'HTTP/1.1') c.keepAlive = connHeader.indexOf('close') < 0
458
484
  else c.keepAlive = connHeader.indexOf('keep-alive') >= 0
@@ -477,7 +503,9 @@ function dispatchRequest(c: Conn, parsed: ParsedHeader, body: Uint8Array, end: n
477
503
 
478
504
  c.ac = ac
479
505
  c.current = { req }
480
- trimParts(c, whole(c), end)
506
+ // `wholeBuf` is the concatenation the caller already produced — reuse it
507
+ // instead of concatenating the very same parts a second time.
508
+ trimParts(c, wholeBuf ?? whole(c), end)
481
509
  void handleRequest(c)
482
510
  }
483
511
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "quickwin",
4
- "version": "2.3.0",
4
+ "version": "2.3.1",
5
5
  "description": "QuickJS Win32 runtime — GUI, networking, WASM, FFI, and more",
6
6
  "bin": {
7
7
  "quickwin": "qwin.exe",
Binary file
package/qwin-nowasm.exe CHANGED
Binary file
package/qwin-x86.exe CHANGED
Binary file
package/qwin.exe CHANGED
Binary file