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/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,14 +532,21 @@ 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
  // 启动后台流处理,捕获任何异步错误
516
539
  parser.processStream(stream).catch((error: unknown) => {
517
- console.error('Error in processStream:', error);
518
- exitFlag = true;
519
- if (!errMsg) {
520
- errMsg = error instanceof Error ? error.message : 'Stream processing failed';
540
+ // 若响应已完整收到(isResponseComplete=true),后置的网络层错误属于正常的
541
+ // 连接拆除过程(如服务端 RST、连接关闭),不影响已成功的调用结果,静默忽略。
542
+ // 若响应尚未完成,才记录错误并唤醒等待者,触发超时/错误路径。
543
+ if (!isResponseComplete) {
544
+ console.error('Error in processStream:', error);
545
+ exitFlag = true;
546
+ if (!errMsg) {
547
+ errMsg = error instanceof Error ? error.message : 'Stream processing failed';
548
+ }
549
+ notifyResponseComplete?.(); // 流处理异常也需唤醒等待者
521
550
  }
522
551
  });
523
552
 
@@ -528,9 +557,11 @@ export class Libp2pGrpcClient {
528
557
  const settingFrme = Http2Frame.createSettingsFrame();
529
558
  await writer.write(settingFrme);
530
559
  // 等待对端 SETTINGS 或 ACK,择一即可,避免偶发握手竞态
560
+ // 注意:未胜出的 promise 内部有超时定时器,它们最终会 reject。
561
+ // 必须绑定 .catch(…) 消除错误,否则在 Node.js 新版本中会导致 UnhandledPromiseRejection 崩溃。
531
562
  await Promise.race([
532
- parser.waitForPeerSettings(1000),
533
- parser.waitForSettingsAck(),
563
+ parser.waitForPeerSettings(1000).catch(() => {}),
564
+ parser.waitForSettingsAck().catch(() => {}),
534
565
  new Promise<void>((res) => setTimeout(res, 300)),
535
566
  ]);
536
567
  // 即使未等到,也继续;多数实现会随后发送
@@ -539,7 +570,8 @@ export class Libp2pGrpcClient {
539
570
  streamId,
540
571
  method,
541
572
  true,
542
- this.token
573
+ this.token,
574
+ this.getAuthority()
543
575
  );
544
576
  await writer.write(headerFrame);
545
577
  // 直接按帧大小分片发送(保持与之前一致的稳定路径)
@@ -560,21 +592,18 @@ export class Libp2pGrpcClient {
560
592
  frameSendTimeout
561
593
  );
562
594
  }
563
- // 等待responseData 不为空,或超时
564
- await new Promise((resolve, reject) => {
595
+ // 等待 responseData 不为空,或超时(事件驱动,不轮询)
596
+ await new Promise<void>((resolve, reject) => {
597
+ if (isResponseComplete || exitFlag) { resolve(); return; }
565
598
  const t = setTimeout(() => {
599
+ notifyResponseComplete = null;
566
600
  reject(new Error("gRPC response timeout"));
567
601
  }, timeout);
568
- const checkResponse = () => {
569
- if (isResponseComplete || exitFlag) {
570
- // 使用新的完成标志
571
- clearTimeout(t);
572
- resolve(responseData);
573
- } else {
574
- setTimeout(checkResponse, 50);
575
- }
602
+ notifyResponseComplete = () => {
603
+ clearTimeout(t);
604
+ notifyResponseComplete = null;
605
+ resolve();
576
606
  };
577
- checkResponse();
578
607
  });
579
608
  try {
580
609
  await writer.flush(timeout);
@@ -584,8 +613,17 @@ export class Libp2pGrpcClient {
584
613
  console.error("unaryCall error:", err);
585
614
  throw err;
586
615
  } finally {
616
+ // 必须先 abort writer(立即强制停止 pushable + stream),再 close stream。
617
+ // 若顺序颠倒:stream.close() 会等待服务端半关闭确认,网络异常时永久挂住,
618
+ // 导致 writer.abort() 永远不执行 → watchdog 定时器 / pushable 泄漏。
619
+ // writer.abort() 内部幂等,成功路径下 writer.end() 已调用 cleanup(),安全。
620
+ writerRef?.abort('unaryCall cleanup');
587
621
  if (stream) {
588
- await stream.close();
622
+ try {
623
+ await stream.close();
624
+ } catch {
625
+ // 流已被 abort,close() 会立即抛出,忽略即可。
626
+ }
589
627
  }
590
628
  if (streamSlotAcquired && state) {
591
629
  state.activeStreams = Math.max(0, state.activeStreams - 1);
@@ -632,6 +670,8 @@ export class Libp2pGrpcClient {
632
670
  const internalController = new AbortController();
633
671
  let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
634
672
  let stream: Stream | null = null;
673
+ // 保存外部 abort 监听器引用,以便操作结束后移除,防止内存泄漏
674
+ let contextAbortHandler: (() => void) | undefined;
635
675
 
636
676
  const profile: TransportProfile =
637
677
  options?.transportProfile ?? this.getDefaultTransportProfile(mode);
@@ -654,18 +694,17 @@ export class Libp2pGrpcClient {
654
694
 
655
695
  // 如果提供了外部信号,监听它
656
696
  if (context?.signal) {
657
- // 如果外部信号已经触发中止,立即返回
697
+ // 如果外部信号已经触发中止,立即返回——避免启动 IIFE 后在 catch 中再次调用 onErrorCallback
658
698
  if (context.signal.aborted) {
659
699
  if (onErrorCallback) {
660
700
  onErrorCallback(new Error("Operation aborted by context"));
661
701
  }
662
- cancelOperation();
702
+ return cancelOperation;
663
703
  }
664
704
 
665
- // 监听外部的abort事件
666
- context.signal.addEventListener("abort", () => {
667
- cancelOperation();
668
- });
705
+ // 监听外部的abort事件(保存引用以便后续移除,防止内存泄漏)
706
+ contextAbortHandler = () => { cancelOperation(); };
707
+ context.signal.addEventListener("abort", contextAbortHandler);
669
708
  }
670
709
 
671
710
  // 超时Promise
@@ -678,13 +717,39 @@ export class Libp2pGrpcClient {
678
717
 
679
718
  // 主操作Promise
680
719
  const operationPromise = (async () => {
681
- let messageBuffer = new Uint8Array(0); // 用于累积跨帧的消息数据
720
+ /**
721
+ * 统一错误报告:确保 onErrorCallback 只被调用一次,
722
+ * 并同时中止操作,防止后续再触发 onEndCallback。
723
+ * 适用于 onGoaway / onHeaders / processStream.catch / onData 等各个错误路径。
724
+ */
725
+ let errorCallbackFired = false;
726
+ const reportError = (err: unknown) => {
727
+ if (errorCallbackFired) return;
728
+ errorCallbackFired = true;
729
+ internalController.abort();
730
+ if (onErrorCallback) onErrorCallback(err);
731
+ };
732
+
733
+ /** 分段列表缓冲,避免每次 payload 到达时 O(n) 全量拷贝 */
734
+ let msgChunks: Uint8Array[] = [];
735
+ let msgTotalLen = 0;
682
736
  let expectedMessageLength = -1; // 当前消息的期望长度
737
+ /** 将分段列表合并为单一 Uint8Array(仅在需要时调用) */
738
+ const flattenMsgBuffer = (): Uint8Array => {
739
+ if (msgChunks.length === 0) return new Uint8Array(0);
740
+ if (msgChunks.length === 1) return msgChunks[0];
741
+ const out = new Uint8Array(msgTotalLen);
742
+ let off = 0;
743
+ for (const c of msgChunks) { out.set(c, off); off += c.length; }
744
+ return out;
745
+ };
683
746
  const hpack = new HPACK();
684
747
  let connection: Connection | null = null;
685
748
  let connectionKey: string | null = null;
686
749
  let state: ConnectionState | null = null;
687
750
  let streamSlotAcquired = false;
751
+ // 提升 writer 作用域到 finally 可访问,确保 unary/server-streaming 模式下也能清理资源
752
+ let writer: StreamWriter | null = null;
688
753
 
689
754
  try {
690
755
  // 检查是否已经中止
@@ -734,7 +799,7 @@ export class Libp2pGrpcClient {
734
799
  });
735
800
  const streamManager = this.getStreamManagerFor(connection as object);
736
801
  const streamId = await streamManager.getNextAppLevelStreamId();
737
- const writer = new StreamWriter(stream, {
802
+ writer = new StreamWriter(stream, {
738
803
  bufferSize: 16 * 1024 * 1024,
739
804
  });
740
805
  try {
@@ -756,11 +821,11 @@ export class Libp2pGrpcClient {
756
821
  const payload = new Uint8Array(8);
757
822
  crypto.getRandomValues?.(payload);
758
823
  const ping = Http2Frame.createFrame(0x6, 0x0, 0, payload);
759
- writer.write(ping);
824
+ writer!.write(ping);
760
825
  } catch { /* ignore ping write errors */ }
761
826
  });
762
827
  } catch { /* ignore addEventListener errors */ }
763
- const parser = new HTTP2Parser(writer, {
828
+ const parser = new HTTP2Parser(writer!, {
764
829
  compatibilityMode: !useFlowControl,
765
830
  });
766
831
  parser.onGoaway = (info) => {
@@ -774,10 +839,8 @@ export class Libp2pGrpcClient {
774
839
  new Error("Connection received GOAWAY")
775
840
  );
776
841
  }
777
- if (onErrorCallback) {
778
- onErrorCallback(new Error(`GOAWAY received: code=${info.errorCode}`));
779
- }
780
- internalController.abort();
842
+ // reportError 统一完成:标记已报错 + abort + 触发回调(幂等,不会重复触发)
843
+ reportError(new Error(`GOAWAY received: code=${info.errorCode}`));
781
844
  try {
782
845
  connection?.close();
783
846
  } catch (err) {
@@ -805,7 +868,7 @@ export class Libp2pGrpcClient {
805
868
  parser,
806
869
  streamId,
807
870
  frame,
808
- writer,
871
+ writer!,
809
872
  internalController.signal,
810
873
  sendWindowTimeout
811
874
  );
@@ -813,7 +876,7 @@ export class Libp2pGrpcClient {
813
876
  if (internalController.signal.aborted) {
814
877
  throw new Error("Operation aborted");
815
878
  }
816
- await writer.write(frame);
879
+ await writer!.write(frame);
817
880
  }
818
881
  };
819
882
  const writeDataFrames = async (frames: Uint8Array[]) => {
@@ -823,66 +886,47 @@ export class Libp2pGrpcClient {
823
886
  };
824
887
 
825
888
  // 在各个回调中检查是否已中止
826
- parser.onData = async (payload): Promise<void> => {
827
- // 检查是否已中止
828
- if (internalController.signal.aborted) {
829
- return;
830
- }
889
+ parser.onData = async (payload): Promise<void> => {
890
+ if (internalController.signal.aborted) return;
831
891
 
832
892
  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;
893
+ // 追加到分段列表,O(1),不拷贝历史数据
894
+ msgChunks.push(payload);
895
+ msgTotalLen += payload.length;
840
896
 
841
897
  // 处理缓冲区中的完整消息
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);
898
+ while (msgTotalLen > 0) {
899
+ if (internalController.signal.aborted) return;
900
+
901
+ // 读取 gRPC 消息头(5字节)
902
+ if (expectedMessageLength === -1 && msgTotalLen >= 5) {
903
+ const flat = flattenMsgBuffer();
904
+ msgChunks = [flat];
905
+ const lengthBytes = flat.slice(1, 5);
852
906
  expectedMessageLength = new DataView(
853
907
  lengthBytes.buffer,
854
908
  lengthBytes.byteOffset
855
- ).getUint32(0, false); // big-endian
909
+ ).getUint32(0, false);
856
910
  }
857
911
 
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
- // 调用回调处理这个完整消息
912
+ // 有完整消息
913
+ if (expectedMessageLength !== -1 && msgTotalLen >= expectedMessageLength + 5) {
914
+ const flat = flattenMsgBuffer();
915
+ msgChunks = [flat];
916
+ const completeMessage = flat.slice(5, expectedMessageLength + 5);
870
917
  onDataCallback(completeMessage);
871
-
872
- // 移除已处理的消息,保留剩余数据
873
- messageBuffer = messageBuffer.slice(expectedMessageLength + 5);
918
+ // 移除已处理消息,保留剩余
919
+ const remaining = flat.slice(expectedMessageLength + 5);
920
+ msgChunks = remaining.length > 0 ? [remaining] : [];
921
+ msgTotalLen = remaining.length;
874
922
  expectedMessageLength = -1;
875
923
  } else {
876
- // 没有足够数据构成完整消息,等待更多数据
877
924
  break;
878
925
  }
879
926
  }
880
927
  } catch (error: unknown) {
881
- if (onErrorCallback) {
882
- onErrorCallback(error);
883
- } else {
884
- throw error;
885
- }
928
+ // reportError 统一报错并中止,防止 onEndCallback 在数据处理异常后仍被调用
929
+ reportError(error);
886
930
  }
887
931
  };
888
932
 
@@ -891,7 +935,7 @@ export class Libp2pGrpcClient {
891
935
  if (internalController.signal.aborted) return;
892
936
 
893
937
  const ackSettingFrame = Http2Frame.createSettingsAckFrame();
894
- writer.write(ackSettingFrame);
938
+ writer!.write(ackSettingFrame);
895
939
  };
896
940
 
897
941
  parser.onHeaders = (headers) => {
@@ -904,19 +948,16 @@ export class Libp2pGrpcClient {
904
948
  } else if (plainHeaders.get("grpc-status") !== undefined) {
905
949
  const errMsg =
906
950
  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
- }
951
+ // reportError 统一完成:标记已报错 + abort + 触发回调(幂等,不会重复触发)
952
+ reportError(new Error(errMsg));
913
953
  }
914
954
  };
915
955
  // 启动后台流处理
916
956
  parser.processStream(stream).catch((error: unknown) => {
917
- console.error('Error in processStream:', error);
918
- if (onErrorCallback) {
919
- onErrorCallback(error);
957
+ // abort() 触发的清理错误属于预期行为,不打印错误日志,不重复触发回调
958
+ if (!internalController.signal.aborted) {
959
+ console.error('Error in processStream:', error);
960
+ reportError(error);
920
961
  }
921
962
  });
922
963
 
@@ -944,10 +985,12 @@ export class Libp2pGrpcClient {
944
985
  }
945
986
 
946
987
  // 等待对端 SETTINGS 或 ACK,择一即可,避免偶发握手竞态
988
+ // 注意:未胜出的 promise 内部有超时定时器,它们最终会 reject。
989
+ // 必须绑定 .catch(…) 消除错误,否则在 Node.js 新版本中会导致 UnhandledPromiseRejection 崩溃。
947
990
  {
948
991
  await Promise.race([
949
- parser.waitForPeerSettings(1000),
950
- parser.waitForSettingsAck(),
992
+ parser.waitForPeerSettings(1000).catch(() => {}),
993
+ parser.waitForSettingsAck().catch(() => {}),
951
994
  new Promise<void>((res) => setTimeout(res, 300)),
952
995
  ]);
953
996
  // 即使未等到,也继续;多数实现会随后发送
@@ -958,17 +1001,13 @@ export class Libp2pGrpcClient {
958
1001
  throw new Error("Operation aborted");
959
1002
  }
960
1003
 
961
- // 检查是否已中止
962
- if (internalController.signal.aborted) {
963
- throw new Error("Operation aborted");
964
- }
965
-
966
1004
  // Create header frame
967
1005
  const headerFrame = Http2Frame.createHeadersFrame(
968
1006
  streamId,
969
1007
  method,
970
1008
  true,
971
- this.token
1009
+ this.token,
1010
+ this.getAuthority()
972
1011
  );
973
1012
  if (mode === "unary" || mode === "server-streaming") {
974
1013
  await writer.write(headerFrame);
@@ -1009,8 +1048,16 @@ export class Libp2pGrpcClient {
1009
1048
  reject: (reason?: unknown) => void;
1010
1049
  }[] = [];
1011
1050
 
1051
+ /** 事件驱动:批处理完成后唤醒 waitForQueue 等待者 */
1052
+ const batchDoneWaiters: Array<() => void> = [];
1053
+
1012
1054
  let isProcessing = false;
1013
1055
 
1056
+ const _notifyBatchDone = () => {
1057
+ const ws = batchDoneWaiters.splice(0);
1058
+ for (const fn of ws) { try { fn(); } catch { /* ignore */ } }
1059
+ };
1060
+
1014
1061
  const processNextBatch = async () => {
1015
1062
  if (isProcessing || processingQueue.length === 0) return;
1016
1063
  isProcessing = true;
@@ -1065,12 +1112,12 @@ export class Libp2pGrpcClient {
1065
1112
  isProcessing = false;
1066
1113
 
1067
1114
  // 如果队列中还有数据,继续处理
1068
- if (
1069
- processingQueue.length > 0 &&
1070
- !internalController.signal.aborted
1071
- ) {
1072
- // 使用 setTimeout 避免阻塞,让新数据有机会加入队列
1073
- setTimeout(() => processNextBatch(), 0);
1115
+ if (processingQueue.length > 0 && !internalController.signal.aborted) {
1116
+ // 直接递归调用(已是 async,自动让出事件循环)
1117
+ processNextBatch().catch((err) => { console.error("Error in processNextBatch:", err); });
1118
+ } else {
1119
+ // 队列清空,唤醒等待者
1120
+ _notifyBatchDone();
1074
1121
  }
1075
1122
  }
1076
1123
  };
@@ -1128,42 +1175,32 @@ export class Libp2pGrpcClient {
1128
1175
  throw error;
1129
1176
  }
1130
1177
 
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
- }
1178
+ // 等待所有剩余的数据处理完成(事件驱动,无 10ms 轮询)
1179
+ await new Promise<void>((resolve, reject) => {
1180
+ const check = () => {
1181
+ if (internalController.signal.aborted) {
1182
+ reject(new Error("Operation aborted"));
1183
+ return;
1184
+ }
1185
+ if (processingQueue.length === 0 && !isProcessing) {
1186
+ resolve();
1187
+ return;
1188
+ }
1189
+ // processNextBatch 结束时会通知这里
1190
+ batchDoneWaiters.push(check);
1191
+ };
1192
+ check();
1193
+ });
1156
1194
 
1157
1195
  // 检查是否已中止
1158
1196
  if (internalController.signal.aborted) {
1159
1197
  throw new Error("Operation aborted");
1160
1198
  }
1161
1199
 
1162
- const finalFrame = Http2Frame.createDataFrame(
1163
- streamId,
1164
- new Uint8Array(),
1165
- true
1166
- );
1200
+ // 发送纯 HTTP/2 END_STREAM 信号帧(0 字节 payload),而非带 gRPC 消息头的空消息。
1201
+ // createDataFrame 会额外附加 5 字节 gRPC 消息头 [0,0,0,0,0],服务端会将其解析
1202
+ // 为一个长度=0 的额外 gRPC 消息,而不仅仅是流结束信号,可能导致协议混淆。
1203
+ const finalFrame = Http2Frame.createFrame(0x0, 0x01, streamId, new Uint8Array(0));
1167
1204
  await writeFrame(finalFrame);
1168
1205
  // 在结束前尽量冲刷内部队列,避免服务器看到部分数据 + context canceled
1169
1206
  try {
@@ -1177,10 +1214,24 @@ export class Libp2pGrpcClient {
1177
1214
  throw new Error("Operation aborted");
1178
1215
  }
1179
1216
 
1180
- await parser.waitForEndOfStream(0);
1181
-
1182
- if (onEndCallback) {
1183
- onEndCallback();
1217
+ // 仅在未中止时等待并回调:
1218
+ // 1. 若已中止(如 onHeaders gRPC 错误),跳过 waitForEndOfStream(0) 避免永久阻塞
1219
+ // (waitForEndOfStream(0) 无超时,需等到 processStream 自然结束,
1220
+ // 而 processStream 结束依赖 stream.close(),但 stream.close() 在 finally 中——形成死锁)
1221
+ // 2. 避免在 onErrorCallback 之后再调用 onEndCallback
1222
+ if (!internalController.signal.aborted) {
1223
+ await parser.waitForEndOfStream(0);
1224
+ // Yield one microtask tick so that processStream.catch (which calls
1225
+ // reportError + internalController.abort()) has a chance to run before
1226
+ // we check abort status. Without this yield, if the stream died
1227
+ // unexpectedly (network error), onEndCallback and onErrorCallback
1228
+ // could both fire because _notifyEndOfStream() is called in
1229
+ // processStream's catch block before the re-throw schedules the
1230
+ // .catch handler as a microtask.
1231
+ await Promise.resolve();
1232
+ if (!internalController.signal.aborted && onEndCallback) {
1233
+ onEndCallback();
1234
+ }
1184
1235
  }
1185
1236
  } catch (err: unknown) {
1186
1237
  // 如果是由于取消导致的错误,使用特定的错误消息
@@ -1189,12 +1240,14 @@ export class Libp2pGrpcClient {
1189
1240
  err instanceof Error &&
1190
1241
  err.message === "Operation aborted"
1191
1242
  ) {
1192
- if (onErrorCallback) {
1243
+ // onHeaders / onGoaway / processStream 错误已通过 reportError 处理,
1244
+ // 此处仅在回调尚未触发时才报告(外部取消/超时场景)
1245
+ if (!errorCallbackFired && onErrorCallback) {
1193
1246
  onErrorCallback(new Error("Operation cancelled by user"));
1194
1247
  }
1195
- } else if (onErrorCallback) {
1248
+ } else if (!errorCallbackFired && onErrorCallback) {
1196
1249
  onErrorCallback(err);
1197
- } else {
1250
+ } else if (!errorCallbackFired) {
1198
1251
  if (err instanceof Error) {
1199
1252
  console.error("asyncCall error:", err.message);
1200
1253
  } else {
@@ -1203,11 +1256,24 @@ export class Libp2pGrpcClient {
1203
1256
  }
1204
1257
  } finally {
1205
1258
  clearTimeout(timeoutHandle);
1259
+ // 移除外部 abort 监听器,防止 AbortController 复用时触发迟到的 cancelOperation()
1260
+ if (contextAbortHandler && context?.signal) {
1261
+ context.signal.removeEventListener("abort", contextAbortHandler);
1262
+ }
1263
+ // 首先标记操作已结束(正常或异常),确保 processStream.catch 不会把
1264
+ // writer.abort() 产生的 'Call cleanup' 错误误判为真实错误并触发 onErrorCallback。
1265
+ // internalController.abort() 是幂等的,重复调用安全。
1266
+ internalController.abort();
1267
+ // 必须先 abort writer(立即强制停止 pushable + stream),再 close stream。
1268
+ // 若顺序颠倒:stream.close() 等待服务端半关闭确认,网络异常时永久挂住,
1269
+ // writer.abort() 永远不执行 → watchdog / pushable 泄漏。
1270
+ // abort() 内部幂等,重复调用安全。
1271
+ writer?.abort('Call cleanup');
1206
1272
  if (stream) {
1207
1273
  try {
1208
1274
  await stream.close();
1209
- } catch (err) {
1210
- console.error("Error closing stream:", err);
1275
+ } catch {
1276
+ // 流已被 abort,close() 会立即抛出,忽略即可。
1211
1277
  }
1212
1278
  }
1213
1279
  // 如果本次强制使用了新连接,结束时尽量关闭它,避免连接泄漏