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