grpc-libp2p-client 0.0.38 → 0.0.40
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/dist/dc-http2/frame.cjs.js +51 -19
- package/dist/dc-http2/frame.cjs.js.map +1 -1
- package/dist/dc-http2/frame.d.ts +1 -1
- package/dist/dc-http2/frame.esm.js +51 -19
- package/dist/dc-http2/frame.esm.js.map +1 -1
- package/dist/dc-http2/hpack.cjs.js +43 -13
- package/dist/dc-http2/hpack.cjs.js.map +1 -1
- package/dist/dc-http2/hpack.esm.js +43 -13
- package/dist/dc-http2/hpack.esm.js.map +1 -1
- package/dist/dc-http2/parser.cjs.js +281 -189
- package/dist/dc-http2/parser.cjs.js.map +1 -1
- package/dist/dc-http2/parser.d.ts +21 -2
- package/dist/dc-http2/parser.esm.js +281 -189
- package/dist/dc-http2/parser.esm.js.map +1 -1
- package/dist/dc-http2/stream.cjs.js +111 -74
- package/dist/dc-http2/stream.cjs.js.map +1 -1
- package/dist/dc-http2/stream.d.ts +2 -0
- package/dist/dc-http2/stream.esm.js +111 -74
- package/dist/dc-http2/stream.esm.js.map +1 -1
- package/dist/grpc.js +824 -583
- package/dist/grpc.js.map +1 -1
- package/dist/grpc.min.js +1 -1
- package/dist/grpc.min.js.map +1 -1
- package/dist/index.cjs.js +647 -415
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +647 -415
- package/dist/index.esm.js.map +1 -1
- package/dist/stats.html +1 -1
- package/package.json +1 -1
- package/src/dc-http2/frame.ts +11 -9
- package/src/dc-http2/hpack.ts +43 -13
- package/src/dc-http2/parser.ts +219 -196
- package/src/dc-http2/stream.ts +99 -84
- package/src/index.ts +240 -183
package/dist/index.cjs.js
CHANGED
|
@@ -241,7 +241,6 @@ class HPACK {
|
|
|
241
241
|
this.dynamicTable.unshift([name, value]);
|
|
242
242
|
this.dynamicTableSize += size;
|
|
243
243
|
}
|
|
244
|
-
this.dynamicTable.push([name, value]);
|
|
245
244
|
}
|
|
246
245
|
// 获取索引的头部
|
|
247
246
|
getIndexedHeader(index) {
|
|
@@ -358,22 +357,29 @@ class HPACK {
|
|
|
358
357
|
// Huffman编码实现
|
|
359
358
|
huffmanEncode(bytes) {
|
|
360
359
|
const result = [];
|
|
360
|
+
// 使用高精度浮点数累积位,避免 JS 32-bit 有符号整数在位数 >31 时溢出。
|
|
361
|
+
// Huffman 码最长 30 bits,加上未输出的最多 7 bits = 37 bits,超过 32-bit 安全范围。
|
|
362
|
+
// Number 可精确表示 2^53 以内的整数,足够累积多个码字。
|
|
361
363
|
let current = 0;
|
|
362
364
|
let bits = 0;
|
|
363
365
|
for (let i = 0; i < bytes.length; i++) {
|
|
364
366
|
const b = bytes[i];
|
|
365
367
|
const code = this.huffmanTable.codes[b];
|
|
366
368
|
const length = this.huffmanTable.lengths[b];
|
|
369
|
+
// 用乘法左移替代 <<,避免 32-bit 截断
|
|
370
|
+
current = current * (1 << length) + code;
|
|
367
371
|
bits += length;
|
|
368
|
-
current = (current << length) | code;
|
|
369
372
|
while (bits >= 8) {
|
|
370
373
|
bits -= 8;
|
|
371
|
-
result.push((current
|
|
374
|
+
result.push(Math.floor(current / (1 << bits)) & 0xFF);
|
|
375
|
+
// 保留低 bits 位
|
|
376
|
+
current = current % (1 << bits);
|
|
372
377
|
}
|
|
373
378
|
}
|
|
374
|
-
//
|
|
379
|
+
// 处理剩余的位(用 EOS 填充 1)
|
|
375
380
|
if (bits > 0) {
|
|
376
|
-
|
|
381
|
+
const pad = 8 - bits;
|
|
382
|
+
current = current * (1 << pad) + ((1 << pad) - 1);
|
|
377
383
|
result.push(current & 0xFF);
|
|
378
384
|
}
|
|
379
385
|
return new Uint8Array(result);
|
|
@@ -396,8 +402,16 @@ class HPACK {
|
|
|
396
402
|
headers.set(name, value);
|
|
397
403
|
index = newIndex;
|
|
398
404
|
}
|
|
399
|
-
else if ((firstByte & 0x20) !== 0) { // 001xxxxx - Dynamic Table Size Update
|
|
400
|
-
index
|
|
405
|
+
else if ((firstByte & 0x20) !== 0) { // 001xxxxx - Dynamic Table Size Update (RFC 7541 §6.3)
|
|
406
|
+
const [newSize, newIndex] = this.decodeInteger(buffer, index, 5);
|
|
407
|
+
this.maxDynamicTableSize = newSize;
|
|
408
|
+
// evict entries that exceed the new limit
|
|
409
|
+
while (this.dynamicTableSize > this.maxDynamicTableSize && this.dynamicTable.length > 0) {
|
|
410
|
+
const entry = this.dynamicTable.pop();
|
|
411
|
+
if (entry)
|
|
412
|
+
this.dynamicTableSize -= entry[0].length + entry[1].length + 32;
|
|
413
|
+
}
|
|
414
|
+
index = newIndex;
|
|
401
415
|
}
|
|
402
416
|
else if ((firstByte & 0x10) !== 0) { // 0001xxxx - Literal Header Field Never Indexed
|
|
403
417
|
const [name, value, newIndex] = this.decodeLiteralHeaderWithoutIndexing(buffer, index);
|
|
@@ -469,18 +483,18 @@ class HPACK {
|
|
|
469
483
|
if (staticIndex <= 0) {
|
|
470
484
|
return ['', '', newIndex];
|
|
471
485
|
}
|
|
472
|
-
const headerField = this.
|
|
486
|
+
const headerField = this.getIndexedHeader(staticIndex);
|
|
473
487
|
if (!headerField) {
|
|
474
488
|
return ['', '', newIndex];
|
|
475
489
|
}
|
|
476
490
|
return [headerField[0], headerField[1], newIndex];
|
|
477
491
|
}
|
|
478
492
|
decodeLiteralHeaderWithIndexing(buffer, index) {
|
|
479
|
-
const [
|
|
480
|
-
index =
|
|
493
|
+
const [nameIndex, nextIndex] = this.decodeInteger(buffer, index, 6);
|
|
494
|
+
index = nextIndex;
|
|
481
495
|
let name;
|
|
482
|
-
if (
|
|
483
|
-
const headerField = this.
|
|
496
|
+
if (nameIndex > 0) {
|
|
497
|
+
const headerField = this.getIndexedHeader(nameIndex);
|
|
484
498
|
name = headerField ? headerField[0] : '';
|
|
485
499
|
}
|
|
486
500
|
else {
|
|
@@ -489,10 +503,26 @@ class HPACK {
|
|
|
489
503
|
index = newIndex;
|
|
490
504
|
}
|
|
491
505
|
const [value, finalIndex] = this.decodeLiteralString(buffer, index);
|
|
506
|
+
// RFC 7541 §6.2.1: Literal Header Field with Incremental Indexing must add to dynamic table
|
|
507
|
+
this.addToDynamicTable(name, value);
|
|
492
508
|
return [name, value, finalIndex];
|
|
493
509
|
}
|
|
494
510
|
decodeLiteralHeaderWithoutIndexing(buffer, index) {
|
|
495
|
-
|
|
511
|
+
// RFC 7541 §6.2.2 / §6.2.3: 4-bit prefix, do NOT add to dynamic table
|
|
512
|
+
const [nameIndex, nextIndex] = this.decodeInteger(buffer, index, 4);
|
|
513
|
+
index = nextIndex;
|
|
514
|
+
let name;
|
|
515
|
+
if (nameIndex > 0) {
|
|
516
|
+
const headerField = this.getIndexedHeader(nameIndex);
|
|
517
|
+
name = headerField ? headerField[0] : '';
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
const [decodedName, newIndex] = this.decodeLiteralString(buffer, index);
|
|
521
|
+
name = decodedName;
|
|
522
|
+
index = newIndex;
|
|
523
|
+
}
|
|
524
|
+
const [value, finalIndex] = this.decodeLiteralString(buffer, index);
|
|
525
|
+
return [name, value, finalIndex];
|
|
496
526
|
}
|
|
497
527
|
// 直接转换为字符串的方法
|
|
498
528
|
huffmanDecodeToString(bytes) {
|
|
@@ -580,9 +610,11 @@ const SETTINGS_PARAMETERS = {
|
|
|
580
610
|
};
|
|
581
611
|
const defaultSettings = {
|
|
582
612
|
[SETTINGS_PARAMETERS.HEADER_TABLE_SIZE]: 4096,
|
|
583
|
-
|
|
613
|
+
// gRPC 客户端不使用 Server Push,禁用以避免无效的 PUSH_PROMISE 处理
|
|
614
|
+
[SETTINGS_PARAMETERS.ENABLE_PUSH]: 0,
|
|
584
615
|
[SETTINGS_PARAMETERS.MAX_CONCURRENT_STREAMS]: 100,
|
|
585
|
-
|
|
616
|
+
// 匹配 parser 的实际接收缓冲区大小(4MB),避免服务端在单流上过早被限速
|
|
617
|
+
[SETTINGS_PARAMETERS.INITIAL_WINDOW_SIZE]: 4 << 20, // 4MB
|
|
586
618
|
[SETTINGS_PARAMETERS.MAX_FRAME_SIZE]: 16 << 10, // 16k
|
|
587
619
|
[SETTINGS_PARAMETERS.MAX_HEADER_LIST_SIZE]: 8192
|
|
588
620
|
};
|
|
@@ -643,8 +675,8 @@ class Http2Frame {
|
|
|
643
675
|
// Message-Data
|
|
644
676
|
grpcMessage.set(data, 5);
|
|
645
677
|
// 然后将完整的 gRPC 消息分割成多个 HTTP/2 DATA 帧
|
|
646
|
-
//
|
|
647
|
-
const maxDataPerFrame = maxFrameSize
|
|
678
|
+
// maxFrameSize 是 payload 上限(RFC 7540 §6.5.2 MAX_FRAME_SIZE),不含 9 字节帧头
|
|
679
|
+
const maxDataPerFrame = maxFrameSize;
|
|
648
680
|
for (let offset = 0; offset < grpcMessage.length; offset += maxDataPerFrame) {
|
|
649
681
|
const remaining = grpcMessage.length - offset;
|
|
650
682
|
const chunkSize = Math.min(maxDataPerFrame, remaining);
|
|
@@ -676,13 +708,13 @@ class Http2Frame {
|
|
|
676
708
|
const flags = endStream ? 0x01 : 0x0; // END_STREAM flag
|
|
677
709
|
return Http2Frame.createFrame(0x0, flags, streamId, framedData);
|
|
678
710
|
}
|
|
679
|
-
static createHeadersFrame(streamId, path, endHeaders = true, token) {
|
|
711
|
+
static createHeadersFrame(streamId, path, endHeaders = true, token, authority = 'localhost') {
|
|
680
712
|
// gRPC-Web 需要的标准 headers
|
|
681
713
|
const headersList = {
|
|
682
714
|
':path': path,
|
|
683
715
|
':method': 'POST',
|
|
684
716
|
':scheme': 'http',
|
|
685
|
-
':authority':
|
|
717
|
+
':authority': authority,
|
|
686
718
|
'content-type': 'application/grpc+proto',
|
|
687
719
|
'user-agent': 'grpc-web-client/0.1',
|
|
688
720
|
'accept': 'application/grpc+proto',
|
|
@@ -809,8 +841,15 @@ function _createPayload(settings) {
|
|
|
809
841
|
|
|
810
842
|
const HTTP2_PREFACE = new TextEncoder().encode("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
|
|
811
843
|
class HTTP2Parser {
|
|
844
|
+
/** 兼容旧代码读取 buffer —— 仅在必须全量访问时调用 _flattenBuffer() */
|
|
845
|
+
get buffer() { return this._flattenBuffer(); }
|
|
846
|
+
set buffer(v) { this.bufferChunks = v.length ? [v] : []; this.bufferTotalLength = v.length; }
|
|
812
847
|
constructor(writer, options) {
|
|
813
|
-
|
|
848
|
+
/** 分段缓冲:避免每次 chunk 到达时 O(n) 全量拷贝 */
|
|
849
|
+
this.bufferChunks = [];
|
|
850
|
+
this.bufferTotalLength = 0;
|
|
851
|
+
this.bufferChunks = [];
|
|
852
|
+
this.bufferTotalLength = 0;
|
|
814
853
|
this.settingsAckReceived = false;
|
|
815
854
|
this.peerSettingsReceived = false;
|
|
816
855
|
// 初始化连接级别的流控制窗口大小(默认值:65,535)
|
|
@@ -824,11 +863,38 @@ class HTTP2Parser {
|
|
|
824
863
|
this.sendStreamWindows = new Map();
|
|
825
864
|
this.peerInitialStreamWindow = 65535;
|
|
826
865
|
this.sendWindowWaiters = [];
|
|
866
|
+
this.settingsAckWaiters = [];
|
|
867
|
+
this.peerSettingsWaiters = [];
|
|
868
|
+
this.endOfStreamWaiters = [];
|
|
827
869
|
// 结束标志
|
|
828
870
|
this.endFlag = false;
|
|
829
871
|
this.writer = writer;
|
|
830
872
|
this.compatibilityMode = options?.compatibilityMode ?? false;
|
|
831
873
|
}
|
|
874
|
+
/** 将所有分段合并为一个连续 Uint8Array(仅在必要时调用)*/
|
|
875
|
+
_flattenBuffer() {
|
|
876
|
+
if (this.bufferChunks.length === 0)
|
|
877
|
+
return new Uint8Array(0);
|
|
878
|
+
if (this.bufferChunks.length === 1)
|
|
879
|
+
return this.bufferChunks[0];
|
|
880
|
+
const out = new Uint8Array(this.bufferTotalLength);
|
|
881
|
+
let off = 0;
|
|
882
|
+
for (const c of this.bufferChunks) {
|
|
883
|
+
out.set(c, off);
|
|
884
|
+
off += c.length;
|
|
885
|
+
}
|
|
886
|
+
return out;
|
|
887
|
+
}
|
|
888
|
+
/** 唤醒所有发送窗口等待者 */
|
|
889
|
+
_wakeWindowWaiters() {
|
|
890
|
+
const ws = this.sendWindowWaiters.splice(0);
|
|
891
|
+
for (const w of ws) {
|
|
892
|
+
try {
|
|
893
|
+
w.resolve();
|
|
894
|
+
}
|
|
895
|
+
catch { /* ignore */ }
|
|
896
|
+
}
|
|
897
|
+
}
|
|
832
898
|
// 持续处理流数据
|
|
833
899
|
async processStream(stream) {
|
|
834
900
|
try {
|
|
@@ -838,7 +904,6 @@ class HTTP2Parser {
|
|
|
838
904
|
}
|
|
839
905
|
// Stream 结束后的清理工作
|
|
840
906
|
if (!this.compatibilityMode && !this.endFlag) {
|
|
841
|
-
this.endFlag = true;
|
|
842
907
|
try {
|
|
843
908
|
this.onEnd?.();
|
|
844
909
|
}
|
|
@@ -846,51 +911,84 @@ class HTTP2Parser {
|
|
|
846
911
|
console.error("Error during onEnd callback:", err);
|
|
847
912
|
}
|
|
848
913
|
}
|
|
914
|
+
// 无论何种模式,stream 结束时都通知 waitForEndOfStream 等待者,
|
|
915
|
+
// 防止 compatibilityMode=true(server-streaming)时 waitForEndOfStream(0) 永久挂死
|
|
916
|
+
if (!this.endFlag) {
|
|
917
|
+
this._notifyEndOfStream();
|
|
918
|
+
}
|
|
849
919
|
}
|
|
850
920
|
catch (error) {
|
|
851
|
-
|
|
921
|
+
// abort() 触发的清理错误(如 'Call cleanup' / 'unaryCall cleanup')属于预期行为,降级为 debug 日志
|
|
922
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
923
|
+
const isAbortCleanup = /cleanup/i.test(errMsg) || /aborted/i.test(errMsg);
|
|
924
|
+
if (isAbortCleanup) {
|
|
925
|
+
console.debug("[processStream] stream aborted (expected):", errMsg);
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
console.error("Error processing stream:", error);
|
|
929
|
+
}
|
|
930
|
+
// 确保 waitForEndOfStream 等待者得到通知,防止 operationPromise 后台挂死
|
|
931
|
+
if (!this.endFlag) {
|
|
932
|
+
this._notifyEndOfStream();
|
|
933
|
+
}
|
|
852
934
|
throw error;
|
|
853
935
|
}
|
|
854
936
|
}
|
|
855
|
-
// 处理单个数据块
|
|
937
|
+
// 处理单个数据块 — 分段列表追加,避免每次 O(n) 全量拷贝
|
|
856
938
|
_processChunk(chunk) {
|
|
857
939
|
// chunk 是 Uint8ArrayList 或 Uint8Array
|
|
858
940
|
const newData = 'subarray' in chunk && typeof chunk.subarray === 'function'
|
|
859
941
|
? chunk.subarray()
|
|
860
942
|
: chunk;
|
|
861
|
-
//
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
943
|
+
// 追加到分段列表,O(1),不拷贝历史数据
|
|
944
|
+
if (newData.length > 0) {
|
|
945
|
+
this.bufferChunks.push(newData);
|
|
946
|
+
this.bufferTotalLength += newData.length;
|
|
947
|
+
}
|
|
948
|
+
// 将所有分段合并为一块后处理帧(只合并一次,后续 slice 替换)
|
|
949
|
+
// 仅在确实有完整帧时才触发合并,碎片仅 push 不合并
|
|
950
|
+
if (this.bufferTotalLength < 9)
|
|
951
|
+
return;
|
|
952
|
+
// 合并一次
|
|
953
|
+
const flat = this._flattenBuffer();
|
|
954
|
+
this.bufferChunks = [flat];
|
|
955
|
+
// bufferTotalLength 保持不变
|
|
866
956
|
// 持续处理所有完整的帧
|
|
867
957
|
let readOffset = 0;
|
|
868
|
-
while (
|
|
958
|
+
while (flat.length - readOffset >= 9) {
|
|
869
959
|
// 判断是否有HTTP/2前导
|
|
870
|
-
if (
|
|
960
|
+
if (flat.length - readOffset >= 24 && this.isHttp2Preface(flat.subarray(readOffset))) {
|
|
871
961
|
readOffset += 24;
|
|
872
962
|
// 发送SETTINGS帧
|
|
873
963
|
const settingFrame = Http2Frame.createSettingsFrame();
|
|
874
964
|
this.writer.write(settingFrame);
|
|
875
965
|
continue;
|
|
876
966
|
}
|
|
877
|
-
const frameHeader = this._parseFrameHeader(
|
|
967
|
+
const frameHeader = this._parseFrameHeader(flat.subarray(readOffset));
|
|
878
968
|
const totalFrameLength = 9 + frameHeader.length;
|
|
879
969
|
// 检查是否有完整的帧
|
|
880
|
-
if (
|
|
970
|
+
if (flat.length - readOffset < totalFrameLength) {
|
|
881
971
|
break;
|
|
882
972
|
}
|
|
883
|
-
//
|
|
884
|
-
const frameData =
|
|
973
|
+
// 获取完整帧数据(subarray 视图,零拷贝)
|
|
974
|
+
const frameData = flat.subarray(readOffset, readOffset + totalFrameLength);
|
|
885
975
|
// 处理不同类型的帧
|
|
886
976
|
this._handleFrame(frameHeader, frameData).catch((err) => {
|
|
887
977
|
console.error("Error handling frame:", err);
|
|
888
978
|
});
|
|
889
|
-
// 移动偏移量
|
|
890
979
|
readOffset += totalFrameLength;
|
|
891
980
|
}
|
|
981
|
+
// 保留未消费的尾部字节(slice 一次,后续仍分段追加)
|
|
892
982
|
if (readOffset > 0) {
|
|
893
|
-
|
|
983
|
+
if (readOffset >= flat.length) {
|
|
984
|
+
this.bufferChunks = [];
|
|
985
|
+
this.bufferTotalLength = 0;
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
const remaining = flat.slice(readOffset);
|
|
989
|
+
this.bufferChunks = [remaining];
|
|
990
|
+
this.bufferTotalLength = remaining.length;
|
|
991
|
+
}
|
|
894
992
|
}
|
|
895
993
|
}
|
|
896
994
|
isHttp2Preface(buffer) {
|
|
@@ -902,50 +1000,67 @@ class HTTP2Parser {
|
|
|
902
1000
|
}
|
|
903
1001
|
return true;
|
|
904
1002
|
}
|
|
905
|
-
//
|
|
906
|
-
_oldProcessStream_removed() {
|
|
907
|
-
// 这个方法已被上面的事件驱动实现替代
|
|
908
|
-
}
|
|
909
|
-
// 等待SETTINGS ACK
|
|
1003
|
+
// 等待SETTINGS ACK — 事件驱动,无轮询
|
|
910
1004
|
waitForSettingsAck() {
|
|
911
1005
|
return new Promise((resolve, reject) => {
|
|
912
1006
|
if (this.settingsAckReceived) {
|
|
913
1007
|
resolve();
|
|
914
1008
|
return;
|
|
915
1009
|
}
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
clearInterval(interval);
|
|
919
|
-
clearTimeout(timeout);
|
|
920
|
-
resolve();
|
|
921
|
-
}
|
|
922
|
-
}, 100);
|
|
1010
|
+
const waiter = { resolve, reject };
|
|
1011
|
+
this.settingsAckWaiters.push(waiter);
|
|
923
1012
|
const timeout = setTimeout(() => {
|
|
924
|
-
|
|
1013
|
+
const idx = this.settingsAckWaiters.indexOf(waiter);
|
|
1014
|
+
if (idx >= 0)
|
|
1015
|
+
this.settingsAckWaiters.splice(idx, 1);
|
|
925
1016
|
reject(new Error("Settings ACK timeout"));
|
|
926
1017
|
}, 30000);
|
|
1018
|
+
// 覆盖 resolve 以便超时前自动清理定时器
|
|
1019
|
+
waiter.resolve = () => { clearTimeout(timeout); resolve(); };
|
|
1020
|
+
waiter.reject = (e) => { clearTimeout(timeout); reject(e); };
|
|
927
1021
|
});
|
|
928
1022
|
}
|
|
929
|
-
|
|
1023
|
+
/** 内部调用:SETTINGS ACK 收到时唤醒所有等待者 */
|
|
1024
|
+
_notifySettingsAck() {
|
|
1025
|
+
this.settingsAckReceived = true;
|
|
1026
|
+
const ws = this.settingsAckWaiters.splice(0);
|
|
1027
|
+
for (const w of ws) {
|
|
1028
|
+
try {
|
|
1029
|
+
w.resolve();
|
|
1030
|
+
}
|
|
1031
|
+
catch { /* ignore */ }
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
// 等待接收来自对端的 SETTINGS(非 ACK)— 事件驱动,无轮询
|
|
930
1035
|
waitForPeerSettings(timeoutMs = 30000) {
|
|
931
1036
|
return new Promise((resolve, reject) => {
|
|
932
1037
|
if (this.peerSettingsReceived) {
|
|
933
1038
|
resolve();
|
|
934
1039
|
return;
|
|
935
1040
|
}
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
clearInterval(interval);
|
|
939
|
-
clearTimeout(timeout);
|
|
940
|
-
resolve();
|
|
941
|
-
}
|
|
942
|
-
}, 100);
|
|
1041
|
+
const waiter = { resolve, reject };
|
|
1042
|
+
this.peerSettingsWaiters.push(waiter);
|
|
943
1043
|
const timeout = setTimeout(() => {
|
|
944
|
-
|
|
1044
|
+
const idx = this.peerSettingsWaiters.indexOf(waiter);
|
|
1045
|
+
if (idx >= 0)
|
|
1046
|
+
this.peerSettingsWaiters.splice(idx, 1);
|
|
945
1047
|
reject(new Error("Peer SETTINGS timeout"));
|
|
946
1048
|
}, timeoutMs);
|
|
1049
|
+
waiter.resolve = () => { clearTimeout(timeout); resolve(); };
|
|
1050
|
+
waiter.reject = (e) => { clearTimeout(timeout); reject(e); };
|
|
947
1051
|
});
|
|
948
1052
|
}
|
|
1053
|
+
/** 内部调用:收到对端 SETTINGS(非 ACK)时唤醒等待者 */
|
|
1054
|
+
_notifyPeerSettings() {
|
|
1055
|
+
this.peerSettingsReceived = true;
|
|
1056
|
+
const ws = this.peerSettingsWaiters.splice(0);
|
|
1057
|
+
for (const w of ws) {
|
|
1058
|
+
try {
|
|
1059
|
+
w.resolve();
|
|
1060
|
+
}
|
|
1061
|
+
catch { /* ignore */ }
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
949
1064
|
// 注册我们要发送数据的出站流(用于初始化该流的对端窗口)
|
|
950
1065
|
registerOutboundStream(streamId) {
|
|
951
1066
|
if (!this.sendStreamWindows.has(streamId)) {
|
|
@@ -972,54 +1087,51 @@ class HTTP2Parser {
|
|
|
972
1087
|
this.sendConnWindow = Math.min(0x7fffffff, this.sendConnWindow + bytes);
|
|
973
1088
|
const cur = this.sendStreamWindows.get(streamId) ?? 0;
|
|
974
1089
|
this.sendStreamWindows.set(streamId, Math.min(0x7fffffff, cur + bytes));
|
|
1090
|
+
// 窗口增大,唤醒等待者
|
|
1091
|
+
this._wakeWindowWaiters();
|
|
975
1092
|
}
|
|
976
|
-
//
|
|
977
|
-
|
|
978
|
-
const
|
|
1093
|
+
// 等待可用发送窗口 — 事件驱动,WINDOW_UPDATE/SETTINGS 收到时直接唤醒
|
|
1094
|
+
waitForSendWindow(streamId, minBytes = 1, timeoutMs = 30000) {
|
|
1095
|
+
const { conn, stream } = this.getSendWindows(streamId);
|
|
1096
|
+
if (conn >= minBytes && stream >= minBytes)
|
|
1097
|
+
return Promise.resolve();
|
|
979
1098
|
return new Promise((resolve, reject) => {
|
|
980
|
-
let interval = null;
|
|
981
1099
|
let settled = false;
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1100
|
+
const timeout = timeoutMs > 0
|
|
1101
|
+
? setTimeout(() => {
|
|
1102
|
+
if (settled)
|
|
1103
|
+
return;
|
|
1104
|
+
settled = true;
|
|
1105
|
+
const idx = this.sendWindowWaiters.findIndex(w => w.resolve === resolveWrap);
|
|
1106
|
+
if (idx >= 0)
|
|
1107
|
+
this.sendWindowWaiters.splice(idx, 1);
|
|
1108
|
+
reject(new Error('Send window wait timeout'));
|
|
1109
|
+
}, timeoutMs)
|
|
1110
|
+
: undefined;
|
|
1111
|
+
const resolveWrap = () => {
|
|
1112
|
+
if (settled)
|
|
1113
|
+
return;
|
|
1114
|
+
const { conn: c2, stream: s2 } = this.getSendWindows(streamId);
|
|
1115
|
+
if (c2 >= minBytes && s2 >= minBytes) {
|
|
1116
|
+
settled = true;
|
|
1117
|
+
if (timeout)
|
|
1118
|
+
clearTimeout(timeout);
|
|
1119
|
+
resolve();
|
|
1120
|
+
}
|
|
1121
|
+
else {
|
|
1122
|
+
// 窗口仍不够,重新入队等待下一次更新
|
|
1123
|
+
this.sendWindowWaiters.push({ resolve: resolveWrap, reject: rejectWrap });
|
|
1005
1124
|
}
|
|
1006
|
-
return false;
|
|
1007
1125
|
};
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1126
|
+
const rejectWrap = (e) => {
|
|
1127
|
+
if (settled)
|
|
1128
|
+
return;
|
|
1129
|
+
settled = true;
|
|
1130
|
+
if (timeout)
|
|
1131
|
+
clearTimeout(timeout);
|
|
1132
|
+
reject(e);
|
|
1012
1133
|
};
|
|
1013
|
-
|
|
1014
|
-
// 简单的等待模型:依赖 WINDOW_UPDATE 到达时调用 wake
|
|
1015
|
-
this.sendWindowWaiters.push(wake);
|
|
1016
|
-
// 同时做一个轻微的轮询,防止错过唤醒
|
|
1017
|
-
interval = setInterval(() => {
|
|
1018
|
-
if (check() && interval) {
|
|
1019
|
-
clearInterval(interval);
|
|
1020
|
-
interval = null;
|
|
1021
|
-
}
|
|
1022
|
-
}, 50);
|
|
1134
|
+
this.sendWindowWaiters.push({ resolve: resolveWrap, reject: rejectWrap });
|
|
1023
1135
|
});
|
|
1024
1136
|
}
|
|
1025
1137
|
// 处理单个帧
|
|
@@ -1027,7 +1139,7 @@ class HTTP2Parser {
|
|
|
1027
1139
|
switch (frameHeader.type) {
|
|
1028
1140
|
case FRAME_TYPES.SETTINGS:
|
|
1029
1141
|
if ((frameHeader.flags & FRAME_FLAGS.ACK) === FRAME_FLAGS.ACK) {
|
|
1030
|
-
this.
|
|
1142
|
+
this._notifySettingsAck();
|
|
1031
1143
|
}
|
|
1032
1144
|
else {
|
|
1033
1145
|
//接收到Setting请求,进行解析
|
|
@@ -1037,10 +1149,12 @@ class HTTP2Parser {
|
|
|
1037
1149
|
for (let i = 0; i < settingsPayload.length; i += 6) {
|
|
1038
1150
|
// 正确解析:2字节ID + 4字节值
|
|
1039
1151
|
const id = (settingsPayload[i] << 8) | settingsPayload[i + 1];
|
|
1040
|
-
|
|
1152
|
+
// >>> 0 将结果转为无符号 32 位整数,防止高位为 1 时(如 0xffffffff)
|
|
1153
|
+
// 被 JS 按有符号解读为负数,导致 maxConcurrentStreams 等字段为负值
|
|
1154
|
+
const value = ((settingsPayload[i + 2] << 24) |
|
|
1041
1155
|
(settingsPayload[i + 3] << 16) |
|
|
1042
1156
|
(settingsPayload[i + 4] << 8) |
|
|
1043
|
-
settingsPayload[i + 5];
|
|
1157
|
+
settingsPayload[i + 5]) >>> 0;
|
|
1044
1158
|
if (id === 4) {
|
|
1045
1159
|
// SETTINGS_INITIAL_WINDOW_SIZE
|
|
1046
1160
|
this.defaultStreamWindowSize = value; // 我方接收窗口(入站)
|
|
@@ -1077,47 +1191,47 @@ class HTTP2Parser {
|
|
|
1077
1191
|
if (this.onSettings) {
|
|
1078
1192
|
this.onSettings(frameHeader);
|
|
1079
1193
|
}
|
|
1080
|
-
// 标记已收到对端 SETTINGS
|
|
1081
|
-
this.
|
|
1082
|
-
//
|
|
1083
|
-
|
|
1084
|
-
waiters.forEach(fn => { try {
|
|
1085
|
-
fn();
|
|
1086
|
-
}
|
|
1087
|
-
catch (e) {
|
|
1088
|
-
console.debug('waiter error', e);
|
|
1089
|
-
} });
|
|
1194
|
+
// 标记已收到对端 SETTINGS 并唤醒等待者
|
|
1195
|
+
this._notifyPeerSettings();
|
|
1196
|
+
// 唤醒发送窗口等待者(以防部分实现通过 SETTINGS 改变有效窗口)
|
|
1197
|
+
this._wakeWindowWaiters();
|
|
1090
1198
|
}
|
|
1091
1199
|
break;
|
|
1092
|
-
case FRAME_TYPES.DATA:
|
|
1200
|
+
case FRAME_TYPES.DATA: {
|
|
1093
1201
|
// 处理数据帧
|
|
1094
1202
|
if (this.onData) {
|
|
1095
1203
|
this.onData(frameData.slice(9), frameHeader); // 跳过帧头
|
|
1096
1204
|
}
|
|
1097
1205
|
// 更新流窗口和连接窗口
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1206
|
+
// 仅在帧有实际数据时才发送 WINDOW_UPDATE:
|
|
1207
|
+
// RFC 7540 §6.9.1 明确禁止 increment=0 的 WINDOW_UPDATE,
|
|
1208
|
+
// 服务端必须以 PROTOCOL_ERROR 响应,会导致连接被强制关闭。
|
|
1209
|
+
// 空 DATA 帧(如纯 END_STREAM 帧)length=0,不需要归还窗口。
|
|
1210
|
+
const dataLength = frameHeader.length ?? 0;
|
|
1211
|
+
if (dataLength > 0) {
|
|
1212
|
+
try {
|
|
1213
|
+
// 更新流级别的窗口
|
|
1214
|
+
if (frameHeader.streamId !== 0) {
|
|
1215
|
+
const streamWindowUpdate = Http2Frame.createWindowUpdateFrame(frameHeader.streamId, dataLength);
|
|
1216
|
+
this.writer.write(streamWindowUpdate);
|
|
1217
|
+
}
|
|
1218
|
+
// 更新连接级别的窗口
|
|
1219
|
+
const connWindowUpdate = Http2Frame.createWindowUpdateFrame(0, dataLength);
|
|
1220
|
+
this.writer.write(connWindowUpdate);
|
|
1221
|
+
}
|
|
1222
|
+
catch (err) {
|
|
1223
|
+
console.error("[HTTP2] Error sending window update:", err);
|
|
1103
1224
|
}
|
|
1104
|
-
// 更新连接级别的窗口
|
|
1105
|
-
const connWindowUpdate = Http2Frame.createWindowUpdateFrame(0, frameHeader.length ?? 0);
|
|
1106
|
-
this.writer.write(connWindowUpdate);
|
|
1107
|
-
}
|
|
1108
|
-
catch (err) {
|
|
1109
|
-
console.error("[HTTP2] Error sending window update:", err);
|
|
1110
1225
|
}
|
|
1111
1226
|
//判断是否是最后一个帧
|
|
1112
1227
|
if ((frameHeader.flags & FRAME_FLAGS.END_STREAM) ===
|
|
1113
1228
|
FRAME_FLAGS.END_STREAM) {
|
|
1114
|
-
this.
|
|
1115
|
-
|
|
1116
|
-
this.onEnd();
|
|
1117
|
-
}
|
|
1229
|
+
this.onEnd?.();
|
|
1230
|
+
this._notifyEndOfStream();
|
|
1118
1231
|
return;
|
|
1119
1232
|
}
|
|
1120
1233
|
break;
|
|
1234
|
+
}
|
|
1121
1235
|
case FRAME_TYPES.HEADERS:
|
|
1122
1236
|
// 处理头部帧
|
|
1123
1237
|
if (this.onHeaders) {
|
|
@@ -1126,35 +1240,26 @@ class HTTP2Parser {
|
|
|
1126
1240
|
//判断是否是最后一个帧
|
|
1127
1241
|
if ((frameHeader.flags & FRAME_FLAGS.END_STREAM) ===
|
|
1128
1242
|
FRAME_FLAGS.END_STREAM) {
|
|
1129
|
-
this.
|
|
1130
|
-
|
|
1131
|
-
this.onEnd();
|
|
1132
|
-
}
|
|
1243
|
+
this.onEnd?.();
|
|
1244
|
+
this._notifyEndOfStream();
|
|
1133
1245
|
return;
|
|
1134
1246
|
}
|
|
1135
1247
|
break;
|
|
1136
1248
|
case FRAME_TYPES.WINDOW_UPDATE:
|
|
1137
|
-
//
|
|
1138
|
-
this.handleWindowUpdateFrame(frameHeader, frameData);
|
|
1139
|
-
// 更新发送窗口(对端接收窗口)
|
|
1249
|
+
// 处理窗口更新帧(同时更新接收侧诊断计数器和发送侧流控窗口,只解析一次)
|
|
1140
1250
|
try {
|
|
1141
|
-
const
|
|
1251
|
+
const result = this.handleWindowUpdateFrame(frameHeader, frameData);
|
|
1252
|
+
// 更新发送方向窗口(对端的接收窗口)
|
|
1142
1253
|
if (frameHeader.streamId === 0) {
|
|
1143
|
-
this.sendConnWindow +=
|
|
1254
|
+
this.sendConnWindow += result.windowSizeIncrement;
|
|
1144
1255
|
}
|
|
1145
1256
|
else {
|
|
1146
1257
|
const cur = this.sendStreamWindows.get(frameHeader.streamId) ?? this.peerInitialStreamWindow;
|
|
1147
|
-
this.sendStreamWindows.set(frameHeader.streamId, cur +
|
|
1258
|
+
this.sendStreamWindows.set(frameHeader.streamId, cur + result.windowSizeIncrement);
|
|
1148
1259
|
}
|
|
1149
|
-
|
|
1150
|
-
waiters.forEach(fn => { try {
|
|
1151
|
-
fn();
|
|
1152
|
-
}
|
|
1153
|
-
catch (e) {
|
|
1154
|
-
console.debug('waiter error', e);
|
|
1155
|
-
} });
|
|
1260
|
+
this._wakeWindowWaiters();
|
|
1156
1261
|
}
|
|
1157
|
-
catch { /* ignore WINDOW_UPDATE parse errors */ }
|
|
1262
|
+
catch { /* ignore WINDOW_UPDATE parse errors (e.g. increment=0 is RFC PROTOCOL_ERROR) */ }
|
|
1158
1263
|
break;
|
|
1159
1264
|
case FRAME_TYPES.PING:
|
|
1160
1265
|
// 处理PING帧
|
|
@@ -1183,13 +1288,13 @@ class HTTP2Parser {
|
|
|
1183
1288
|
catch (err) {
|
|
1184
1289
|
console.error('Error during GOAWAY callback:', err);
|
|
1185
1290
|
}
|
|
1186
|
-
this.endFlag = true;
|
|
1187
1291
|
try {
|
|
1188
1292
|
this.onEnd?.();
|
|
1189
1293
|
}
|
|
1190
1294
|
catch (err) {
|
|
1191
1295
|
console.error('Error during GOAWAY onEnd callback:', err);
|
|
1192
1296
|
}
|
|
1297
|
+
this._notifyEndOfStream();
|
|
1193
1298
|
break;
|
|
1194
1299
|
}
|
|
1195
1300
|
// case FRAME_TYPES.PUSH_PROMISE:
|
|
@@ -1197,10 +1302,8 @@ class HTTP2Parser {
|
|
|
1197
1302
|
// this.handlePushPromiseFrame(frameHeader, frameData);
|
|
1198
1303
|
// break;
|
|
1199
1304
|
case FRAME_TYPES.RST_STREAM:
|
|
1200
|
-
this.
|
|
1201
|
-
|
|
1202
|
-
this.onEnd();
|
|
1203
|
-
}
|
|
1305
|
+
this.onEnd?.();
|
|
1306
|
+
this._notifyEndOfStream();
|
|
1204
1307
|
break;
|
|
1205
1308
|
default:
|
|
1206
1309
|
console.debug("Unknown frame type:", frameHeader.type);
|
|
@@ -1210,7 +1313,8 @@ class HTTP2Parser {
|
|
|
1210
1313
|
const length = (buffer[0] << 16) | (buffer[1] << 8) | buffer[2];
|
|
1211
1314
|
const type = buffer[3];
|
|
1212
1315
|
const flags = buffer[4];
|
|
1213
|
-
|
|
1316
|
+
// RFC 7540 §4.1: most significant bit is reserved and MUST be ignored on receipt
|
|
1317
|
+
const streamId = ((buffer[5] << 24) | (buffer[6] << 16) | (buffer[7] << 8) | buffer[8]) & 0x7fffffff;
|
|
1214
1318
|
return {
|
|
1215
1319
|
length,
|
|
1216
1320
|
type,
|
|
@@ -1239,52 +1343,40 @@ class HTTP2Parser {
|
|
|
1239
1343
|
throw error;
|
|
1240
1344
|
}
|
|
1241
1345
|
}
|
|
1242
|
-
|
|
1346
|
+
// 等待流结束 — 事件驱动,onEnd 触发时直接唤醒,无 setInterval 轮询
|
|
1243
1347
|
waitForEndOfStream(waitTime) {
|
|
1244
1348
|
return new Promise((resolve, reject) => {
|
|
1245
|
-
// If the stream has already ended, resolve immediately
|
|
1246
1349
|
if (this.endFlag) {
|
|
1247
1350
|
resolve();
|
|
1248
1351
|
return;
|
|
1249
1352
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1353
|
+
const waiter = { resolve, reject };
|
|
1354
|
+
this.endOfStreamWaiters.push(waiter);
|
|
1355
|
+
const timeout = waitTime > 0
|
|
1356
|
+
? setTimeout(() => {
|
|
1357
|
+
const idx = this.endOfStreamWaiters.indexOf(waiter);
|
|
1358
|
+
if (idx >= 0)
|
|
1359
|
+
this.endOfStreamWaiters.splice(idx, 1);
|
|
1255
1360
|
reject(new Error("End of stream timeout"));
|
|
1256
|
-
}, waitTime)
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
if (this.endFlag) {
|
|
1263
|
-
if (timeout !== null) {
|
|
1264
|
-
clearTimeout(timeout);
|
|
1265
|
-
}
|
|
1266
|
-
clearInterval(interval);
|
|
1267
|
-
resolve();
|
|
1268
|
-
}
|
|
1269
|
-
}, checkInterval);
|
|
1270
|
-
// If the onEnd is triggered externally, it should now be marked manually
|
|
1271
|
-
const originalOnEnd = this.onEnd;
|
|
1272
|
-
this.onEnd = () => {
|
|
1273
|
-
if (!this.endFlag) {
|
|
1274
|
-
// The external trigger may set endFlag; if not, handle here
|
|
1275
|
-
this.endFlag = true;
|
|
1276
|
-
}
|
|
1277
|
-
if (timeout !== null) {
|
|
1278
|
-
clearTimeout(timeout);
|
|
1279
|
-
}
|
|
1280
|
-
clearInterval(interval);
|
|
1281
|
-
resolve();
|
|
1282
|
-
if (originalOnEnd) {
|
|
1283
|
-
originalOnEnd(); // Call the original onEnd function if set
|
|
1284
|
-
}
|
|
1285
|
-
};
|
|
1361
|
+
}, waitTime)
|
|
1362
|
+
: null;
|
|
1363
|
+
waiter.resolve = () => { if (timeout)
|
|
1364
|
+
clearTimeout(timeout); resolve(); };
|
|
1365
|
+
waiter.reject = (e) => { if (timeout)
|
|
1366
|
+
clearTimeout(timeout); reject(e); };
|
|
1286
1367
|
});
|
|
1287
1368
|
}
|
|
1369
|
+
/** 内部调用:流结束时唤醒所有 waitForEndOfStream 等待者 */
|
|
1370
|
+
_notifyEndOfStream() {
|
|
1371
|
+
this.endFlag = true;
|
|
1372
|
+
const ws = this.endOfStreamWaiters.splice(0);
|
|
1373
|
+
for (const w of ws) {
|
|
1374
|
+
try {
|
|
1375
|
+
w.resolve();
|
|
1376
|
+
}
|
|
1377
|
+
catch { /* ignore */ }
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1288
1380
|
// 解析 WINDOW_UPDATE 帧
|
|
1289
1381
|
parseWindowUpdateFrame(frameBuffer, frameHeader) {
|
|
1290
1382
|
// WINDOW_UPDATE帧的payload固定为4字节
|
|
@@ -1343,6 +1435,8 @@ class StreamWriter {
|
|
|
1343
1435
|
this.lastBytesDrainedSeen = 0;
|
|
1344
1436
|
this.lastBpWarnAt = 0;
|
|
1345
1437
|
this.isHandlingError = false; // 防止重复错误处理
|
|
1438
|
+
/** drain 事件驱动等待者,替代 flush() 中的 setInterval 轮询 */
|
|
1439
|
+
this.drainWaiters = [];
|
|
1346
1440
|
// 事件系统
|
|
1347
1441
|
this.listeners = new Map();
|
|
1348
1442
|
// 验证 stream 参数
|
|
@@ -1444,8 +1538,9 @@ class StreamWriter {
|
|
|
1444
1538
|
// 使用 stream.send() 发送数据,返回 false 表示需要等待 drain
|
|
1445
1539
|
const canContinue = this.stream.send(chunk);
|
|
1446
1540
|
if (!canContinue) {
|
|
1447
|
-
//
|
|
1448
|
-
|
|
1541
|
+
// 传入 abort signal,当流被 abort 时 onDrain() 会立即 reject,
|
|
1542
|
+
// 避免在 abort 路径下永久挂住
|
|
1543
|
+
await this.stream.onDrain({ signal: this.abortController.signal });
|
|
1449
1544
|
}
|
|
1450
1545
|
}
|
|
1451
1546
|
catch (err) {
|
|
@@ -1461,6 +1556,9 @@ class StreamWriter {
|
|
|
1461
1556
|
throw err;
|
|
1462
1557
|
}
|
|
1463
1558
|
}
|
|
1559
|
+
// pipeline 正常结束(stream 关闭或 pushable 耗尽)—— 确保资源清理
|
|
1560
|
+
// 若已通过 abort() 触发则 cleanup() 内部幂等处理
|
|
1561
|
+
this.cleanup();
|
|
1464
1562
|
}
|
|
1465
1563
|
createTransform() {
|
|
1466
1564
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
@@ -1482,6 +1580,16 @@ class StreamWriter {
|
|
|
1482
1580
|
self.lastDrainEventAt = now;
|
|
1483
1581
|
self.dispatchEvent(new CustomEvent('drain', { detail: { drained: self.bytesDrained, queueSize: self.queueSize } }));
|
|
1484
1582
|
}
|
|
1583
|
+
// 唤醒所有在等 flush() 或背压解除 的 drainWaiters(队列降低时就可唤醒)
|
|
1584
|
+
if (self.drainWaiters.length > 0) {
|
|
1585
|
+
const ws = self.drainWaiters.splice(0);
|
|
1586
|
+
for (const fn of ws) {
|
|
1587
|
+
try {
|
|
1588
|
+
fn();
|
|
1589
|
+
}
|
|
1590
|
+
catch { /* ignore */ }
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1485
1593
|
// 记录本次已消耗字节,用于看门狗判断是否前进
|
|
1486
1594
|
self.lastBytesDrainedSeen = self.bytesDrained;
|
|
1487
1595
|
}
|
|
@@ -1493,10 +1601,11 @@ class StreamWriter {
|
|
|
1493
1601
|
};
|
|
1494
1602
|
}
|
|
1495
1603
|
// 简单的卡顿看门狗:当队列长期高位且 bytesDrained 无进展时发出 stalled 事件
|
|
1604
|
+
// 使用递归 setTimeout 而非 setInterval,避免标签页后台恢复时回调堆积导致 Violation
|
|
1496
1605
|
startWatchdog(intervalMs = 500, stallMs = 1500) {
|
|
1497
1606
|
if (this.watchdogTimer)
|
|
1498
1607
|
return;
|
|
1499
|
-
|
|
1608
|
+
const tick = () => {
|
|
1500
1609
|
if (this.abortController.signal.aborted)
|
|
1501
1610
|
return;
|
|
1502
1611
|
const baseThreshold = this.options.bufferSize * 0.7;
|
|
@@ -1507,7 +1616,13 @@ class StreamWriter {
|
|
|
1507
1616
|
if (!this.stallStartAt)
|
|
1508
1617
|
this.stallStartAt = now;
|
|
1509
1618
|
if (now - this.stallStartAt >= stallMs) {
|
|
1510
|
-
|
|
1619
|
+
// 异步触发事件,让当前 tick 立即返回,避免同步事件处理器阻塞主线程
|
|
1620
|
+
const detail = { queueSize: q, drained: this.bytesDrained, sinceMs: now - this.stallStartAt };
|
|
1621
|
+
setTimeout(() => {
|
|
1622
|
+
if (!this.abortController.signal.aborted) {
|
|
1623
|
+
this.dispatchEvent(new CustomEvent('stalled', { detail }));
|
|
1624
|
+
}
|
|
1625
|
+
}, 0);
|
|
1511
1626
|
// 避免持续触发,推进起点
|
|
1512
1627
|
this.stallStartAt = now;
|
|
1513
1628
|
}
|
|
@@ -1522,7 +1637,10 @@ class StreamWriter {
|
|
|
1522
1637
|
// 队列回落,重置
|
|
1523
1638
|
this.stallStartAt = 0;
|
|
1524
1639
|
}
|
|
1525
|
-
|
|
1640
|
+
// 本次 tick 完成后再安排下一次,不会因主线程繁忙而堆积
|
|
1641
|
+
this.watchdogTimer = setTimeout(tick, intervalMs);
|
|
1642
|
+
};
|
|
1643
|
+
this.watchdogTimer = setTimeout(tick, intervalMs);
|
|
1526
1644
|
}
|
|
1527
1645
|
async write(data) {
|
|
1528
1646
|
// 静默处理 aborted 状态,避免在正常的流关闭场景下抛出错误
|
|
@@ -1559,10 +1677,11 @@ class StreamWriter {
|
|
|
1559
1677
|
return data;
|
|
1560
1678
|
}
|
|
1561
1679
|
async writeChunks(buffer) {
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
const
|
|
1565
|
-
|
|
1680
|
+
const src = new Uint8Array(buffer);
|
|
1681
|
+
for (let offset = 0; offset < src.byteLength; offset += this.options.chunkSize) {
|
|
1682
|
+
const end = Math.min(offset + this.options.chunkSize, src.byteLength);
|
|
1683
|
+
// subarray 创建视图,不拷贝内存。pushable.push 不修改内容,安全。
|
|
1684
|
+
const chunk = src.subarray(offset, end);
|
|
1566
1685
|
await this.retryableWrite(chunk);
|
|
1567
1686
|
this.updateProgress(chunk.byteLength);
|
|
1568
1687
|
}
|
|
@@ -1583,18 +1702,12 @@ class StreamWriter {
|
|
|
1583
1702
|
if (this.abortController.signal.aborted) {
|
|
1584
1703
|
throw new Error('Stream aborted during backpressure monitoring');
|
|
1585
1704
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
this.p.push(chunk);
|
|
1589
|
-
}
|
|
1590
|
-
catch (err) {
|
|
1591
|
-
reject(err);
|
|
1592
|
-
}
|
|
1593
|
-
resolve();
|
|
1594
|
-
});
|
|
1705
|
+
// push 是同步操作,直接调用即可
|
|
1706
|
+
this.p.push(chunk);
|
|
1595
1707
|
}
|
|
1596
1708
|
catch (err) {
|
|
1597
|
-
|
|
1709
|
+
// aborted 时不重试,立即抛出
|
|
1710
|
+
if (!this.abortController.signal.aborted && attempt < this.options.retries) {
|
|
1598
1711
|
const delay = this.calculateRetryDelay(attempt);
|
|
1599
1712
|
await new Promise(r => setTimeout(r, delay));
|
|
1600
1713
|
return this.retryableWrite(chunk, attempt + 1);
|
|
@@ -1603,58 +1716,45 @@ class StreamWriter {
|
|
|
1603
1716
|
}
|
|
1604
1717
|
}
|
|
1605
1718
|
async monitorBackpressure() {
|
|
1606
|
-
const
|
|
1607
|
-
const
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
if (currentSize < baseThreshold) {
|
|
1719
|
+
const baseThreshold = this.options.bufferSize * 0.7;
|
|
1720
|
+
const criticalThreshold = this.options.bufferSize * 0.9;
|
|
1721
|
+
// 快速路径
|
|
1722
|
+
if (this.queueSize < baseThreshold) {
|
|
1611
1723
|
if (this.isBackpressure) {
|
|
1612
1724
|
this.isBackpressure = false;
|
|
1613
|
-
this.dispatchBackpressureEvent({
|
|
1614
|
-
currentSize,
|
|
1615
|
-
averageSize: this.getAverageQueueSize(),
|
|
1616
|
-
threshold: baseThreshold,
|
|
1617
|
-
waitingTime: 0
|
|
1618
|
-
});
|
|
1725
|
+
this.dispatchBackpressureEvent({ currentSize: this.queueSize, averageSize: this.getAverageQueueSize(), threshold: baseThreshold, waitingTime: 0 });
|
|
1619
1726
|
}
|
|
1620
1727
|
return;
|
|
1621
1728
|
}
|
|
1622
|
-
// 进入背压状态
|
|
1623
1729
|
if (!this.isBackpressure) {
|
|
1624
1730
|
this.isBackpressure = true;
|
|
1625
|
-
this.dispatchBackpressureEvent({
|
|
1626
|
-
currentSize,
|
|
1627
|
-
averageSize: this.getAverageQueueSize(),
|
|
1628
|
-
threshold: baseThreshold,
|
|
1629
|
-
waitingTime: 0
|
|
1630
|
-
});
|
|
1631
|
-
}
|
|
1632
|
-
// 智能等待策略
|
|
1633
|
-
const pressure = currentSize / this.options.bufferSize;
|
|
1634
|
-
let waitTime;
|
|
1635
|
-
if (currentSize >= criticalThreshold) {
|
|
1636
|
-
// 临界状态:长时间等待
|
|
1637
|
-
waitTime = 50 + Math.min(200, pressure * 100);
|
|
1638
|
-
}
|
|
1639
|
-
else {
|
|
1640
|
-
// 轻度背压:短时间等待
|
|
1641
|
-
waitTime = Math.min(20, pressure * 30);
|
|
1731
|
+
this.dispatchBackpressureEvent({ currentSize: this.queueSize, averageSize: this.getAverageQueueSize(), threshold: baseThreshold, waitingTime: 0 });
|
|
1642
1732
|
}
|
|
1643
|
-
//
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
while (this.queueSize >= baseThreshold && retryCount < maxRetries) {
|
|
1733
|
+
// 事件驱动等待:每轮等到 drain 触发或超时,最多 3 轮
|
|
1734
|
+
const maxRounds = 3;
|
|
1735
|
+
for (let i = 0; i < maxRounds; i++) {
|
|
1647
1736
|
if (this.abortController.signal.aborted)
|
|
1648
1737
|
break;
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1738
|
+
if (this.queueSize < baseThreshold)
|
|
1739
|
+
break;
|
|
1740
|
+
const isCritical = this.queueSize >= criticalThreshold;
|
|
1741
|
+
const waitMs = isCritical ? 100 : 30;
|
|
1742
|
+
await new Promise(resolve => {
|
|
1743
|
+
let done = false;
|
|
1744
|
+
const timer = setTimeout(() => { if (!done) {
|
|
1745
|
+
done = true;
|
|
1746
|
+
resolve();
|
|
1747
|
+
} }, waitMs);
|
|
1748
|
+
this.drainWaiters.push(() => { if (!done) {
|
|
1749
|
+
done = true;
|
|
1750
|
+
clearTimeout(timer);
|
|
1751
|
+
resolve();
|
|
1752
|
+
} });
|
|
1753
|
+
});
|
|
1653
1754
|
}
|
|
1654
|
-
// 如果仍然背压但达到最大重试次数,记录警告但继续执行
|
|
1655
1755
|
if (this.queueSize >= baseThreshold) {
|
|
1656
1756
|
const now = Date.now();
|
|
1657
|
-
if (now - this.lastBpWarnAt > 1000) {
|
|
1757
|
+
if (now - this.lastBpWarnAt > 1000) {
|
|
1658
1758
|
this.lastBpWarnAt = now;
|
|
1659
1759
|
console.warn(`Stream writer: High backpressure detected (${this.queueSize} bytes), continuing anyway`);
|
|
1660
1760
|
}
|
|
@@ -1746,11 +1846,21 @@ class StreamWriter {
|
|
|
1746
1846
|
if (!this.abortController.signal.aborted) {
|
|
1747
1847
|
this.abortController.abort();
|
|
1748
1848
|
}
|
|
1749
|
-
//
|
|
1849
|
+
// 执行所有待处理的写入任务:它们会检查 signal.aborted 并立即 resolve,
|
|
1850
|
+
// 不执行的话调用方的 Promise 会永远挂住
|
|
1750
1851
|
const pendingTasks = this.writeQueue.splice(0);
|
|
1751
|
-
pendingTasks
|
|
1752
|
-
|
|
1753
|
-
}
|
|
1852
|
+
for (const task of pendingTasks) {
|
|
1853
|
+
task().catch(() => { });
|
|
1854
|
+
}
|
|
1855
|
+
// 唤醒所有 drainWaiters(flush / monitorBackpressure 中的等待者),
|
|
1856
|
+
// 让它们检查 signal.aborted 并立即 resolve,不必等到各自的超时
|
|
1857
|
+
const ws = this.drainWaiters.splice(0);
|
|
1858
|
+
for (const fn of ws) {
|
|
1859
|
+
try {
|
|
1860
|
+
fn();
|
|
1861
|
+
}
|
|
1862
|
+
catch { /* ignore */ }
|
|
1863
|
+
}
|
|
1754
1864
|
try {
|
|
1755
1865
|
this.p.end();
|
|
1756
1866
|
}
|
|
@@ -1758,29 +1868,48 @@ class StreamWriter {
|
|
|
1758
1868
|
// Ignore errors when ending pushable
|
|
1759
1869
|
}
|
|
1760
1870
|
if (this.watchdogTimer) {
|
|
1761
|
-
|
|
1871
|
+
clearTimeout(this.watchdogTimer);
|
|
1762
1872
|
this.watchdogTimer = undefined;
|
|
1763
1873
|
}
|
|
1764
1874
|
}
|
|
1765
1875
|
// 等待内部队列被下游完全消费(用于在结束前确保尽量发送完数据)
|
|
1766
1876
|
// 默认超时 10s,避免无限等待
|
|
1767
1877
|
async flush(timeoutMs = 10000) {
|
|
1768
|
-
const start = Date.now();
|
|
1769
1878
|
// 快速路径
|
|
1770
1879
|
if (this.queueSize <= 0 && !this.isProcessingQueue && this.writeQueue.length === 0)
|
|
1771
1880
|
return;
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
if (this.queueSize <= 0 && !this.isProcessingQueue && this.writeQueue.length === 0)
|
|
1777
|
-
|
|
1778
|
-
if (Date.now() - start > timeoutMs) {
|
|
1779
|
-
console.warn(`Stream writer: flush timeout with ${this.queueSize} bytes still queued`);
|
|
1881
|
+
if (this.abortController.signal.aborted)
|
|
1882
|
+
return;
|
|
1883
|
+
await new Promise((resolve) => {
|
|
1884
|
+
// 已经清空
|
|
1885
|
+
if (this.queueSize <= 0 && !this.isProcessingQueue && this.writeQueue.length === 0) {
|
|
1886
|
+
resolve();
|
|
1780
1887
|
return;
|
|
1781
1888
|
}
|
|
1782
|
-
|
|
1783
|
-
|
|
1889
|
+
let done = false;
|
|
1890
|
+
const timer = setTimeout(() => {
|
|
1891
|
+
if (!done) {
|
|
1892
|
+
done = true;
|
|
1893
|
+
console.warn(`Stream writer: flush timeout with ${this.queueSize} bytes still queued`);
|
|
1894
|
+
resolve();
|
|
1895
|
+
}
|
|
1896
|
+
}, timeoutMs);
|
|
1897
|
+
// 由 createTransform 在每个 chunk 被下游消耗后唤醒
|
|
1898
|
+
const check = () => {
|
|
1899
|
+
if (this.abortController.signal.aborted || (this.queueSize <= 0 && !this.isProcessingQueue && this.writeQueue.length === 0)) {
|
|
1900
|
+
if (!done) {
|
|
1901
|
+
done = true;
|
|
1902
|
+
clearTimeout(timer);
|
|
1903
|
+
resolve();
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
else {
|
|
1907
|
+
// 下次 drain 时再检查
|
|
1908
|
+
this.drainWaiters.push(check);
|
|
1909
|
+
}
|
|
1910
|
+
};
|
|
1911
|
+
this.drainWaiters.push(check);
|
|
1912
|
+
});
|
|
1784
1913
|
}
|
|
1785
1914
|
addEventListener(type, callback) {
|
|
1786
1915
|
const handlers = this.listeners.get(type) || [];
|
|
@@ -2039,18 +2168,41 @@ class Libp2pGrpcClient {
|
|
|
2039
2168
|
setToken(token) {
|
|
2040
2169
|
this.token = token;
|
|
2041
2170
|
}
|
|
2171
|
+
/** 从 peerAddr 提取 HTTP/2 :authority 字段(host:port 格式) */
|
|
2172
|
+
getAuthority() {
|
|
2173
|
+
try {
|
|
2174
|
+
const addr = this.peerAddr.toString();
|
|
2175
|
+
const ip4 = addr.match(/\/ip4\/(\d[\d.]+)\/tcp\/(\d+)/);
|
|
2176
|
+
if (ip4)
|
|
2177
|
+
return `${ip4[1]}:${ip4[2]}`;
|
|
2178
|
+
const ip6 = addr.match(/\/ip6\/([^/]+)\/tcp\/(\d+)/);
|
|
2179
|
+
if (ip6)
|
|
2180
|
+
return `[${ip6[1]}]:${ip6[2]}`;
|
|
2181
|
+
const dns = addr.match(/\/dns(?:4|6)?\/([.\w-]+)\/tcp\/(\d+)/);
|
|
2182
|
+
if (dns)
|
|
2183
|
+
return `${dns[1]}:${dns[2]}`;
|
|
2184
|
+
}
|
|
2185
|
+
catch { /* ignore */ }
|
|
2186
|
+
return 'localhost';
|
|
2187
|
+
}
|
|
2042
2188
|
async unaryCall(method, requestData, timeout = 30000) {
|
|
2043
2189
|
let stream = null;
|
|
2044
2190
|
let responseData = null;
|
|
2045
2191
|
let responseBuffer = []; // 添加缓冲区来累积数据
|
|
2046
2192
|
let responseDataExpectedLength = -1; // 当前响应的期望长度
|
|
2193
|
+
/** 跨 DATA 帧的部分 gRPC 消息头缓冲(当一帧的 payload < 5 字节时积累) */
|
|
2194
|
+
let headerPartialBuffer = [];
|
|
2047
2195
|
const hpack = new HPACK();
|
|
2048
2196
|
let exitFlag = false;
|
|
2049
2197
|
let errMsg = "";
|
|
2050
2198
|
let isResponseComplete = false; // 添加标志来标识响应是否完成
|
|
2199
|
+
/** 事件驱动:响应完成时的唤醒函数 */
|
|
2200
|
+
let notifyResponseComplete = null;
|
|
2051
2201
|
let connection = null;
|
|
2052
2202
|
let state = null;
|
|
2053
2203
|
let streamSlotAcquired = false;
|
|
2204
|
+
// 提升 writer 作用域到 finally 可访问,确保错误路径下也能调用 abort() 清理资源
|
|
2205
|
+
let writerRef = null;
|
|
2054
2206
|
try {
|
|
2055
2207
|
// const stream = await this.node.dialProtocol(this.peerAddr, this.protocol)
|
|
2056
2208
|
connection = await this.acquireConnection(false);
|
|
@@ -2074,6 +2226,7 @@ class Libp2pGrpcClient {
|
|
|
2074
2226
|
const writer = new StreamWriter(stream, {
|
|
2075
2227
|
bufferSize: 16 * 1024 * 1024,
|
|
2076
2228
|
});
|
|
2229
|
+
writerRef = writer;
|
|
2077
2230
|
try {
|
|
2078
2231
|
writer.addEventListener("backpressure", (e) => {
|
|
2079
2232
|
const d = e.detail || {};
|
|
@@ -2104,6 +2257,7 @@ class Libp2pGrpcClient {
|
|
|
2104
2257
|
}
|
|
2105
2258
|
exitFlag = true;
|
|
2106
2259
|
errMsg = `GOAWAY received: code=${info.errorCode}`;
|
|
2260
|
+
notifyResponseComplete?.(); // 唤醒等待中的 Promise
|
|
2107
2261
|
try {
|
|
2108
2262
|
connection?.close();
|
|
2109
2263
|
}
|
|
@@ -2122,41 +2276,59 @@ class Libp2pGrpcClient {
|
|
|
2122
2276
|
parser.registerOutboundStream(streamId);
|
|
2123
2277
|
responseDataExpectedLength = -1; // 重置期望长度
|
|
2124
2278
|
responseBuffer = []; // 重置缓冲区
|
|
2279
|
+
headerPartialBuffer = []; // 重置跨帧头部缓冲
|
|
2125
2280
|
parser.onData = (payload, frameHeader) => {
|
|
2126
2281
|
//接收数据
|
|
2127
2282
|
if (responseDataExpectedLength === -1) {
|
|
2128
2283
|
//grpc消息头部未读取
|
|
2284
|
+
// 如果有跨帧积累的部分头字节,先与本帧 payload 合并
|
|
2285
|
+
let effectivePayload = payload;
|
|
2286
|
+
if (headerPartialBuffer.length > 0) {
|
|
2287
|
+
headerPartialBuffer.push(payload);
|
|
2288
|
+
const totalLen = headerPartialBuffer.reduce((s, c) => s + c.length, 0);
|
|
2289
|
+
effectivePayload = new Uint8Array(totalLen);
|
|
2290
|
+
let off = 0;
|
|
2291
|
+
for (const c of headerPartialBuffer) {
|
|
2292
|
+
effectivePayload.set(c, off);
|
|
2293
|
+
off += c.length;
|
|
2294
|
+
}
|
|
2295
|
+
headerPartialBuffer = [];
|
|
2296
|
+
}
|
|
2129
2297
|
//提取gRPC消息头部
|
|
2130
|
-
if (
|
|
2298
|
+
if (effectivePayload.length < 5) {
|
|
2299
|
+
// 头部字节不足 5,先缓存,等待后续帧补全
|
|
2300
|
+
headerPartialBuffer.push(effectivePayload);
|
|
2131
2301
|
return;
|
|
2132
2302
|
}
|
|
2133
|
-
const lengthBytes =
|
|
2134
|
-
responseDataExpectedLength = new DataView(lengthBytes.buffer, lengthBytes.byteOffset).getUint32(0, false); // big-endian
|
|
2135
|
-
if (responseDataExpectedLength
|
|
2136
|
-
throw new Error("Invalid gRPC message length");
|
|
2137
|
-
}
|
|
2138
|
-
if (responseDataExpectedLength + 5 > payload.length) {
|
|
2303
|
+
const lengthBytes = effectivePayload.slice(1, 5); // 消息长度的4字节
|
|
2304
|
+
responseDataExpectedLength = new DataView(lengthBytes.buffer, lengthBytes.byteOffset).getUint32(0, false); // big-endian(getUint32 返回无符号整数,结果不会为负)
|
|
2305
|
+
if (responseDataExpectedLength + 5 > effectivePayload.length) {
|
|
2139
2306
|
// 如果当前 payload 不足以包含完整的 gRPC 消息,缓存数据
|
|
2140
|
-
const grpcData =
|
|
2307
|
+
const grpcData = effectivePayload.subarray(5);
|
|
2141
2308
|
responseBuffer.push(grpcData);
|
|
2142
2309
|
responseDataExpectedLength -= grpcData.length; // 更新期望长度
|
|
2143
2310
|
return;
|
|
2144
2311
|
}
|
|
2145
2312
|
else {
|
|
2146
|
-
//
|
|
2147
|
-
const
|
|
2313
|
+
// payload 已包含完整的 gRPC 消息体,精确截取(避免尾部多余字节污染)
|
|
2314
|
+
const msgLen = responseDataExpectedLength;
|
|
2315
|
+
const grpcData = effectivePayload.slice(5, 5 + msgLen);
|
|
2148
2316
|
responseBuffer.push(grpcData);
|
|
2149
2317
|
responseData = grpcData;
|
|
2150
2318
|
isResponseComplete = true;
|
|
2151
|
-
responseDataExpectedLength = -1;
|
|
2319
|
+
responseDataExpectedLength = -1;
|
|
2320
|
+
notifyResponseComplete?.();
|
|
2152
2321
|
}
|
|
2153
2322
|
}
|
|
2154
2323
|
else if (responseDataExpectedLength > 0) {
|
|
2155
2324
|
//grpc消息头部已读取
|
|
2156
|
-
|
|
2157
|
-
responseDataExpectedLength -= payload.length; // 更新期望长度
|
|
2325
|
+
responseDataExpectedLength -= payload.length;
|
|
2158
2326
|
if (responseDataExpectedLength <= 0) {
|
|
2159
|
-
//
|
|
2327
|
+
// 超收时截掉多余字节
|
|
2328
|
+
const exactPayload = responseDataExpectedLength < 0
|
|
2329
|
+
? payload.slice(0, payload.length + responseDataExpectedLength)
|
|
2330
|
+
: payload;
|
|
2331
|
+
responseBuffer.push(exactPayload);
|
|
2160
2332
|
responseData = new Uint8Array(responseBuffer.reduce((sum, chunk) => sum + chunk.length, 0));
|
|
2161
2333
|
let offset = 0;
|
|
2162
2334
|
for (const chunk of responseBuffer) {
|
|
@@ -2164,41 +2336,36 @@ class Libp2pGrpcClient {
|
|
|
2164
2336
|
offset += chunk.length;
|
|
2165
2337
|
}
|
|
2166
2338
|
responseDataExpectedLength = -1;
|
|
2167
|
-
isResponseComplete = true;
|
|
2339
|
+
isResponseComplete = true;
|
|
2340
|
+
notifyResponseComplete?.();
|
|
2168
2341
|
}
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
if (frameHeader && frameHeader.flags & 0x1 && !isResponseComplete) {
|
|
2172
|
-
// END_STREAM flag
|
|
2173
|
-
// 合并所有缓冲的数据
|
|
2174
|
-
const totalLength = responseBuffer.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
2175
|
-
responseData = new Uint8Array(totalLength);
|
|
2176
|
-
let offset = 0;
|
|
2177
|
-
for (const chunk of responseBuffer) {
|
|
2178
|
-
responseData.set(chunk, offset);
|
|
2179
|
-
offset += chunk.length;
|
|
2342
|
+
else {
|
|
2343
|
+
responseBuffer.push(payload); // 还不完整,继续累积
|
|
2180
2344
|
}
|
|
2181
|
-
isResponseComplete = true;
|
|
2182
2345
|
}
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
isResponseComplete = true; // 设置响应完成标志
|
|
2188
|
-
if (responseBuffer.length === 0) {
|
|
2189
|
-
responseData = new Uint8Array(); // 如果没有数据,返回空数组
|
|
2190
|
-
}
|
|
2191
|
-
else {
|
|
2192
|
-
// 合并所有缓冲的数据
|
|
2193
|
-
const totalLength = responseBuffer.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
2346
|
+
// END_STREAM 兜底:数据路径已处理大多数情况;此分支仅在边缘情况下触发
|
|
2347
|
+
if (frameHeader && frameHeader.flags & 0x1 && !isResponseComplete) {
|
|
2348
|
+
if (responseBuffer.length > 0) {
|
|
2349
|
+
const totalLength = responseBuffer.reduce((sum, c) => sum + c.length, 0);
|
|
2194
2350
|
responseData = new Uint8Array(totalLength);
|
|
2195
2351
|
let offset = 0;
|
|
2196
2352
|
for (const chunk of responseBuffer) {
|
|
2197
2353
|
responseData.set(chunk, offset);
|
|
2198
2354
|
offset += chunk.length;
|
|
2199
2355
|
}
|
|
2200
|
-
isResponseComplete = true;
|
|
2201
2356
|
}
|
|
2357
|
+
else {
|
|
2358
|
+
responseData = new Uint8Array(0);
|
|
2359
|
+
}
|
|
2360
|
+
isResponseComplete = true;
|
|
2361
|
+
notifyResponseComplete?.();
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2364
|
+
parser.onEnd = () => {
|
|
2365
|
+
// 流结束时若响应未标记完成(空响应 / 纯 trailers),强制标记并唤醒等待者
|
|
2366
|
+
if (!isResponseComplete) {
|
|
2367
|
+
isResponseComplete = true;
|
|
2368
|
+
notifyResponseComplete?.();
|
|
2202
2369
|
}
|
|
2203
2370
|
};
|
|
2204
2371
|
parser.onSettings = () => {
|
|
@@ -2214,6 +2381,7 @@ class Libp2pGrpcClient {
|
|
|
2214
2381
|
else if (plainHeaders.get("grpc-status") !== undefined) {
|
|
2215
2382
|
exitFlag = true;
|
|
2216
2383
|
errMsg = plainHeaders.get("grpc-message") || "gRPC call failed";
|
|
2384
|
+
notifyResponseComplete?.(); // 唤醒等待中的 Promise
|
|
2217
2385
|
}
|
|
2218
2386
|
};
|
|
2219
2387
|
// 启动后台流处理,捕获任何异步错误
|
|
@@ -2223,6 +2391,7 @@ class Libp2pGrpcClient {
|
|
|
2223
2391
|
if (!errMsg) {
|
|
2224
2392
|
errMsg = error instanceof Error ? error.message : 'Stream processing failed';
|
|
2225
2393
|
}
|
|
2394
|
+
notifyResponseComplete?.(); // 流处理异常也需唤醒等待者
|
|
2226
2395
|
});
|
|
2227
2396
|
// 握手
|
|
2228
2397
|
const preface = Http2Frame.createPreface();
|
|
@@ -2231,14 +2400,16 @@ class Libp2pGrpcClient {
|
|
|
2231
2400
|
const settingFrme = Http2Frame.createSettingsFrame();
|
|
2232
2401
|
await writer.write(settingFrme);
|
|
2233
2402
|
// 等待对端 SETTINGS 或 ACK,择一即可,避免偶发握手竞态
|
|
2403
|
+
// 注意:未胜出的 promise 内部有超时定时器,它们最终会 reject。
|
|
2404
|
+
// 必须绑定 .catch(…) 消除错误,否则在 Node.js 新版本中会导致 UnhandledPromiseRejection 崩溃。
|
|
2234
2405
|
await Promise.race([
|
|
2235
|
-
parser.waitForPeerSettings(1000),
|
|
2236
|
-
parser.waitForSettingsAck(),
|
|
2406
|
+
parser.waitForPeerSettings(1000).catch(() => { }),
|
|
2407
|
+
parser.waitForSettingsAck().catch(() => { }),
|
|
2237
2408
|
new Promise((res) => setTimeout(res, 300)),
|
|
2238
2409
|
]);
|
|
2239
2410
|
// 即使未等到,也继续;多数实现会随后发送
|
|
2240
2411
|
// 创建头部帧
|
|
2241
|
-
const headerFrame = Http2Frame.createHeadersFrame(streamId, method, true, this.token);
|
|
2412
|
+
const headerFrame = Http2Frame.createHeadersFrame(streamId, method, true, this.token, this.getAuthority());
|
|
2242
2413
|
await writer.write(headerFrame);
|
|
2243
2414
|
// 直接按帧大小分片发送(保持与之前一致的稳定路径)
|
|
2244
2415
|
const dataFrames = Http2Frame.createDataFrames(streamId, requestData, true);
|
|
@@ -2246,22 +2417,21 @@ class Libp2pGrpcClient {
|
|
|
2246
2417
|
for (const df of dataFrames) {
|
|
2247
2418
|
await this.sendFrameWithFlowControl(parser, streamId, df, writer, undefined, frameSendTimeout);
|
|
2248
2419
|
}
|
|
2249
|
-
// 等待responseData
|
|
2420
|
+
// 等待 responseData 不为空,或超时(事件驱动,不轮询)
|
|
2250
2421
|
await new Promise((resolve, reject) => {
|
|
2422
|
+
if (isResponseComplete || exitFlag) {
|
|
2423
|
+
resolve();
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2251
2426
|
const t = setTimeout(() => {
|
|
2427
|
+
notifyResponseComplete = null;
|
|
2252
2428
|
reject(new Error("gRPC response timeout"));
|
|
2253
2429
|
}, timeout);
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
resolve(responseData);
|
|
2259
|
-
}
|
|
2260
|
-
else {
|
|
2261
|
-
setTimeout(checkResponse, 50);
|
|
2262
|
-
}
|
|
2430
|
+
notifyResponseComplete = () => {
|
|
2431
|
+
clearTimeout(t);
|
|
2432
|
+
notifyResponseComplete = null;
|
|
2433
|
+
resolve();
|
|
2263
2434
|
};
|
|
2264
|
-
checkResponse();
|
|
2265
2435
|
});
|
|
2266
2436
|
try {
|
|
2267
2437
|
await writer.flush(timeout);
|
|
@@ -2274,8 +2444,18 @@ class Libp2pGrpcClient {
|
|
|
2274
2444
|
throw err;
|
|
2275
2445
|
}
|
|
2276
2446
|
finally {
|
|
2447
|
+
// 必须先 abort writer(立即强制停止 pushable + stream),再 close stream。
|
|
2448
|
+
// 若顺序颠倒:stream.close() 会等待服务端半关闭确认,网络异常时永久挂住,
|
|
2449
|
+
// 导致 writer.abort() 永远不执行 → watchdog 定时器 / pushable 泄漏。
|
|
2450
|
+
// writer.abort() 内部幂等,成功路径下 writer.end() 已调用 cleanup(),安全。
|
|
2451
|
+
writerRef?.abort('unaryCall cleanup');
|
|
2277
2452
|
if (stream) {
|
|
2278
|
-
|
|
2453
|
+
try {
|
|
2454
|
+
await stream.close();
|
|
2455
|
+
}
|
|
2456
|
+
catch {
|
|
2457
|
+
// 流已被 abort,close() 会立即抛出,忽略即可。
|
|
2458
|
+
}
|
|
2279
2459
|
}
|
|
2280
2460
|
if (streamSlotAcquired && state) {
|
|
2281
2461
|
state.activeStreams = Math.max(0, state.activeStreams - 1);
|
|
@@ -2310,6 +2490,8 @@ class Libp2pGrpcClient {
|
|
|
2310
2490
|
const internalController = new AbortController();
|
|
2311
2491
|
let timeoutHandle;
|
|
2312
2492
|
let stream = null;
|
|
2493
|
+
// 保存外部 abort 监听器引用,以便操作结束后移除,防止内存泄漏
|
|
2494
|
+
let contextAbortHandler;
|
|
2313
2495
|
const profile = options?.transportProfile ?? this.getDefaultTransportProfile(mode);
|
|
2314
2496
|
const useFlowControl = profile === "flow-control";
|
|
2315
2497
|
// 取消函数 - 将在最后返回给调用者
|
|
@@ -2329,17 +2511,16 @@ class Libp2pGrpcClient {
|
|
|
2329
2511
|
};
|
|
2330
2512
|
// 如果提供了外部信号,监听它
|
|
2331
2513
|
if (context?.signal) {
|
|
2332
|
-
//
|
|
2514
|
+
// 如果外部信号已经触发中止,立即返回——避免启动 IIFE 后在 catch 中再次调用 onErrorCallback
|
|
2333
2515
|
if (context.signal.aborted) {
|
|
2334
2516
|
if (onErrorCallback) {
|
|
2335
2517
|
onErrorCallback(new Error("Operation aborted by context"));
|
|
2336
2518
|
}
|
|
2337
|
-
cancelOperation
|
|
2519
|
+
return cancelOperation;
|
|
2338
2520
|
}
|
|
2339
|
-
// 监听外部的abort
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
});
|
|
2521
|
+
// 监听外部的abort事件(保存引用以便后续移除,防止内存泄漏)
|
|
2522
|
+
contextAbortHandler = () => { cancelOperation(); };
|
|
2523
|
+
context.signal.addEventListener("abort", contextAbortHandler);
|
|
2343
2524
|
}
|
|
2344
2525
|
// 超时Promise
|
|
2345
2526
|
const timeoutPromise = new Promise((_, reject) => {
|
|
@@ -2350,13 +2531,45 @@ class Libp2pGrpcClient {
|
|
|
2350
2531
|
});
|
|
2351
2532
|
// 主操作Promise
|
|
2352
2533
|
const operationPromise = (async () => {
|
|
2353
|
-
|
|
2534
|
+
/**
|
|
2535
|
+
* 统一错误报告:确保 onErrorCallback 只被调用一次,
|
|
2536
|
+
* 并同时中止操作,防止后续再触发 onEndCallback。
|
|
2537
|
+
* 适用于 onGoaway / onHeaders / processStream.catch / onData 等各个错误路径。
|
|
2538
|
+
*/
|
|
2539
|
+
let errorCallbackFired = false;
|
|
2540
|
+
const reportError = (err) => {
|
|
2541
|
+
if (errorCallbackFired)
|
|
2542
|
+
return;
|
|
2543
|
+
errorCallbackFired = true;
|
|
2544
|
+
internalController.abort();
|
|
2545
|
+
if (onErrorCallback)
|
|
2546
|
+
onErrorCallback(err);
|
|
2547
|
+
};
|
|
2548
|
+
/** 分段列表缓冲,避免每次 payload 到达时 O(n) 全量拷贝 */
|
|
2549
|
+
let msgChunks = [];
|
|
2550
|
+
let msgTotalLen = 0;
|
|
2354
2551
|
let expectedMessageLength = -1; // 当前消息的期望长度
|
|
2552
|
+
/** 将分段列表合并为单一 Uint8Array(仅在需要时调用) */
|
|
2553
|
+
const flattenMsgBuffer = () => {
|
|
2554
|
+
if (msgChunks.length === 0)
|
|
2555
|
+
return new Uint8Array(0);
|
|
2556
|
+
if (msgChunks.length === 1)
|
|
2557
|
+
return msgChunks[0];
|
|
2558
|
+
const out = new Uint8Array(msgTotalLen);
|
|
2559
|
+
let off = 0;
|
|
2560
|
+
for (const c of msgChunks) {
|
|
2561
|
+
out.set(c, off);
|
|
2562
|
+
off += c.length;
|
|
2563
|
+
}
|
|
2564
|
+
return out;
|
|
2565
|
+
};
|
|
2355
2566
|
const hpack = new HPACK();
|
|
2356
2567
|
let connection = null;
|
|
2357
2568
|
let connectionKey = null;
|
|
2358
2569
|
let state = null;
|
|
2359
2570
|
let streamSlotAcquired = false;
|
|
2571
|
+
// 提升 writer 作用域到 finally 可访问,确保 unary/server-streaming 模式下也能清理资源
|
|
2572
|
+
let writer = null;
|
|
2360
2573
|
try {
|
|
2361
2574
|
// 检查是否已经中止
|
|
2362
2575
|
if (internalController.signal.aborted) {
|
|
@@ -2394,7 +2607,7 @@ class Libp2pGrpcClient {
|
|
|
2394
2607
|
});
|
|
2395
2608
|
const streamManager = this.getStreamManagerFor(connection);
|
|
2396
2609
|
const streamId = await streamManager.getNextAppLevelStreamId();
|
|
2397
|
-
|
|
2610
|
+
writer = new StreamWriter(stream, {
|
|
2398
2611
|
bufferSize: 16 * 1024 * 1024,
|
|
2399
2612
|
});
|
|
2400
2613
|
try {
|
|
@@ -2429,10 +2642,8 @@ class Libp2pGrpcClient {
|
|
|
2429
2642
|
if (state) {
|
|
2430
2643
|
this.rejectStreamWaiters(state, new Error("Connection received GOAWAY"));
|
|
2431
2644
|
}
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
}
|
|
2435
|
-
internalController.abort();
|
|
2645
|
+
// reportError 统一完成:标记已报错 + abort + 触发回调(幂等,不会重复触发)
|
|
2646
|
+
reportError(new Error(`GOAWAY received: code=${info.errorCode}`));
|
|
2436
2647
|
try {
|
|
2437
2648
|
connection?.close();
|
|
2438
2649
|
}
|
|
@@ -2470,52 +2681,43 @@ class Libp2pGrpcClient {
|
|
|
2470
2681
|
};
|
|
2471
2682
|
// 在各个回调中检查是否已中止
|
|
2472
2683
|
parser.onData = async (payload) => {
|
|
2473
|
-
|
|
2474
|
-
if (internalController.signal.aborted) {
|
|
2684
|
+
if (internalController.signal.aborted)
|
|
2475
2685
|
return;
|
|
2476
|
-
}
|
|
2477
2686
|
try {
|
|
2478
|
-
//
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
newBuffer.set(payload, messageBuffer.length);
|
|
2482
|
-
messageBuffer = newBuffer;
|
|
2687
|
+
// 追加到分段列表,O(1),不拷贝历史数据
|
|
2688
|
+
msgChunks.push(payload);
|
|
2689
|
+
msgTotalLen += payload.length;
|
|
2483
2690
|
// 处理缓冲区中的完整消息
|
|
2484
|
-
while (
|
|
2485
|
-
|
|
2486
|
-
if (internalController.signal.aborted) {
|
|
2691
|
+
while (msgTotalLen > 0) {
|
|
2692
|
+
if (internalController.signal.aborted)
|
|
2487
2693
|
return;
|
|
2694
|
+
// 读取 gRPC 消息头(5字节)
|
|
2695
|
+
if (expectedMessageLength === -1 && msgTotalLen >= 5) {
|
|
2696
|
+
const flat = flattenMsgBuffer();
|
|
2697
|
+
msgChunks = [flat];
|
|
2698
|
+
const lengthBytes = flat.slice(1, 5);
|
|
2699
|
+
expectedMessageLength = new DataView(lengthBytes.buffer, lengthBytes.byteOffset).getUint32(0, false);
|
|
2488
2700
|
}
|
|
2489
|
-
//
|
|
2490
|
-
if (expectedMessageLength
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
}
|
|
2495
|
-
// 如果知道期望长度且有足够数据
|
|
2496
|
-
if (expectedMessageLength !== -1 &&
|
|
2497
|
-
messageBuffer.length >= expectedMessageLength + 5) {
|
|
2498
|
-
// 提取完整消息(跳过5字节头部)
|
|
2499
|
-
const completeMessage = messageBuffer.slice(5, expectedMessageLength + 5);
|
|
2500
|
-
// 调用回调处理这个完整消息
|
|
2701
|
+
// 有完整消息
|
|
2702
|
+
if (expectedMessageLength !== -1 && msgTotalLen >= expectedMessageLength + 5) {
|
|
2703
|
+
const flat = flattenMsgBuffer();
|
|
2704
|
+
msgChunks = [flat];
|
|
2705
|
+
const completeMessage = flat.slice(5, expectedMessageLength + 5);
|
|
2501
2706
|
onDataCallback(completeMessage);
|
|
2502
|
-
//
|
|
2503
|
-
|
|
2707
|
+
// 移除已处理消息,保留剩余
|
|
2708
|
+
const remaining = flat.slice(expectedMessageLength + 5);
|
|
2709
|
+
msgChunks = remaining.length > 0 ? [remaining] : [];
|
|
2710
|
+
msgTotalLen = remaining.length;
|
|
2504
2711
|
expectedMessageLength = -1;
|
|
2505
2712
|
}
|
|
2506
2713
|
else {
|
|
2507
|
-
// 没有足够数据构成完整消息,等待更多数据
|
|
2508
2714
|
break;
|
|
2509
2715
|
}
|
|
2510
2716
|
}
|
|
2511
2717
|
}
|
|
2512
2718
|
catch (error) {
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
}
|
|
2516
|
-
else {
|
|
2517
|
-
throw error;
|
|
2518
|
-
}
|
|
2719
|
+
// reportError 统一报错并中止,防止 onEndCallback 在数据处理异常后仍被调用
|
|
2720
|
+
reportError(error);
|
|
2519
2721
|
}
|
|
2520
2722
|
};
|
|
2521
2723
|
parser.onSettings = () => {
|
|
@@ -2535,20 +2737,16 @@ class Libp2pGrpcClient {
|
|
|
2535
2737
|
}
|
|
2536
2738
|
else if (plainHeaders.get("grpc-status") !== undefined) {
|
|
2537
2739
|
const errMsg = plainHeaders.get("grpc-message") || "gRPC call failed";
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
onErrorCallback(err);
|
|
2541
|
-
}
|
|
2542
|
-
else {
|
|
2543
|
-
throw err;
|
|
2544
|
-
}
|
|
2740
|
+
// reportError 统一完成:标记已报错 + abort + 触发回调(幂等,不会重复触发)
|
|
2741
|
+
reportError(new Error(errMsg));
|
|
2545
2742
|
}
|
|
2546
2743
|
};
|
|
2547
2744
|
// 启动后台流处理
|
|
2548
2745
|
parser.processStream(stream).catch((error) => {
|
|
2549
|
-
|
|
2550
|
-
if (
|
|
2551
|
-
|
|
2746
|
+
// abort() 触发的清理错误属于预期行为,不打印错误日志,不重复触发回调
|
|
2747
|
+
if (!internalController.signal.aborted) {
|
|
2748
|
+
console.error('Error in processStream:', error);
|
|
2749
|
+
reportError(error);
|
|
2552
2750
|
}
|
|
2553
2751
|
});
|
|
2554
2752
|
// 检查是否已中止
|
|
@@ -2570,10 +2768,12 @@ class Libp2pGrpcClient {
|
|
|
2570
2768
|
throw new Error("Operation aborted");
|
|
2571
2769
|
}
|
|
2572
2770
|
// 等待对端 SETTINGS 或 ACK,择一即可,避免偶发握手竞态
|
|
2771
|
+
// 注意:未胜出的 promise 内部有超时定时器,它们最终会 reject。
|
|
2772
|
+
// 必须绑定 .catch(…) 消除错误,否则在 Node.js 新版本中会导致 UnhandledPromiseRejection 崩溃。
|
|
2573
2773
|
{
|
|
2574
2774
|
await Promise.race([
|
|
2575
|
-
parser.waitForPeerSettings(1000),
|
|
2576
|
-
parser.waitForSettingsAck(),
|
|
2775
|
+
parser.waitForPeerSettings(1000).catch(() => { }),
|
|
2776
|
+
parser.waitForSettingsAck().catch(() => { }),
|
|
2577
2777
|
new Promise((res) => setTimeout(res, 300)),
|
|
2578
2778
|
]);
|
|
2579
2779
|
// 即使未等到,也继续;多数实现会随后发送
|
|
@@ -2582,12 +2782,8 @@ class Libp2pGrpcClient {
|
|
|
2582
2782
|
if (internalController.signal.aborted) {
|
|
2583
2783
|
throw new Error("Operation aborted");
|
|
2584
2784
|
}
|
|
2585
|
-
// 检查是否已中止
|
|
2586
|
-
if (internalController.signal.aborted) {
|
|
2587
|
-
throw new Error("Operation aborted");
|
|
2588
|
-
}
|
|
2589
2785
|
// Create header frame
|
|
2590
|
-
const headerFrame = Http2Frame.createHeadersFrame(streamId, method, true, this.token);
|
|
2786
|
+
const headerFrame = Http2Frame.createHeadersFrame(streamId, method, true, this.token, this.getAuthority());
|
|
2591
2787
|
if (mode === "unary" || mode === "server-streaming") {
|
|
2592
2788
|
await writer.write(headerFrame);
|
|
2593
2789
|
const dfs = Http2Frame.createDataFrames(streamId, requestData, true);
|
|
@@ -2612,7 +2808,18 @@ class Libp2pGrpcClient {
|
|
|
2612
2808
|
const batchSize = options?.batchSize || 10;
|
|
2613
2809
|
// 动态批处理器
|
|
2614
2810
|
const processingQueue = [];
|
|
2811
|
+
/** 事件驱动:批处理完成后唤醒 waitForQueue 等待者 */
|
|
2812
|
+
const batchDoneWaiters = [];
|
|
2615
2813
|
let isProcessing = false;
|
|
2814
|
+
const _notifyBatchDone = () => {
|
|
2815
|
+
const ws = batchDoneWaiters.splice(0);
|
|
2816
|
+
for (const fn of ws) {
|
|
2817
|
+
try {
|
|
2818
|
+
fn();
|
|
2819
|
+
}
|
|
2820
|
+
catch { /* ignore */ }
|
|
2821
|
+
}
|
|
2822
|
+
};
|
|
2616
2823
|
const processNextBatch = async () => {
|
|
2617
2824
|
if (isProcessing || processingQueue.length === 0)
|
|
2618
2825
|
return;
|
|
@@ -2656,10 +2863,13 @@ class Libp2pGrpcClient {
|
|
|
2656
2863
|
finally {
|
|
2657
2864
|
isProcessing = false;
|
|
2658
2865
|
// 如果队列中还有数据,继续处理
|
|
2659
|
-
if (processingQueue.length > 0 &&
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2866
|
+
if (processingQueue.length > 0 && !internalController.signal.aborted) {
|
|
2867
|
+
// 直接递归调用(已是 async,自动让出事件循环)
|
|
2868
|
+
processNextBatch().catch((err) => { console.error("Error in processNextBatch:", err); });
|
|
2869
|
+
}
|
|
2870
|
+
else {
|
|
2871
|
+
// 队列清空,唤醒等待者
|
|
2872
|
+
_notifyBatchDone();
|
|
2663
2873
|
}
|
|
2664
2874
|
}
|
|
2665
2875
|
};
|
|
@@ -2709,34 +2919,30 @@ class Libp2pGrpcClient {
|
|
|
2709
2919
|
});
|
|
2710
2920
|
throw error;
|
|
2711
2921
|
}
|
|
2712
|
-
//
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
console.warn("Error rejecting timeout promise:", err);
|
|
2729
|
-
}
|
|
2730
|
-
});
|
|
2731
|
-
throw new Error("Queue processing timeout");
|
|
2732
|
-
}
|
|
2733
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
2734
|
-
}
|
|
2922
|
+
// 等待所有剩余的数据处理完成(事件驱动,无 10ms 轮询)
|
|
2923
|
+
await new Promise((resolve, reject) => {
|
|
2924
|
+
const check = () => {
|
|
2925
|
+
if (internalController.signal.aborted) {
|
|
2926
|
+
reject(new Error("Operation aborted"));
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
if (processingQueue.length === 0 && !isProcessing) {
|
|
2930
|
+
resolve();
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
// processNextBatch 结束时会通知这里
|
|
2934
|
+
batchDoneWaiters.push(check);
|
|
2935
|
+
};
|
|
2936
|
+
check();
|
|
2937
|
+
});
|
|
2735
2938
|
// 检查是否已中止
|
|
2736
2939
|
if (internalController.signal.aborted) {
|
|
2737
2940
|
throw new Error("Operation aborted");
|
|
2738
2941
|
}
|
|
2739
|
-
|
|
2942
|
+
// 发送纯 HTTP/2 END_STREAM 信号帧(0 字节 payload),而非带 gRPC 消息头的空消息。
|
|
2943
|
+
// createDataFrame 会额外附加 5 字节 gRPC 消息头 [0,0,0,0,0],服务端会将其解析
|
|
2944
|
+
// 为一个长度=0 的额外 gRPC 消息,而不仅仅是流结束信号,可能导致协议混淆。
|
|
2945
|
+
const finalFrame = Http2Frame.createFrame(0x0, 0x01, streamId, new Uint8Array(0));
|
|
2740
2946
|
await writeFrame(finalFrame);
|
|
2741
2947
|
// 在结束前尽量冲刷内部队列,避免服务器看到部分数据 + context canceled
|
|
2742
2948
|
try {
|
|
@@ -2749,9 +2955,24 @@ class Libp2pGrpcClient {
|
|
|
2749
2955
|
if (internalController.signal.aborted) {
|
|
2750
2956
|
throw new Error("Operation aborted");
|
|
2751
2957
|
}
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2958
|
+
// 仅在未中止时等待并回调:
|
|
2959
|
+
// 1. 若已中止(如 onHeaders gRPC 错误),跳过 waitForEndOfStream(0) 避免永久阻塞
|
|
2960
|
+
// (waitForEndOfStream(0) 无超时,需等到 processStream 自然结束,
|
|
2961
|
+
// 而 processStream 结束依赖 stream.close(),但 stream.close() 在 finally 中——形成死锁)
|
|
2962
|
+
// 2. 避免在 onErrorCallback 之后再调用 onEndCallback
|
|
2963
|
+
if (!internalController.signal.aborted) {
|
|
2964
|
+
await parser.waitForEndOfStream(0);
|
|
2965
|
+
// Yield one microtask tick so that processStream.catch (which calls
|
|
2966
|
+
// reportError + internalController.abort()) has a chance to run before
|
|
2967
|
+
// we check abort status. Without this yield, if the stream died
|
|
2968
|
+
// unexpectedly (network error), onEndCallback and onErrorCallback
|
|
2969
|
+
// could both fire because _notifyEndOfStream() is called in
|
|
2970
|
+
// processStream's catch block before the re-throw schedules the
|
|
2971
|
+
// .catch handler as a microtask.
|
|
2972
|
+
await Promise.resolve();
|
|
2973
|
+
if (!internalController.signal.aborted && onEndCallback) {
|
|
2974
|
+
onEndCallback();
|
|
2975
|
+
}
|
|
2755
2976
|
}
|
|
2756
2977
|
}
|
|
2757
2978
|
catch (err) {
|
|
@@ -2759,14 +2980,16 @@ class Libp2pGrpcClient {
|
|
|
2759
2980
|
if (internalController.signal.aborted &&
|
|
2760
2981
|
err instanceof Error &&
|
|
2761
2982
|
err.message === "Operation aborted") {
|
|
2762
|
-
|
|
2983
|
+
// onHeaders / onGoaway / processStream 错误已通过 reportError 处理,
|
|
2984
|
+
// 此处仅在回调尚未触发时才报告(外部取消/超时场景)
|
|
2985
|
+
if (!errorCallbackFired && onErrorCallback) {
|
|
2763
2986
|
onErrorCallback(new Error("Operation cancelled by user"));
|
|
2764
2987
|
}
|
|
2765
2988
|
}
|
|
2766
|
-
else if (onErrorCallback) {
|
|
2989
|
+
else if (!errorCallbackFired && onErrorCallback) {
|
|
2767
2990
|
onErrorCallback(err);
|
|
2768
2991
|
}
|
|
2769
|
-
else {
|
|
2992
|
+
else if (!errorCallbackFired) {
|
|
2770
2993
|
if (err instanceof Error) {
|
|
2771
2994
|
console.error("asyncCall error:", err.message);
|
|
2772
2995
|
}
|
|
@@ -2777,12 +3000,21 @@ class Libp2pGrpcClient {
|
|
|
2777
3000
|
}
|
|
2778
3001
|
finally {
|
|
2779
3002
|
clearTimeout(timeoutHandle);
|
|
3003
|
+
// 移除外部 abort 监听器,防止 AbortController 复用时触发迟到的 cancelOperation()
|
|
3004
|
+
if (contextAbortHandler && context?.signal) {
|
|
3005
|
+
context.signal.removeEventListener("abort", contextAbortHandler);
|
|
3006
|
+
}
|
|
3007
|
+
// 必须先 abort writer(立即强制停止 pushable + stream),再 close stream。
|
|
3008
|
+
// 若顺序颠倒:stream.close() 等待服务端半关闭确认,网络异常时永久挂住,
|
|
3009
|
+
// writer.abort() 永远不执行 → watchdog / pushable 泄漏。
|
|
3010
|
+
// abort() 内部幂等,重复调用安全。
|
|
3011
|
+
writer?.abort('Call cleanup');
|
|
2780
3012
|
if (stream) {
|
|
2781
3013
|
try {
|
|
2782
3014
|
await stream.close();
|
|
2783
3015
|
}
|
|
2784
|
-
catch
|
|
2785
|
-
|
|
3016
|
+
catch {
|
|
3017
|
+
// 流已被 abort,close() 会立即抛出,忽略即可。
|
|
2786
3018
|
}
|
|
2787
3019
|
}
|
|
2788
3020
|
// 如果本次强制使用了新连接,结束时尽量关闭它,避免连接泄漏
|