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/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,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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
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
|
-
|
|
667
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
778
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
835
|
-
|
|
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 (
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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);
|
|
909
|
+
).getUint32(0, false);
|
|
856
910
|
}
|
|
857
911
|
|
|
858
|
-
//
|
|
859
|
-
if (
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
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
|
-
|
|
882
|
-
|
|
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
|
|
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
|
-
|
|
908
|
-
|
|
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
|
-
|
|
918
|
-
if (
|
|
919
|
-
|
|
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
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
//
|
|
1073
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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
|
-
|
|
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
|
|
1210
|
-
|
|
1275
|
+
} catch {
|
|
1276
|
+
// 流已被 abort,close() 会立即抛出,忽略即可。
|
|
1211
1277
|
}
|
|
1212
1278
|
}
|
|
1213
1279
|
// 如果本次强制使用了新连接,结束时尽量关闭它,避免连接泄漏
|