grpc-libp2p-client 0.0.39 → 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/src/index.ts CHANGED
@@ -319,6 +319,20 @@ export class Libp2pGrpcClient {
319
319
  this.token = token;
320
320
  }
321
321
 
322
+ /** 从 peerAddr 提取 HTTP/2 :authority 字段(host:port 格式) */
323
+ private getAuthority(): string {
324
+ try {
325
+ const addr = this.peerAddr.toString();
326
+ const ip4 = addr.match(/\/ip4\/(\d[\d.]+)\/tcp\/(\d+)/);
327
+ if (ip4) return `${ip4[1]}:${ip4[2]}`;
328
+ const ip6 = addr.match(/\/ip6\/([^/]+)\/tcp\/(\d+)/);
329
+ if (ip6) return `[${ip6[1]}]:${ip6[2]}`;
330
+ const dns = addr.match(/\/dns(?:4|6)?\/([.\w-]+)\/tcp\/(\d+)/);
331
+ if (dns) return `${dns[1]}:${dns[2]}`;
332
+ } catch { /* ignore */ }
333
+ return 'localhost';
334
+ }
335
+
322
336
  async unaryCall(
323
337
  method: string,
324
338
  requestData: Uint8Array,
@@ -328,13 +342,19 @@ export class Libp2pGrpcClient {
328
342
  let responseData: Uint8Array | null = null;
329
343
  let responseBuffer: Uint8Array[] = []; // 添加缓冲区来累积数据
330
344
  let responseDataExpectedLength = -1; // 当前响应的期望长度
345
+ /** 跨 DATA 帧的部分 gRPC 消息头缓冲(当一帧的 payload < 5 字节时积累) */
346
+ let headerPartialBuffer: Uint8Array[] = [];
331
347
  const hpack = new HPACK();
332
348
  let exitFlag = false;
333
349
  let errMsg = "";
334
350
  let isResponseComplete = false; // 添加标志来标识响应是否完成
351
+ /** 事件驱动:响应完成时的唤醒函数 */
352
+ let notifyResponseComplete: (() => void) | null = null;
335
353
  let connection: Connection | null = null;
336
354
  let state: ConnectionState | null = null;
337
355
  let streamSlotAcquired = false;
356
+ // 提升 writer 作用域到 finally 可访问,确保错误路径下也能调用 abort() 清理资源
357
+ let writerRef: StreamWriter | null = null;
338
358
  try {
339
359
  // const stream = await this.node.dialProtocol(this.peerAddr, this.protocol)
340
360
  connection = await this.acquireConnection(false);
@@ -357,6 +377,7 @@ export class Libp2pGrpcClient {
357
377
  const writer = new StreamWriter(stream, {
358
378
  bufferSize: 16 * 1024 * 1024,
359
379
  });
380
+ writerRef = writer;
360
381
  try {
361
382
  writer.addEventListener("backpressure", (e: CustomEvent) => {
362
383
  const d = e.detail || {};
@@ -392,6 +413,7 @@ export class Libp2pGrpcClient {
392
413
  }
393
414
  exitFlag = true;
394
415
  errMsg = `GOAWAY received: code=${info.errorCode}`;
416
+ notifyResponseComplete?.(); // 唤醒等待中的 Promise
395
417
  try {
396
418
  connection?.close();
397
419
  } catch (err) {
@@ -411,42 +433,57 @@ export class Libp2pGrpcClient {
411
433
  parser.registerOutboundStream(streamId);
412
434
  responseDataExpectedLength = -1; // 重置期望长度
413
435
  responseBuffer = []; // 重置缓冲区
436
+ headerPartialBuffer = []; // 重置跨帧头部缓冲
414
437
  parser.onData = (payload, frameHeader) => {
415
438
  //接收数据
416
439
  if (responseDataExpectedLength === -1) {
417
440
  //grpc消息头部未读取
441
+ // 如果有跨帧积累的部分头字节,先与本帧 payload 合并
442
+ let effectivePayload = payload;
443
+ if (headerPartialBuffer.length > 0) {
444
+ headerPartialBuffer.push(payload);
445
+ const totalLen = headerPartialBuffer.reduce((s, c) => s + c.length, 0);
446
+ effectivePayload = new Uint8Array(totalLen);
447
+ let off = 0;
448
+ for (const c of headerPartialBuffer) { effectivePayload.set(c, off); off += c.length; }
449
+ headerPartialBuffer = [];
450
+ }
418
451
  //提取gRPC消息头部
419
- if (payload.length < 5) {
452
+ if (effectivePayload.length < 5) {
453
+ // 头部字节不足 5,先缓存,等待后续帧补全
454
+ headerPartialBuffer.push(effectivePayload);
420
455
  return;
421
456
  }
422
- const lengthBytes = payload.slice(1, 5); // 消息长度的4字节
457
+ const lengthBytes = effectivePayload.slice(1, 5); // 消息长度的4字节
423
458
  responseDataExpectedLength = new DataView(
424
459
  lengthBytes.buffer,
425
460
  lengthBytes.byteOffset
426
- ).getUint32(0, false); // big-endian
427
- if (responseDataExpectedLength < 0) {
428
- throw new Error("Invalid gRPC message length");
429
- }
430
- if (responseDataExpectedLength + 5 > payload.length) {
461
+ ).getUint32(0, false); // big-endian(getUint32 返回无符号整数,结果不会为负)
462
+ if (responseDataExpectedLength + 5 > effectivePayload.length) {
431
463
  // 如果当前 payload 不足以包含完整的 gRPC 消息,缓存数据
432
- const grpcData = payload.subarray(5);
464
+ const grpcData = effectivePayload.subarray(5);
433
465
  responseBuffer.push(grpcData);
434
466
  responseDataExpectedLength -= grpcData.length; // 更新期望长度
435
467
  return;
436
468
  } else {
437
- // 如果当前 payload 足以包含完整的 gRPC 消息,重置缓冲区
438
- const grpcData = payload.subarray(5); // 提取完整的 gRPC 消息
469
+ // payload 已包含完整的 gRPC 消息体,精确截取(避免尾部多余字节污染)
470
+ const msgLen = responseDataExpectedLength;
471
+ const grpcData = effectivePayload.slice(5, 5 + msgLen);
439
472
  responseBuffer.push(grpcData);
440
473
  responseData = grpcData;
441
474
  isResponseComplete = true;
442
- responseDataExpectedLength = -1; // 重置期望长度
475
+ responseDataExpectedLength = -1;
476
+ notifyResponseComplete?.();
443
477
  }
444
478
  } else if (responseDataExpectedLength > 0) {
445
479
  //grpc消息头部已读取
446
- responseBuffer.push(payload); // 将数据添加到缓冲区
447
- responseDataExpectedLength -= payload.length; // 更新期望长度
480
+ responseDataExpectedLength -= payload.length;
448
481
  if (responseDataExpectedLength <= 0) {
449
- // 如果缓冲区中的数据已经完全处理,重置缓冲区
482
+ // 超收时截掉多余字节
483
+ const exactPayload = responseDataExpectedLength < 0
484
+ ? payload.slice(0, payload.length + responseDataExpectedLength)
485
+ : payload;
486
+ responseBuffer.push(exactPayload);
450
487
  responseData = new Uint8Array(
451
488
  responseBuffer.reduce((sum, chunk) => sum + chunk.length, 0)
452
489
  );
@@ -456,46 +493,31 @@ export class Libp2pGrpcClient {
456
493
  offset += chunk.length;
457
494
  }
458
495
  responseDataExpectedLength = -1;
459
- isResponseComplete = true; // 设置响应完成标志
496
+ isResponseComplete = true;
497
+ notifyResponseComplete?.();
498
+ } else {
499
+ responseBuffer.push(payload); // 还不完整,继续累积
460
500
  }
461
501
  }
462
- // 检查是否是流的最后一个帧(END_STREAM 标志)
502
+ // END_STREAM 兜底:数据路径已处理大多数情况;此分支仅在边缘情况下触发
463
503
  if (frameHeader && frameHeader.flags & 0x1 && !isResponseComplete) {
464
- // END_STREAM flag
465
- // 合并所有缓冲的数据
466
- const totalLength = responseBuffer.reduce(
467
- (sum, chunk) => sum + chunk.length,
468
- 0
469
- );
470
- responseData = new Uint8Array(totalLength);
471
- let offset = 0;
472
- for (const chunk of responseBuffer) {
473
- responseData.set(chunk, offset);
474
- offset += chunk.length;
504
+ if (responseBuffer.length > 0) {
505
+ const totalLength = responseBuffer.reduce((sum, c) => sum + c.length, 0);
506
+ responseData = new Uint8Array(totalLength);
507
+ let offset = 0;
508
+ for (const chunk of responseBuffer) { responseData.set(chunk, offset); offset += chunk.length; }
509
+ } else {
510
+ responseData = new Uint8Array(0);
475
511
  }
476
512
  isResponseComplete = true;
513
+ notifyResponseComplete?.();
477
514
  }
478
515
  };
479
516
  parser.onEnd = () => {
480
- //接收结束
517
+ // 流结束时若响应未标记完成(空响应 / 纯 trailers),强制标记并唤醒等待者
481
518
  if (!isResponseComplete) {
482
- isResponseComplete = true; // 设置响应完成标志
483
- if (responseBuffer.length === 0) {
484
- responseData = new Uint8Array(); // 如果没有数据,返回空数组
485
- } else {
486
- // 合并所有缓冲的数据
487
- const totalLength = responseBuffer.reduce(
488
- (sum, chunk) => sum + chunk.length,
489
- 0
490
- );
491
- responseData = new Uint8Array(totalLength);
492
- let offset = 0;
493
- for (const chunk of responseBuffer) {
494
- responseData.set(chunk, offset);
495
- offset += chunk.length;
496
- }
497
- isResponseComplete = true;
498
- }
519
+ isResponseComplete = true;
520
+ notifyResponseComplete?.();
499
521
  }
500
522
  };
501
523
  parser.onSettings = () => {
@@ -510,6 +532,7 @@ export class Libp2pGrpcClient {
510
532
  } else if (plainHeaders.get("grpc-status") !== undefined) {
511
533
  exitFlag = true;
512
534
  errMsg = plainHeaders.get("grpc-message") || "gRPC call failed";
535
+ notifyResponseComplete?.(); // 唤醒等待中的 Promise
513
536
  }
514
537
  };
515
538
  // 启动后台流处理,捕获任何异步错误
@@ -519,6 +542,7 @@ export class Libp2pGrpcClient {
519
542
  if (!errMsg) {
520
543
  errMsg = error instanceof Error ? error.message : 'Stream processing failed';
521
544
  }
545
+ notifyResponseComplete?.(); // 流处理异常也需唤醒等待者
522
546
  });
523
547
 
524
548
  // 握手
@@ -528,9 +552,11 @@ export class Libp2pGrpcClient {
528
552
  const settingFrme = Http2Frame.createSettingsFrame();
529
553
  await writer.write(settingFrme);
530
554
  // 等待对端 SETTINGS 或 ACK,择一即可,避免偶发握手竞态
555
+ // 注意:未胜出的 promise 内部有超时定时器,它们最终会 reject。
556
+ // 必须绑定 .catch(…) 消除错误,否则在 Node.js 新版本中会导致 UnhandledPromiseRejection 崩溃。
531
557
  await Promise.race([
532
- parser.waitForPeerSettings(1000),
533
- parser.waitForSettingsAck(),
558
+ parser.waitForPeerSettings(1000).catch(() => {}),
559
+ parser.waitForSettingsAck().catch(() => {}),
534
560
  new Promise<void>((res) => setTimeout(res, 300)),
535
561
  ]);
536
562
  // 即使未等到,也继续;多数实现会随后发送
@@ -539,7 +565,8 @@ export class Libp2pGrpcClient {
539
565
  streamId,
540
566
  method,
541
567
  true,
542
- this.token
568
+ this.token,
569
+ this.getAuthority()
543
570
  );
544
571
  await writer.write(headerFrame);
545
572
  // 直接按帧大小分片发送(保持与之前一致的稳定路径)
@@ -560,21 +587,18 @@ export class Libp2pGrpcClient {
560
587
  frameSendTimeout
561
588
  );
562
589
  }
563
- // 等待responseData 不为空,或超时
564
- await new Promise((resolve, reject) => {
590
+ // 等待 responseData 不为空,或超时(事件驱动,不轮询)
591
+ await new Promise<void>((resolve, reject) => {
592
+ if (isResponseComplete || exitFlag) { resolve(); return; }
565
593
  const t = setTimeout(() => {
594
+ notifyResponseComplete = null;
566
595
  reject(new Error("gRPC response timeout"));
567
596
  }, timeout);
568
- const checkResponse = () => {
569
- if (isResponseComplete || exitFlag) {
570
- // 使用新的完成标志
571
- clearTimeout(t);
572
- resolve(responseData);
573
- } else {
574
- setTimeout(checkResponse, 50);
575
- }
597
+ notifyResponseComplete = () => {
598
+ clearTimeout(t);
599
+ notifyResponseComplete = null;
600
+ resolve();
576
601
  };
577
- checkResponse();
578
602
  });
579
603
  try {
580
604
  await writer.flush(timeout);
@@ -584,8 +608,17 @@ export class Libp2pGrpcClient {
584
608
  console.error("unaryCall error:", err);
585
609
  throw err;
586
610
  } finally {
611
+ // 必须先 abort writer(立即强制停止 pushable + stream),再 close stream。
612
+ // 若顺序颠倒:stream.close() 会等待服务端半关闭确认,网络异常时永久挂住,
613
+ // 导致 writer.abort() 永远不执行 → watchdog 定时器 / pushable 泄漏。
614
+ // writer.abort() 内部幂等,成功路径下 writer.end() 已调用 cleanup(),安全。
615
+ writerRef?.abort('unaryCall cleanup');
587
616
  if (stream) {
588
- await stream.close();
617
+ try {
618
+ await stream.close();
619
+ } catch {
620
+ // 流已被 abort,close() 会立即抛出,忽略即可。
621
+ }
589
622
  }
590
623
  if (streamSlotAcquired && state) {
591
624
  state.activeStreams = Math.max(0, state.activeStreams - 1);
@@ -632,6 +665,8 @@ export class Libp2pGrpcClient {
632
665
  const internalController = new AbortController();
633
666
  let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
634
667
  let stream: Stream | null = null;
668
+ // 保存外部 abort 监听器引用,以便操作结束后移除,防止内存泄漏
669
+ let contextAbortHandler: (() => void) | undefined;
635
670
 
636
671
  const profile: TransportProfile =
637
672
  options?.transportProfile ?? this.getDefaultTransportProfile(mode);
@@ -654,18 +689,17 @@ export class Libp2pGrpcClient {
654
689
 
655
690
  // 如果提供了外部信号,监听它
656
691
  if (context?.signal) {
657
- // 如果外部信号已经触发中止,立即返回
692
+ // 如果外部信号已经触发中止,立即返回——避免启动 IIFE 后在 catch 中再次调用 onErrorCallback
658
693
  if (context.signal.aborted) {
659
694
  if (onErrorCallback) {
660
695
  onErrorCallback(new Error("Operation aborted by context"));
661
696
  }
662
- cancelOperation();
697
+ return cancelOperation;
663
698
  }
664
699
 
665
- // 监听外部的abort事件
666
- context.signal.addEventListener("abort", () => {
667
- cancelOperation();
668
- });
700
+ // 监听外部的abort事件(保存引用以便后续移除,防止内存泄漏)
701
+ contextAbortHandler = () => { cancelOperation(); };
702
+ context.signal.addEventListener("abort", contextAbortHandler);
669
703
  }
670
704
 
671
705
  // 超时Promise
@@ -678,13 +712,39 @@ export class Libp2pGrpcClient {
678
712
 
679
713
  // 主操作Promise
680
714
  const operationPromise = (async () => {
681
- let messageBuffer = new Uint8Array(0); // 用于累积跨帧的消息数据
715
+ /**
716
+ * 统一错误报告:确保 onErrorCallback 只被调用一次,
717
+ * 并同时中止操作,防止后续再触发 onEndCallback。
718
+ * 适用于 onGoaway / onHeaders / processStream.catch / onData 等各个错误路径。
719
+ */
720
+ let errorCallbackFired = false;
721
+ const reportError = (err: unknown) => {
722
+ if (errorCallbackFired) return;
723
+ errorCallbackFired = true;
724
+ internalController.abort();
725
+ if (onErrorCallback) onErrorCallback(err);
726
+ };
727
+
728
+ /** 分段列表缓冲,避免每次 payload 到达时 O(n) 全量拷贝 */
729
+ let msgChunks: Uint8Array[] = [];
730
+ let msgTotalLen = 0;
682
731
  let expectedMessageLength = -1; // 当前消息的期望长度
732
+ /** 将分段列表合并为单一 Uint8Array(仅在需要时调用) */
733
+ const flattenMsgBuffer = (): Uint8Array => {
734
+ if (msgChunks.length === 0) return new Uint8Array(0);
735
+ if (msgChunks.length === 1) return msgChunks[0];
736
+ const out = new Uint8Array(msgTotalLen);
737
+ let off = 0;
738
+ for (const c of msgChunks) { out.set(c, off); off += c.length; }
739
+ return out;
740
+ };
683
741
  const hpack = new HPACK();
684
742
  let connection: Connection | null = null;
685
743
  let connectionKey: string | null = null;
686
744
  let state: ConnectionState | null = null;
687
745
  let streamSlotAcquired = false;
746
+ // 提升 writer 作用域到 finally 可访问,确保 unary/server-streaming 模式下也能清理资源
747
+ let writer: StreamWriter | null = null;
688
748
 
689
749
  try {
690
750
  // 检查是否已经中止
@@ -734,7 +794,7 @@ export class Libp2pGrpcClient {
734
794
  });
735
795
  const streamManager = this.getStreamManagerFor(connection as object);
736
796
  const streamId = await streamManager.getNextAppLevelStreamId();
737
- const writer = new StreamWriter(stream, {
797
+ writer = new StreamWriter(stream, {
738
798
  bufferSize: 16 * 1024 * 1024,
739
799
  });
740
800
  try {
@@ -756,11 +816,11 @@ export class Libp2pGrpcClient {
756
816
  const payload = new Uint8Array(8);
757
817
  crypto.getRandomValues?.(payload);
758
818
  const ping = Http2Frame.createFrame(0x6, 0x0, 0, payload);
759
- writer.write(ping);
819
+ writer!.write(ping);
760
820
  } catch { /* ignore ping write errors */ }
761
821
  });
762
822
  } catch { /* ignore addEventListener errors */ }
763
- const parser = new HTTP2Parser(writer, {
823
+ const parser = new HTTP2Parser(writer!, {
764
824
  compatibilityMode: !useFlowControl,
765
825
  });
766
826
  parser.onGoaway = (info) => {
@@ -774,10 +834,8 @@ export class Libp2pGrpcClient {
774
834
  new Error("Connection received GOAWAY")
775
835
  );
776
836
  }
777
- if (onErrorCallback) {
778
- onErrorCallback(new Error(`GOAWAY received: code=${info.errorCode}`));
779
- }
780
- internalController.abort();
837
+ // reportError 统一完成:标记已报错 + abort + 触发回调(幂等,不会重复触发)
838
+ reportError(new Error(`GOAWAY received: code=${info.errorCode}`));
781
839
  try {
782
840
  connection?.close();
783
841
  } catch (err) {
@@ -805,7 +863,7 @@ export class Libp2pGrpcClient {
805
863
  parser,
806
864
  streamId,
807
865
  frame,
808
- writer,
866
+ writer!,
809
867
  internalController.signal,
810
868
  sendWindowTimeout
811
869
  );
@@ -813,7 +871,7 @@ export class Libp2pGrpcClient {
813
871
  if (internalController.signal.aborted) {
814
872
  throw new Error("Operation aborted");
815
873
  }
816
- await writer.write(frame);
874
+ await writer!.write(frame);
817
875
  }
818
876
  };
819
877
  const writeDataFrames = async (frames: Uint8Array[]) => {
@@ -823,66 +881,47 @@ export class Libp2pGrpcClient {
823
881
  };
824
882
 
825
883
  // 在各个回调中检查是否已中止
826
- parser.onData = async (payload): Promise<void> => {
827
- // 检查是否已中止
828
- if (internalController.signal.aborted) {
829
- return;
830
- }
884
+ parser.onData = async (payload): Promise<void> => {
885
+ if (internalController.signal.aborted) return;
831
886
 
832
887
  try {
833
- // 将新数据添加到消息缓冲区
834
- const newBuffer = new Uint8Array(
835
- messageBuffer.length + payload.length
836
- );
837
- newBuffer.set(messageBuffer);
838
- newBuffer.set(payload, messageBuffer.length);
839
- messageBuffer = newBuffer;
888
+ // 追加到分段列表,O(1),不拷贝历史数据
889
+ msgChunks.push(payload);
890
+ msgTotalLen += payload.length;
840
891
 
841
892
  // 处理缓冲区中的完整消息
842
- while (messageBuffer.length > 0) {
843
- // 如果已经中止,停止处理
844
- if (internalController.signal.aborted) {
845
- return;
846
- }
847
-
848
- // 如果还没有读取消息长度,且缓冲区有足够数据
849
- if (expectedMessageLength === -1 && messageBuffer.length >= 5) {
850
- // 读取 gRPC 消息头:1字节压缩标志 + 4字节长度
851
- const lengthBytes = messageBuffer.slice(1, 5);
893
+ while (msgTotalLen > 0) {
894
+ if (internalController.signal.aborted) return;
895
+
896
+ // 读取 gRPC 消息头(5字节)
897
+ if (expectedMessageLength === -1 && msgTotalLen >= 5) {
898
+ const flat = flattenMsgBuffer();
899
+ msgChunks = [flat];
900
+ const lengthBytes = flat.slice(1, 5);
852
901
  expectedMessageLength = new DataView(
853
902
  lengthBytes.buffer,
854
903
  lengthBytes.byteOffset
855
- ).getUint32(0, false); // big-endian
904
+ ).getUint32(0, false);
856
905
  }
857
906
 
858
- // 如果知道期望长度且有足够数据
859
- if (
860
- expectedMessageLength !== -1 &&
861
- messageBuffer.length >= expectedMessageLength + 5
862
- ) {
863
- // 提取完整消息(跳过5字节头部)
864
- const completeMessage = messageBuffer.slice(
865
- 5,
866
- expectedMessageLength + 5
867
- );
868
-
869
- // 调用回调处理这个完整消息
907
+ // 有完整消息
908
+ if (expectedMessageLength !== -1 && msgTotalLen >= expectedMessageLength + 5) {
909
+ const flat = flattenMsgBuffer();
910
+ msgChunks = [flat];
911
+ const completeMessage = flat.slice(5, expectedMessageLength + 5);
870
912
  onDataCallback(completeMessage);
871
-
872
- // 移除已处理的消息,保留剩余数据
873
- messageBuffer = messageBuffer.slice(expectedMessageLength + 5);
913
+ // 移除已处理消息,保留剩余
914
+ const remaining = flat.slice(expectedMessageLength + 5);
915
+ msgChunks = remaining.length > 0 ? [remaining] : [];
916
+ msgTotalLen = remaining.length;
874
917
  expectedMessageLength = -1;
875
918
  } else {
876
- // 没有足够数据构成完整消息,等待更多数据
877
919
  break;
878
920
  }
879
921
  }
880
922
  } catch (error: unknown) {
881
- if (onErrorCallback) {
882
- onErrorCallback(error);
883
- } else {
884
- throw error;
885
- }
923
+ // reportError 统一报错并中止,防止 onEndCallback 在数据处理异常后仍被调用
924
+ reportError(error);
886
925
  }
887
926
  };
888
927
 
@@ -891,7 +930,7 @@ export class Libp2pGrpcClient {
891
930
  if (internalController.signal.aborted) return;
892
931
 
893
932
  const ackSettingFrame = Http2Frame.createSettingsAckFrame();
894
- writer.write(ackSettingFrame);
933
+ writer!.write(ackSettingFrame);
895
934
  };
896
935
 
897
936
  parser.onHeaders = (headers) => {
@@ -904,19 +943,16 @@ export class Libp2pGrpcClient {
904
943
  } else if (plainHeaders.get("grpc-status") !== undefined) {
905
944
  const errMsg =
906
945
  plainHeaders.get("grpc-message") || "gRPC call failed";
907
- const err = new Error(errMsg);
908
- if (onErrorCallback) {
909
- onErrorCallback(err);
910
- } else {
911
- throw err;
912
- }
946
+ // reportError 统一完成:标记已报错 + abort + 触发回调(幂等,不会重复触发)
947
+ reportError(new Error(errMsg));
913
948
  }
914
949
  };
915
950
  // 启动后台流处理
916
951
  parser.processStream(stream).catch((error: unknown) => {
917
- console.error('Error in processStream:', error);
918
- if (onErrorCallback) {
919
- onErrorCallback(error);
952
+ // abort() 触发的清理错误属于预期行为,不打印错误日志,不重复触发回调
953
+ if (!internalController.signal.aborted) {
954
+ console.error('Error in processStream:', error);
955
+ reportError(error);
920
956
  }
921
957
  });
922
958
 
@@ -944,10 +980,12 @@ export class Libp2pGrpcClient {
944
980
  }
945
981
 
946
982
  // 等待对端 SETTINGS 或 ACK,择一即可,避免偶发握手竞态
983
+ // 注意:未胜出的 promise 内部有超时定时器,它们最终会 reject。
984
+ // 必须绑定 .catch(…) 消除错误,否则在 Node.js 新版本中会导致 UnhandledPromiseRejection 崩溃。
947
985
  {
948
986
  await Promise.race([
949
- parser.waitForPeerSettings(1000),
950
- parser.waitForSettingsAck(),
987
+ parser.waitForPeerSettings(1000).catch(() => {}),
988
+ parser.waitForSettingsAck().catch(() => {}),
951
989
  new Promise<void>((res) => setTimeout(res, 300)),
952
990
  ]);
953
991
  // 即使未等到,也继续;多数实现会随后发送
@@ -958,17 +996,13 @@ export class Libp2pGrpcClient {
958
996
  throw new Error("Operation aborted");
959
997
  }
960
998
 
961
- // 检查是否已中止
962
- if (internalController.signal.aborted) {
963
- throw new Error("Operation aborted");
964
- }
965
-
966
999
  // Create header frame
967
1000
  const headerFrame = Http2Frame.createHeadersFrame(
968
1001
  streamId,
969
1002
  method,
970
1003
  true,
971
- this.token
1004
+ this.token,
1005
+ this.getAuthority()
972
1006
  );
973
1007
  if (mode === "unary" || mode === "server-streaming") {
974
1008
  await writer.write(headerFrame);
@@ -1009,8 +1043,16 @@ export class Libp2pGrpcClient {
1009
1043
  reject: (reason?: unknown) => void;
1010
1044
  }[] = [];
1011
1045
 
1046
+ /** 事件驱动:批处理完成后唤醒 waitForQueue 等待者 */
1047
+ const batchDoneWaiters: Array<() => void> = [];
1048
+
1012
1049
  let isProcessing = false;
1013
1050
 
1051
+ const _notifyBatchDone = () => {
1052
+ const ws = batchDoneWaiters.splice(0);
1053
+ for (const fn of ws) { try { fn(); } catch { /* ignore */ } }
1054
+ };
1055
+
1014
1056
  const processNextBatch = async () => {
1015
1057
  if (isProcessing || processingQueue.length === 0) return;
1016
1058
  isProcessing = true;
@@ -1065,12 +1107,12 @@ export class Libp2pGrpcClient {
1065
1107
  isProcessing = false;
1066
1108
 
1067
1109
  // 如果队列中还有数据,继续处理
1068
- if (
1069
- processingQueue.length > 0 &&
1070
- !internalController.signal.aborted
1071
- ) {
1072
- // 使用 setTimeout 避免阻塞,让新数据有机会加入队列
1073
- setTimeout(() => processNextBatch(), 0);
1110
+ if (processingQueue.length > 0 && !internalController.signal.aborted) {
1111
+ // 直接递归调用(已是 async,自动让出事件循环)
1112
+ processNextBatch().catch((err) => { console.error("Error in processNextBatch:", err); });
1113
+ } else {
1114
+ // 队列清空,唤醒等待者
1115
+ _notifyBatchDone();
1074
1116
  }
1075
1117
  }
1076
1118
  };
@@ -1128,42 +1170,32 @@ export class Libp2pGrpcClient {
1128
1170
  throw error;
1129
1171
  }
1130
1172
 
1131
- // 等待所有剩余的数据处理完成,添加超时保护
1132
- const queueWaitStart = Date.now();
1133
- const maxQueueWaitMs = timeout; // 使用主超时时间
1134
-
1135
- while (processingQueue.length > 0 || isProcessing) {
1136
- if (internalController.signal.aborted) {
1137
- throw new Error("Operation aborted");
1138
- }
1139
-
1140
- // 防止无限等待
1141
- if (Date.now() - queueWaitStart > maxQueueWaitMs) {
1142
- // 清理剩余队列
1143
- const remainingQueue = processingQueue.splice(0);
1144
- remainingQueue.forEach((item) => {
1145
- try {
1146
- item.reject(new Error("Queue wait timeout"));
1147
- } catch (err) {
1148
- console.warn("Error rejecting timeout promise:", err);
1149
- }
1150
- });
1151
- throw new Error("Queue processing timeout");
1152
- }
1153
-
1154
- await new Promise((resolve) => setTimeout(resolve, 10));
1155
- }
1173
+ // 等待所有剩余的数据处理完成(事件驱动,无 10ms 轮询)
1174
+ await new Promise<void>((resolve, reject) => {
1175
+ const check = () => {
1176
+ if (internalController.signal.aborted) {
1177
+ reject(new Error("Operation aborted"));
1178
+ return;
1179
+ }
1180
+ if (processingQueue.length === 0 && !isProcessing) {
1181
+ resolve();
1182
+ return;
1183
+ }
1184
+ // processNextBatch 结束时会通知这里
1185
+ batchDoneWaiters.push(check);
1186
+ };
1187
+ check();
1188
+ });
1156
1189
 
1157
1190
  // 检查是否已中止
1158
1191
  if (internalController.signal.aborted) {
1159
1192
  throw new Error("Operation aborted");
1160
1193
  }
1161
1194
 
1162
- const finalFrame = Http2Frame.createDataFrame(
1163
- streamId,
1164
- new Uint8Array(),
1165
- true
1166
- );
1195
+ // 发送纯 HTTP/2 END_STREAM 信号帧(0 字节 payload),而非带 gRPC 消息头的空消息。
1196
+ // createDataFrame 会额外附加 5 字节 gRPC 消息头 [0,0,0,0,0],服务端会将其解析
1197
+ // 为一个长度=0 的额外 gRPC 消息,而不仅仅是流结束信号,可能导致协议混淆。
1198
+ const finalFrame = Http2Frame.createFrame(0x0, 0x01, streamId, new Uint8Array(0));
1167
1199
  await writeFrame(finalFrame);
1168
1200
  // 在结束前尽量冲刷内部队列,避免服务器看到部分数据 + context canceled
1169
1201
  try {
@@ -1177,10 +1209,24 @@ export class Libp2pGrpcClient {
1177
1209
  throw new Error("Operation aborted");
1178
1210
  }
1179
1211
 
1180
- await parser.waitForEndOfStream(0);
1181
-
1182
- if (onEndCallback) {
1183
- onEndCallback();
1212
+ // 仅在未中止时等待并回调:
1213
+ // 1. 若已中止(如 onHeaders gRPC 错误),跳过 waitForEndOfStream(0) 避免永久阻塞
1214
+ // (waitForEndOfStream(0) 无超时,需等到 processStream 自然结束,
1215
+ // 而 processStream 结束依赖 stream.close(),但 stream.close() 在 finally 中——形成死锁)
1216
+ // 2. 避免在 onErrorCallback 之后再调用 onEndCallback
1217
+ if (!internalController.signal.aborted) {
1218
+ await parser.waitForEndOfStream(0);
1219
+ // Yield one microtask tick so that processStream.catch (which calls
1220
+ // reportError + internalController.abort()) has a chance to run before
1221
+ // we check abort status. Without this yield, if the stream died
1222
+ // unexpectedly (network error), onEndCallback and onErrorCallback
1223
+ // could both fire because _notifyEndOfStream() is called in
1224
+ // processStream's catch block before the re-throw schedules the
1225
+ // .catch handler as a microtask.
1226
+ await Promise.resolve();
1227
+ if (!internalController.signal.aborted && onEndCallback) {
1228
+ onEndCallback();
1229
+ }
1184
1230
  }
1185
1231
  } catch (err: unknown) {
1186
1232
  // 如果是由于取消导致的错误,使用特定的错误消息
@@ -1189,12 +1235,14 @@ export class Libp2pGrpcClient {
1189
1235
  err instanceof Error &&
1190
1236
  err.message === "Operation aborted"
1191
1237
  ) {
1192
- if (onErrorCallback) {
1238
+ // onHeaders / onGoaway / processStream 错误已通过 reportError 处理,
1239
+ // 此处仅在回调尚未触发时才报告(外部取消/超时场景)
1240
+ if (!errorCallbackFired && onErrorCallback) {
1193
1241
  onErrorCallback(new Error("Operation cancelled by user"));
1194
1242
  }
1195
- } else if (onErrorCallback) {
1243
+ } else if (!errorCallbackFired && onErrorCallback) {
1196
1244
  onErrorCallback(err);
1197
- } else {
1245
+ } else if (!errorCallbackFired) {
1198
1246
  if (err instanceof Error) {
1199
1247
  console.error("asyncCall error:", err.message);
1200
1248
  } else {
@@ -1203,11 +1251,20 @@ export class Libp2pGrpcClient {
1203
1251
  }
1204
1252
  } finally {
1205
1253
  clearTimeout(timeoutHandle);
1254
+ // 移除外部 abort 监听器,防止 AbortController 复用时触发迟到的 cancelOperation()
1255
+ if (contextAbortHandler && context?.signal) {
1256
+ context.signal.removeEventListener("abort", contextAbortHandler);
1257
+ }
1258
+ // 必须先 abort writer(立即强制停止 pushable + stream),再 close stream。
1259
+ // 若顺序颠倒:stream.close() 等待服务端半关闭确认,网络异常时永久挂住,
1260
+ // writer.abort() 永远不执行 → watchdog / pushable 泄漏。
1261
+ // abort() 内部幂等,重复调用安全。
1262
+ writer?.abort('Call cleanup');
1206
1263
  if (stream) {
1207
1264
  try {
1208
1265
  await stream.close();
1209
- } catch (err) {
1210
- console.error("Error closing stream:", err);
1266
+ } catch {
1267
+ // 流已被 abort,close() 会立即抛出,忽略即可。
1211
1268
  }
1212
1269
  }
1213
1270
  // 如果本次强制使用了新连接,结束时尽量关闭它,避免连接泄漏