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/dist/dc-http2/frame.cjs.js +51 -19
- package/dist/dc-http2/frame.cjs.js.map +1 -1
- package/dist/dc-http2/frame.d.ts +1 -1
- package/dist/dc-http2/frame.esm.js +51 -19
- package/dist/dc-http2/frame.esm.js.map +1 -1
- package/dist/dc-http2/hpack.cjs.js +43 -13
- package/dist/dc-http2/hpack.cjs.js.map +1 -1
- package/dist/dc-http2/hpack.esm.js +43 -13
- package/dist/dc-http2/hpack.esm.js.map +1 -1
- package/dist/dc-http2/parser.cjs.js +281 -189
- package/dist/dc-http2/parser.cjs.js.map +1 -1
- package/dist/dc-http2/parser.d.ts +21 -2
- package/dist/dc-http2/parser.esm.js +281 -189
- package/dist/dc-http2/parser.esm.js.map +1 -1
- package/dist/dc-http2/stream.cjs.js +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 +810 -579
- 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 +633 -411
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +633 -411
- package/dist/index.esm.js.map +1 -1
- package/dist/stats.html +1 -1
- package/package.json +1 -1
- package/src/dc-http2/frame.ts +11 -9
- package/src/dc-http2/hpack.ts +43 -13
- package/src/dc-http2/parser.ts +219 -196
- package/src/dc-http2/stream.ts +84 -79
- package/src/index.ts +240 -183
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 (
|
|
452
|
+
if (effectivePayload.length < 5) {
|
|
453
|
+
// 头部字节不足 5,先缓存,等待后续帧补全
|
|
454
|
+
headerPartialBuffer.push(effectivePayload);
|
|
420
455
|
return;
|
|
421
456
|
}
|
|
422
|
-
const lengthBytes =
|
|
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
|
|
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 =
|
|
464
|
+
const grpcData = effectivePayload.subarray(5);
|
|
433
465
|
responseBuffer.push(grpcData);
|
|
434
466
|
responseDataExpectedLength -= grpcData.length; // 更新期望长度
|
|
435
467
|
return;
|
|
436
468
|
} else {
|
|
437
|
-
//
|
|
438
|
-
const
|
|
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
|
-
|
|
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
|
-
//
|
|
502
|
+
// END_STREAM 兜底:数据路径已处理大多数情况;此分支仅在边缘情况下触发
|
|
463
503
|
if (frameHeader && frameHeader.flags & 0x1 && !isResponseComplete) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
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
|
-
|
|
667
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
778
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
835
|
-
|
|
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 (
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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);
|
|
904
|
+
).getUint32(0, false);
|
|
856
905
|
}
|
|
857
906
|
|
|
858
|
-
//
|
|
859
|
-
if (
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
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
|
-
|
|
882
|
-
|
|
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
|
|
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
|
-
|
|
908
|
-
|
|
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
|
-
|
|
918
|
-
if (
|
|
919
|
-
|
|
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
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
//
|
|
1073
|
-
|
|
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
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
//
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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
|
-
|
|
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
|
|
1210
|
-
|
|
1266
|
+
} catch {
|
|
1267
|
+
// 流已被 abort,close() 会立即抛出,忽略即可。
|
|
1211
1268
|
}
|
|
1212
1269
|
}
|
|
1213
1270
|
// 如果本次强制使用了新连接,结束时尽量关闭它,避免连接泄漏
|