@origintrail-official/dkg-core 10.0.19 → 10.0.21
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/absolute-rfc3987-iri.d.ts +8 -0
- package/dist/absolute-rfc3987-iri.d.ts.map +1 -0
- package/dist/absolute-rfc3987-iri.js +8 -0
- package/dist/absolute-rfc3987-iri.js.map +1 -0
- package/dist/author-catalog-directory.d.ts.map +1 -1
- package/dist/author-catalog-directory.js +68 -24
- package/dist/author-catalog-directory.js.map +1 -1
- package/dist/bounded-denial-logger.d.ts +10 -0
- package/dist/bounded-denial-logger.d.ts.map +1 -1
- package/dist/bounded-denial-logger.js +58 -14
- package/dist/bounded-denial-logger.js.map +1 -1
- package/dist/bounded-gzip.d.ts +20 -0
- package/dist/bounded-gzip.d.ts.map +1 -0
- package/dist/bounded-gzip.js +156 -0
- package/dist/bounded-gzip.js.map +1 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +8 -0
- package/dist/constants.js.map +1 -1
- package/dist/context-graph-on-chain-binding.d.ts +8 -1
- package/dist/context-graph-on-chain-binding.d.ts.map +1 -1
- package/dist/context-graph-on-chain-binding.js +18 -3
- package/dist/context-graph-on-chain-binding.js.map +1 -1
- package/dist/crypto/swm-sender-key.d.ts.map +1 -1
- package/dist/crypto/swm-sender-key.js +13 -5
- package/dist/crypto/swm-sender-key.js.map +1 -1
- package/dist/daemon-request-deadlines.d.ts +60 -0
- package/dist/daemon-request-deadlines.d.ts.map +1 -0
- package/dist/daemon-request-deadlines.js +86 -0
- package/dist/daemon-request-deadlines.js.map +1 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +30 -0
- package/dist/errors.js.map +1 -1
- package/dist/event-bus.d.ts +1 -0
- package/dist/event-bus.d.ts.map +1 -1
- package/dist/event-bus.js +1 -0
- package/dist/event-bus.js.map +1 -1
- package/dist/experimental-exact-batch-stream.d.ts +77 -0
- package/dist/experimental-exact-batch-stream.d.ts.map +1 -0
- package/dist/experimental-exact-batch-stream.js +198 -0
- package/dist/experimental-exact-batch-stream.js.map +1 -0
- package/dist/experimental-exact-batch-wire.d.ts +41 -0
- package/dist/experimental-exact-batch-wire.d.ts.map +1 -0
- package/dist/experimental-exact-batch-wire.js +116 -0
- package/dist/experimental-exact-batch-wire.js.map +1 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/network/address-policy.d.ts +5 -0
- package/dist/network/address-policy.d.ts.map +1 -1
- package/dist/network/address-policy.js +24 -5
- package/dist/network/address-policy.js.map +1 -1
- package/dist/network/index.d.ts +1 -1
- package/dist/network/index.d.ts.map +1 -1
- package/dist/network/index.js.map +1 -1
- package/dist/network/libp2p-network.d.ts +2 -1
- package/dist/network/libp2p-network.d.ts.map +1 -1
- package/dist/network/libp2p-network.js +4 -1
- package/dist/network/libp2p-network.js.map +1 -1
- package/dist/network/libp2p-peer-connect.d.ts +10 -4
- package/dist/network/libp2p-peer-connect.d.ts.map +1 -1
- package/dist/network/libp2p-peer-connect.js +73 -9
- package/dist/network/libp2p-peer-connect.js.map +1 -1
- package/dist/network/network.d.ts +19 -0
- package/dist/network/network.d.ts.map +1 -1
- package/dist/network/network.js.map +1 -1
- package/dist/network/peer-resolver.d.ts +11 -0
- package/dist/network/peer-resolver.d.ts.map +1 -1
- package/dist/network/peer-resolver.js +90 -15
- package/dist/network/peer-resolver.js.map +1 -1
- package/dist/proto/swm-sender-key.d.ts +1 -1
- package/dist/proto/swm-sender-key.d.ts.map +1 -1
- package/dist/proto/swm-sender-key.js +1 -0
- package/dist/proto/swm-sender-key.js.map +1 -1
- package/dist/protocol-outbox.d.ts +2 -2
- package/dist/protocol-outbox.js +2 -2
- package/dist/protocol-router.d.ts +42 -0
- package/dist/protocol-router.d.ts.map +1 -1
- package/dist/protocol-router.js +280 -165
- package/dist/protocol-router.js.map +1 -1
- package/dist/request-abort-lifecycle.d.ts +35 -0
- package/dist/request-abort-lifecycle.d.ts.map +1 -0
- package/dist/request-abort-lifecycle.js +54 -0
- package/dist/request-abort-lifecycle.js.map +1 -0
- package/dist/sparql-safe.d.ts +8 -0
- package/dist/sparql-safe.d.ts.map +1 -1
- package/dist/sparql-safe.js +13 -2
- package/dist/sparql-safe.js.map +1 -1
- package/dist/sparql-terms.d.ts +36 -0
- package/dist/sparql-terms.d.ts.map +1 -0
- package/dist/sparql-terms.js +98 -0
- package/dist/sparql-terms.js.map +1 -0
- package/dist/sync-wire-objects.d.ts.map +1 -1
- package/dist/sync-wire-objects.js +9 -4
- package/dist/sync-wire-objects.js.map +1 -1
- package/dist/telemetry-api.d.ts +15 -4
- package/dist/telemetry-api.d.ts.map +1 -1
- package/dist/telemetry-api.js +13 -6
- package/dist/telemetry-api.js.map +1 -1
- package/package.json +7 -2
package/dist/protocol-router.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LibP2PNetwork } from './network/libp2p-network.js';
|
|
2
|
+
import { startRequestAbortLifecycle } from './request-abort-lifecycle.js';
|
|
2
3
|
import { MessageStreamPool, POOLED_MESSAGE_PROTOCOL, } from './message-stream-pool.js';
|
|
3
4
|
import { withSpan, getMetrics } from './telemetry-api.js';
|
|
4
5
|
/** Default max bytes readAll will buffer before aborting (10 MB). */
|
|
@@ -452,8 +453,8 @@ export class ProtocolRouter {
|
|
|
452
453
|
* probe opens and immediately aborts the stream before any payload bytes.
|
|
453
454
|
*/
|
|
454
455
|
async probeProtocol(peerIdStr, protocolId, timeoutMs = 3_000) {
|
|
455
|
-
const
|
|
456
|
-
const signal =
|
|
456
|
+
const lifecycle = startRequestAbortLifecycle(timeoutMs, [this.node.stopSignal]);
|
|
457
|
+
const signal = lifecycle.signal;
|
|
457
458
|
try {
|
|
458
459
|
await this.requirePeerAccepted(peerIdStr, protocolId, 'outbound', { signal, timeoutMs });
|
|
459
460
|
// Match send()'s resolver contract before opening a stream. For the
|
|
@@ -475,10 +476,237 @@ export class ProtocolRouter {
|
|
|
475
476
|
throw error;
|
|
476
477
|
return isProtocolUnsupportedError(error) ? 'unsupported' : 'unavailable';
|
|
477
478
|
}
|
|
479
|
+
finally {
|
|
480
|
+
lifecycle.release();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Own one dedicated duplex stream. Only negotiation may change routes;
|
|
485
|
+
* after callback entry no authenticated request is retried or replayed.
|
|
486
|
+
*/
|
|
487
|
+
async withDuplexStream(peerIdStr, protocolId, options, exchange) {
|
|
488
|
+
this.validateDuplexStreamOptions(options);
|
|
489
|
+
const lifecycle = startRequestAbortLifecycle(options.timeoutMs, [options.signal, this.node.stopSignal]);
|
|
490
|
+
const signal = lifecycle.signal;
|
|
491
|
+
let stream;
|
|
492
|
+
const abortOwned = () => { if (stream)
|
|
493
|
+
abortStream(stream, signal.reason); };
|
|
494
|
+
signal.addEventListener('abort', abortOwned, { once: true });
|
|
495
|
+
try {
|
|
496
|
+
const openStream = await this.createAdmittedPeerStreamOperation(peerIdStr, protocolId, signal, options.timeoutMs);
|
|
497
|
+
({ stream } = await openStream({
|
|
498
|
+
resolverTimeoutMs: options.timeoutMs,
|
|
499
|
+
afterDialFailure: 'reuse-limited',
|
|
500
|
+
cancelledResolution: 'throw',
|
|
501
|
+
}));
|
|
502
|
+
this.boundDuplexStreamBuffers(stream, options.maxReadBufferBytes);
|
|
503
|
+
const result = await exchange(stream, signal);
|
|
504
|
+
if (signal.aborted)
|
|
505
|
+
throw asAbortError(signal.reason);
|
|
506
|
+
await this.closeDuplexStream(stream, signal);
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
if (stream && stream.status !== 'closed')
|
|
511
|
+
abortStream(stream, new Error('duplex stream scope ended'));
|
|
512
|
+
signal.removeEventListener('abort', abortOwned);
|
|
513
|
+
lifecycle.release();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/** Read a bounded request, then retain its continuation for duplex work. */
|
|
517
|
+
registerDuplexStream(protocolId, readRequest, handler, options) {
|
|
518
|
+
this.validateDuplexStreamOptions(options);
|
|
519
|
+
if (!Number.isSafeInteger(options.maxRequestBytes) || options.maxRequestBytes <= 0
|
|
520
|
+
|| options.maxRequestBytes > this.maxReadBytes)
|
|
521
|
+
throw new RangeError('Invalid duplex request limit');
|
|
522
|
+
this.node.libp2p.handle(protocolId, async (stream, connection) => {
|
|
523
|
+
const peerId = connection.remotePeer.toString();
|
|
524
|
+
const closed = new AbortController();
|
|
525
|
+
const onClose = () => closed.abort(new Error('duplex peer stream closed'));
|
|
526
|
+
const lifecycle = startRequestAbortLifecycle(options.timeoutMs, [options.signal, this.node.stopSignal, closed.signal]);
|
|
527
|
+
const signal = lifecycle.signal;
|
|
528
|
+
const onAbort = () => abortStream(stream, signal.reason);
|
|
529
|
+
let request;
|
|
530
|
+
stream.addEventListener('close', onClose, { once: true });
|
|
531
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
532
|
+
try {
|
|
533
|
+
if (signal.aborted)
|
|
534
|
+
throw asAbortError(signal.reason);
|
|
535
|
+
this.rejectKnownRejectedInboundPeer(peerId, protocolId);
|
|
536
|
+
this.boundDuplexStreamBuffers(stream, options.maxReadBufferBytes);
|
|
537
|
+
request = await readRequest(stream, signal);
|
|
538
|
+
if (request.requestData.byteLength > options.maxRequestBytes)
|
|
539
|
+
throw new RangeError('Duplex request byte limit exceeded');
|
|
540
|
+
await this.requirePeerAccepted(peerId, protocolId, 'inbound', { signal, timeoutMs: options.timeoutMs });
|
|
541
|
+
if (signal.aborted)
|
|
542
|
+
throw asAbortError(signal.reason);
|
|
543
|
+
await handler({ ...request, peerId, stream, signal });
|
|
544
|
+
if (signal.aborted)
|
|
545
|
+
throw asAbortError(signal.reason);
|
|
546
|
+
stream.removeEventListener('close', onClose);
|
|
547
|
+
await this.closeDuplexStream(stream, signal);
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
// Do not log authenticated request bytes, metadata, URLs or tokens.
|
|
551
|
+
abortStream(stream, error instanceof Error ? error : new Error('duplex stream failed'));
|
|
552
|
+
}
|
|
553
|
+
finally {
|
|
554
|
+
stream.removeEventListener('close', onClose);
|
|
555
|
+
signal.removeEventListener('abort', onAbort);
|
|
556
|
+
if (stream.status !== 'closed')
|
|
557
|
+
abortStream(stream, new Error('duplex stream scope ended'));
|
|
558
|
+
await request?.dispose?.().catch(() => undefined);
|
|
559
|
+
lifecycle.release();
|
|
560
|
+
}
|
|
561
|
+
}, { runOnLimitedConnection: true });
|
|
562
|
+
}
|
|
563
|
+
validateDuplexStreamOptions(options) {
|
|
564
|
+
if (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0
|
|
565
|
+
|| !Number.isSafeInteger(options.maxReadBufferBytes) || options.maxReadBufferBytes <= 0
|
|
566
|
+
|| options.maxReadBufferBytes > this.maxReadBytes)
|
|
567
|
+
throw new RangeError('Invalid duplex stream limits');
|
|
568
|
+
}
|
|
569
|
+
boundDuplexStreamBuffers(stream, limit) {
|
|
570
|
+
stream.maxReadBufferLength = Math.min(stream.maxReadBufferLength || limit, limit);
|
|
571
|
+
stream.maxWriteBufferLength = Math.min(stream.maxWriteBufferLength || limit, limit);
|
|
572
|
+
}
|
|
573
|
+
async closeDuplexStream(stream, signal) {
|
|
574
|
+
// close() flushes and half-closes only the writable end. Wait for the
|
|
575
|
+
// peer's half-close too: resetting immediately could discard final bytes
|
|
576
|
+
// already handed to the transport but not yet consumed by the peer.
|
|
577
|
+
let onClose;
|
|
578
|
+
let onAbort;
|
|
579
|
+
const settled = new Promise((resolve, reject) => {
|
|
580
|
+
onClose = () => stream.status === 'closed' ? resolve() : reject(new Error('Duplex stream closed before settlement'));
|
|
581
|
+
onAbort = () => reject(asAbortError(signal.reason));
|
|
582
|
+
stream.addEventListener('close', onClose, { once: true });
|
|
583
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
584
|
+
});
|
|
585
|
+
// The close operation may fail before the close-event promise is awaited.
|
|
586
|
+
void settled.catch(() => undefined);
|
|
587
|
+
try {
|
|
588
|
+
if (signal.aborted)
|
|
589
|
+
throw asAbortError(signal.reason);
|
|
590
|
+
await stream.close({ signal });
|
|
591
|
+
if (stream.status !== 'closed')
|
|
592
|
+
await settled;
|
|
593
|
+
}
|
|
594
|
+
finally {
|
|
595
|
+
stream.removeEventListener('close', onClose);
|
|
596
|
+
signal.removeEventListener('abort', onAbort);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Admission is checked once for the operation, before any transport work.
|
|
601
|
+
* Each open uses a fresh connection snapshot and resolver watch. The caller
|
|
602
|
+
* owns payload retries and chooses when a failed direct dial permits relay
|
|
603
|
+
* reuse; this operation never sends or replays application bytes.
|
|
604
|
+
*/
|
|
605
|
+
async createAdmittedPeerStreamOperation(peerIdStr, protocolId, signal, timeoutMs) {
|
|
606
|
+
if (signal.aborted)
|
|
607
|
+
throw asAbortError(signal.reason);
|
|
608
|
+
await this.requirePeerAccepted(peerIdStr, protocolId, 'outbound', { signal, timeoutMs });
|
|
609
|
+
if (signal.aborted)
|
|
610
|
+
throw asAbortError(signal.reason);
|
|
611
|
+
// Keep parsing lazy: pooled sends may settle without a one-shot open.
|
|
612
|
+
let peerId;
|
|
613
|
+
return async (options) => {
|
|
614
|
+
if (signal.aborted)
|
|
615
|
+
throw asAbortError(signal.reason);
|
|
616
|
+
peerId ??= (await import('@libp2p/peer-id')).peerIdFromString(peerIdStr);
|
|
617
|
+
const targetPeer = peerId;
|
|
618
|
+
const libp2p = this.node.libp2p;
|
|
619
|
+
// Raw walks heal keyed-lookup misses. Remember every observed connection
|
|
620
|
+
// so the resolver yields only to a connection this pass has not tried.
|
|
621
|
+
const considered = new WeakSet();
|
|
622
|
+
const getConnections = () => {
|
|
623
|
+
const connections = rawGetConnectionsFor(libp2p, targetPeer);
|
|
624
|
+
for (const connection of connections)
|
|
625
|
+
considered.add(connection);
|
|
626
|
+
return connections;
|
|
627
|
+
};
|
|
628
|
+
const reuseOptions = {
|
|
629
|
+
peerHasDirectAddrs: async () => {
|
|
630
|
+
const peer = await libp2p.peerStore.get(targetPeer);
|
|
631
|
+
return (peer.addresses ?? []).some(({ multiaddr }) => {
|
|
632
|
+
const address = multiaddr?.toString?.() ?? '';
|
|
633
|
+
return Boolean(address) && !address.includes('/p2p-circuit');
|
|
634
|
+
});
|
|
635
|
+
},
|
|
636
|
+
allowLimitedWithDirectAddrs: options.allowLimitedWithDirectAddrs,
|
|
637
|
+
excludeConnections: options.excludeConnections,
|
|
638
|
+
};
|
|
639
|
+
let reused = await tryReuseExistingConnection(getConnections, protocolId, signal, reuseOptions);
|
|
640
|
+
if (!reused && this.peerResolver && !signal.aborted) {
|
|
641
|
+
// Only resolution follows the connection event signal. Negotiation and
|
|
642
|
+
// payload work keep the caller's original cancellation/deadline signal.
|
|
643
|
+
const connected = watchForNewPeerConnection(libp2p, targetPeer, considered);
|
|
644
|
+
const resolution = composeAbortSignalsScoped(signal, connected?.signal);
|
|
645
|
+
try {
|
|
646
|
+
if (!connected?.signal.aborted) {
|
|
647
|
+
await this.peerResolver.resolve(peerIdStr, {
|
|
648
|
+
signal: resolution.signal ?? signal,
|
|
649
|
+
perStepTimeoutMs: options.resolverTimeoutMs,
|
|
650
|
+
}).catch(() => undefined);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
finally {
|
|
654
|
+
resolution.dispose();
|
|
655
|
+
connected?.dispose();
|
|
656
|
+
}
|
|
657
|
+
if (connected?.signal.aborted) {
|
|
658
|
+
reused = await tryReuseExistingConnection(getConnections, protocolId, signal, reuseOptions);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (signal.aborted && (reused || options.cancelledResolution === 'throw')) {
|
|
662
|
+
if (reused)
|
|
663
|
+
abortStream(reused.stream, signal.reason);
|
|
664
|
+
throw asAbortError(signal.reason);
|
|
665
|
+
}
|
|
666
|
+
const dialStartedAt = Date.now();
|
|
667
|
+
let stream;
|
|
668
|
+
if (reused)
|
|
669
|
+
stream = reused.stream;
|
|
670
|
+
else {
|
|
671
|
+
options.onNormalDial?.();
|
|
672
|
+
try {
|
|
673
|
+
stream = await this.network.dialProtocol(peerIdStr, protocolId, { signal });
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
if (options.afterDialFailure === 'throw')
|
|
677
|
+
throw error;
|
|
678
|
+
if (signal.aborted)
|
|
679
|
+
throw asAbortError(signal.reason);
|
|
680
|
+
// No application bytes have been sent. A duplex scope may use an
|
|
681
|
+
// existing relay after the normal dial actually failed.
|
|
682
|
+
reused = await tryReuseExistingConnection(getConnections, protocolId, signal, { ...reuseOptions, allowLimitedWithDirectAddrs: true });
|
|
683
|
+
if (!reused)
|
|
684
|
+
throw error;
|
|
685
|
+
stream = reused.stream;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (signal.aborted) {
|
|
689
|
+
abortStream(stream, signal.reason);
|
|
690
|
+
throw asAbortError(signal.reason);
|
|
691
|
+
}
|
|
692
|
+
return { stream, connection: reused?.connection ?? null, dialDurationMs: Date.now() - dialStartedAt };
|
|
693
|
+
};
|
|
478
694
|
}
|
|
479
695
|
async sendInner(peerIdStr, protocolId, data, timeoutMsOrOpts = DEFAULT_SEND_TIMEOUT_MS) {
|
|
480
696
|
const opts = typeof timeoutMsOrOpts === 'number' ? { timeoutMs: timeoutMsOrOpts } : timeoutMsOrOpts;
|
|
481
697
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_SEND_TIMEOUT_MS;
|
|
698
|
+
// One deadline for the whole send, following the caller's signal and
|
|
699
|
+
// node.stopSignal. Once the send settles nothing of it stays attached to
|
|
700
|
+
// either (#2812).
|
|
701
|
+
const lifecycle = startRequestAbortLifecycle(timeoutMs, [opts.signal, this.node.stopSignal]);
|
|
702
|
+
try {
|
|
703
|
+
return await this.sendWithinLifecycle(peerIdStr, protocolId, data, opts, timeoutMs, lifecycle);
|
|
704
|
+
}
|
|
705
|
+
finally {
|
|
706
|
+
lifecycle.release();
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
async sendWithinLifecycle(peerIdStr, protocolId, data, opts, timeoutMs, lifecycle) {
|
|
482
710
|
const singleUsePayload = opts.payloadReuse === 'single-use';
|
|
483
711
|
const maxAttempts = singleUsePayload ? 1 : 3;
|
|
484
712
|
const parallelPaths = Math.max(1, Math.floor(opts.parallelPaths ?? 1));
|
|
@@ -487,16 +715,10 @@ export class ProtocolRouter {
|
|
|
487
715
|
}
|
|
488
716
|
const maxReadBytes = resolveSendMaxReadBytes(opts.maxReadBytes, this.maxReadBytes);
|
|
489
717
|
const overallStartedAt = Date.now();
|
|
490
|
-
const overallDeadline =
|
|
718
|
+
const overallDeadline = lifecycle.deadline;
|
|
491
719
|
const stopSignal = this.node.stopSignal;
|
|
492
|
-
const
|
|
493
|
-
const
|
|
494
|
-
if (overallSignal.aborted)
|
|
495
|
-
throw asAbortError(overallSignal.reason);
|
|
496
|
-
await this.requirePeerAccepted(peerIdStr, protocolId, 'outbound', {
|
|
497
|
-
signal: overallSignal,
|
|
498
|
-
timeoutMs,
|
|
499
|
-
});
|
|
720
|
+
const overallSignal = lifecycle.signal;
|
|
721
|
+
const openStream = await this.createAdmittedPeerStreamOperation(peerIdStr, protocolId, overallSignal, timeoutMs);
|
|
500
722
|
// Pooled overlay short-circuit. When `enablePooling(protocolId)`
|
|
501
723
|
// has been called for this logical protocol AND the peer hasn't
|
|
502
724
|
// been pinned to the one-shot wire variant by a prior fallback,
|
|
@@ -625,11 +847,11 @@ export class ProtocolRouter {
|
|
|
625
847
|
if (remainingMs === 0) {
|
|
626
848
|
throw new Error(`send timeout: pooled fallback exhausted the ${timeoutMs}ms budget before one-shot attempt`);
|
|
627
849
|
}
|
|
628
|
-
const libp2p = this.node.libp2p;
|
|
629
|
-
const { peerIdFromString } = await import('@libp2p/peer-id');
|
|
630
|
-
const peerId = peerIdFromString(peerIdStr);
|
|
631
850
|
const startedAt = overallStartedAt;
|
|
632
851
|
if (parallelPaths > 1) {
|
|
852
|
+
const libp2p = this.node.libp2p;
|
|
853
|
+
const { peerIdFromString } = await import('@libp2p/peer-id');
|
|
854
|
+
const peerId = peerIdFromString(peerIdStr);
|
|
633
855
|
// Reuse the overall budget rather than starting a fresh one
|
|
634
856
|
// — see comment above. Codex PR #560 round 4.
|
|
635
857
|
const multipathSignalWithStop = overallSignal;
|
|
@@ -706,8 +928,9 @@ export class ProtocolRouter {
|
|
|
706
928
|
lastErr = new Error('send timeout elapsed');
|
|
707
929
|
throw lastErr;
|
|
708
930
|
}
|
|
709
|
-
|
|
710
|
-
|
|
931
|
+
// The request deadline already ends where this attempt's remaining
|
|
932
|
+
// budget does, so every attempt shares the request signal.
|
|
933
|
+
const attemptSignal = overallSignal;
|
|
711
934
|
if (attemptSignal.aborted)
|
|
712
935
|
throw asAbortError(attemptSignal.reason);
|
|
713
936
|
// Track which connection (if any) the fast path picked this
|
|
@@ -717,153 +940,19 @@ export class ProtocolRouter {
|
|
|
717
940
|
let pickedConnection = null;
|
|
718
941
|
let attemptedNormalDial = false;
|
|
719
942
|
try {
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
//
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
// first resolver call landed during a transient routing-table
|
|
726
|
-
// miss (peer's K-bucket entry expired, no live gossip source,
|
|
727
|
-
// DHT walk happened to time out), all 3 dialProtocol attempts
|
|
728
|
-
// hit the same empty peerStore in the next ~1.5s and the loop
|
|
729
|
-
// gave up with `'no valid addresses for peer'` before any DHT
|
|
730
|
-
// advertisement / `connection:open` event from the recipient
|
|
731
|
-
// could populate addresses for a later attempt.
|
|
732
|
-
//
|
|
733
|
-
// Re-running per-attempt is cheap on the warm path (the
|
|
734
|
-
// resolver's step 1 is a sub-millisecond live-connection check
|
|
735
|
-
// that short-circuits when we already have the peer connected
|
|
736
|
-
// or its addresses cached in the peerStore) and only pays the
|
|
737
|
-
// DHT-walk / registry-lookup cost on the cold path — which is
|
|
738
|
-
// exactly the case we want to keep paying for, because that's
|
|
739
|
-
// where address staleness actually hurts. The resolver itself
|
|
740
|
-
// never throws (returns empty array on miss); the dial below
|
|
741
|
-
// surfaces a real transport error if the peer is genuinely
|
|
742
|
-
// unreachable.
|
|
743
|
-
//
|
|
744
|
-
// Codex review feedback on PR #497: pass the same AbortSignal +
|
|
745
|
-
// remaining time budget to the resolver so a caller's
|
|
746
|
-
// `timeoutMs` bounds the entire send (resolver + dial + read).
|
|
747
|
-
// First try the existing-connection fast path: if we already
|
|
748
|
-
// have at least one open connection to this peer (of ANY
|
|
749
|
-
// direction, direct OR circuit-relay-limited), open the new
|
|
750
|
-
// stream on that connection directly via `newStream` instead
|
|
751
|
-
// of going through `dialProtocol`. This sidesteps the entire
|
|
752
|
-
// address-resolution + peerStore lookup path that returns
|
|
753
|
-
// "no valid addresses for peer" when libp2p's peerStore is
|
|
754
|
-
// empty or stale for the peer — the "Window D" failure class
|
|
755
|
-
// identified in the May 2026 Miles↔Lex 6h soak postmortem,
|
|
756
|
-
// where 31 inbound circuit `connection:open` events from peer
|
|
757
|
-
// P over 4 minutes failed to heal any of 20 opportunistic
|
|
758
|
-
// outbound flushes because each one went through dialProtocol
|
|
759
|
-
// and lost the peerStore race.
|
|
760
|
-
//
|
|
761
|
-
// Crucially: when peerStore is empty for P (the Window D
|
|
762
|
-
// shape), the connection-manager-auto-dial side effect of
|
|
763
|
-
// `peerStore.merge` documented at
|
|
764
|
-
// docs/archive/UPSTREAM_ISSUE_DRAFT.md does NOT fire — there
|
|
765
|
-
// are no direct addresses to upgrade to. So this fast path
|
|
766
|
-
// does not introduce the mid-stream-negotiation race the doc
|
|
767
|
-
// warns about; it benefits exactly the case where the doc's
|
|
768
|
-
// race cannot apply.
|
|
769
|
-
//
|
|
770
|
-
// Failure here (connection died between `getConnections` and
|
|
771
|
-
// `newStream`, peer dropped protocol support, etc.) falls
|
|
772
|
-
// through to the dialProtocol path below WITHIN THE SAME
|
|
773
|
-
// ATTEMPT, so we don't waste a retry slot on the fast-path
|
|
774
|
-
// miss.
|
|
775
|
-
// CRITICAL: feed the fast path from a RAW `getConnections()`
|
|
776
|
-
// walk filtered by `remotePeer`, NOT from the peerId-keyed
|
|
777
|
-
// `libp2p.getConnections(peerId)` lookup.
|
|
778
|
-
//
|
|
779
|
-
// The PR #533 diagnostic surface (`PeerDiagnostics`) is
|
|
780
|
-
// explicitly built around the divergence
|
|
781
|
-
// `rawConnectionCount > getConnectionsReturnsForPeer` as the
|
|
782
|
-
// smoking-gun Window D signature: libp2p's peerId-keyed
|
|
783
|
-
// lookup can return `[]` even when a raw walk over all
|
|
784
|
-
// connections shows one or more open connections whose
|
|
785
|
-
// `remotePeer` matches the target peerId. Using the keyed
|
|
786
|
-
// lookup here would make the fast path miss the EXACT case
|
|
787
|
-
// it was built to heal — `tryReuseExistingConnection` would
|
|
788
|
-
// get an empty candidate list, fall through to the resolver
|
|
789
|
-
// + dialProtocol path, and still fail with "no valid
|
|
790
|
-
// addresses for peer". Walk raw, filter ourselves, and
|
|
791
|
-
// accept that we pay the O(N) cost over the whole
|
|
792
|
-
// connection table per send — N is the total open-conn
|
|
793
|
-
// count of the node, typically <100, so the cost is a few
|
|
794
|
-
// microseconds and the correctness win is large.
|
|
795
|
-
//
|
|
796
|
-
// Every connection the walk hands the fast path is recorded, so
|
|
797
|
-
// the resolver watch below can tell a connection that opened
|
|
798
|
-
// after this pass from one the pass already skipped or failed on.
|
|
799
|
-
const consideredConnections = new WeakSet();
|
|
800
|
-
const getPeerConnections = () => {
|
|
801
|
-
const connections = rawGetConnectionsFor(libp2p, peerId);
|
|
802
|
-
for (const connection of connections)
|
|
803
|
-
consideredConnections.add(connection);
|
|
804
|
-
return connections;
|
|
805
|
-
};
|
|
806
|
-
const fastPathOptions = {
|
|
807
|
-
// Probe peerStore for non-circuit (direct) addresses; the
|
|
808
|
-
// fast path uses this to gate whether reusing a LIMITED
|
|
809
|
-
// (circuit-relay-v2) connection is safe or whether
|
|
810
|
-
// libp2p's CM is about to auto-upgrade and prune it
|
|
811
|
-
// mid-stream. See the JSDoc inside
|
|
812
|
-
// `tryReuseExistingConnection` + the PR #537 CI
|
|
813
|
-
// postmortem for the DCUtR upgrade race detail.
|
|
814
|
-
peerHasDirectAddrs: async () => {
|
|
815
|
-
const peer = await libp2p.peerStore.get(peerId);
|
|
816
|
-
const addrs = peer.addresses ?? [];
|
|
817
|
-
for (const a of addrs) {
|
|
818
|
-
const ma = a.multiaddr?.toString?.() ?? '';
|
|
819
|
-
if (ma && !ma.includes('/p2p-circuit'))
|
|
820
|
-
return true;
|
|
821
|
-
}
|
|
822
|
-
return false;
|
|
823
|
-
},
|
|
943
|
+
// Re-prime on each cold retry, while warm reuse skips resolution.
|
|
944
|
+
// A failed normal dial unlocks limited reuse on the next attempt;
|
|
945
|
+
// unlike a duplex scope, send() keeps its existing retry/backoff gate.
|
|
946
|
+
const opened = await openStream({
|
|
947
|
+
resolverTimeoutMs: remaining,
|
|
824
948
|
allowLimitedWithDirectAddrs: normalDialFailed,
|
|
825
949
|
excludeConnections: triedConnections,
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
// meanwhile (the peer dialing us, or a concurrent dial) makes
|
|
833
|
-
// further resolution pointless, just as the resolver's own
|
|
834
|
-
// live-connection step would, so it ends the resolver early and
|
|
835
|
-
// the fast path runs again over it. Only the resolver's signal
|
|
836
|
-
// is cut short; the rest of the attempt keeps `attemptSignal`.
|
|
837
|
-
const peerConnected = watchForNewPeerConnection(libp2p, peerId, consideredConnections);
|
|
838
|
-
try {
|
|
839
|
-
if (!peerConnected?.signal.aborted) {
|
|
840
|
-
await this.peerResolver
|
|
841
|
-
.resolve(peerIdStr, {
|
|
842
|
-
signal: composeAbortSignals(attemptSignal, peerConnected?.signal) ?? attemptSignal,
|
|
843
|
-
perStepTimeoutMs: remaining,
|
|
844
|
-
})
|
|
845
|
-
.catch(() => undefined);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
finally {
|
|
849
|
-
peerConnected?.dispose();
|
|
850
|
-
}
|
|
851
|
-
if (peerConnected?.signal.aborted) {
|
|
852
|
-
fastResult = await tryReuseExistingConnection(getPeerConnections, protocolId, attemptSignal, fastPathOptions);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
const fastStream = fastResult?.stream ?? null;
|
|
856
|
-
pickedConnection = fastResult?.connection ?? null;
|
|
857
|
-
const dialStartedAt = Date.now();
|
|
858
|
-
let stream;
|
|
859
|
-
if (fastStream) {
|
|
860
|
-
stream = fastStream;
|
|
861
|
-
}
|
|
862
|
-
else {
|
|
863
|
-
attemptedNormalDial = true;
|
|
864
|
-
stream = await this.network.dialProtocol(peerIdStr, protocolId, { signal: attemptSignal });
|
|
865
|
-
}
|
|
866
|
-
const dialDurationMs = Date.now() - dialStartedAt;
|
|
950
|
+
afterDialFailure: 'throw',
|
|
951
|
+
cancelledResolution: 'dial-with-signal',
|
|
952
|
+
onNormalDial: () => { attemptedNormalDial = true; },
|
|
953
|
+
});
|
|
954
|
+
const { stream, dialDurationMs } = opened;
|
|
955
|
+
pickedConnection = opened.connection;
|
|
867
956
|
if (stream.writeStatus === 'closed' || stream.writeStatus === 'closing') {
|
|
868
957
|
stream.abort(new Error('stream closed before send'));
|
|
869
958
|
throw new Error('stream returned in closed state');
|
|
@@ -916,7 +1005,10 @@ export class ProtocolRouter {
|
|
|
916
1005
|
// during the backoff, we throw immediately rather than
|
|
917
1006
|
// continuing into another attempt that's already over budget.
|
|
918
1007
|
await new Promise((resolve, reject) => {
|
|
919
|
-
const t = setTimeout(
|
|
1008
|
+
const t = setTimeout(() => {
|
|
1009
|
+
overallSignal.removeEventListener('abort', onAbort);
|
|
1010
|
+
resolve();
|
|
1011
|
+
}, backoff);
|
|
920
1012
|
const onAbort = () => {
|
|
921
1013
|
clearTimeout(t);
|
|
922
1014
|
if (stopSignal?.aborted) {
|
|
@@ -1220,7 +1312,21 @@ export async function raceMultiPath(args) {
|
|
|
1220
1312
|
const picked = candidates.slice(0, parallelPaths);
|
|
1221
1313
|
const streams = picked.map(() => null);
|
|
1222
1314
|
let winnerIdx = -1;
|
|
1315
|
+
// One controller per path, following the shared signal, so a loser is
|
|
1316
|
+
// cancelled as soon as the winner settles even while it is still waiting
|
|
1317
|
+
// for `newStream`, instead of running on until the shared signal aborts.
|
|
1318
|
+
const pathControllers = picked.map(() => new AbortController());
|
|
1319
|
+
const forwardShared = () => {
|
|
1320
|
+
for (const controller of pathControllers) {
|
|
1321
|
+
if (!controller.signal.aborted)
|
|
1322
|
+
controller.abort(signal.reason);
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
signal.addEventListener('abort', forwardShared, { once: true });
|
|
1223
1326
|
const abortPath = (idx, reason) => {
|
|
1327
|
+
const controller = pathControllers[idx];
|
|
1328
|
+
if (!controller.signal.aborted)
|
|
1329
|
+
controller.abort(reason);
|
|
1224
1330
|
const s = streams[idx];
|
|
1225
1331
|
if (!s || s.aborted)
|
|
1226
1332
|
return;
|
|
@@ -1240,9 +1346,10 @@ export async function raceMultiPath(args) {
|
|
|
1240
1346
|
}
|
|
1241
1347
|
};
|
|
1242
1348
|
const attempts = picked.map(async (conn, idx) => {
|
|
1349
|
+
const pathSignal = pathControllers[idx].signal;
|
|
1243
1350
|
const stream = await conn.newStream(protocolId, {
|
|
1244
1351
|
runOnLimitedConnection: true,
|
|
1245
|
-
signal,
|
|
1352
|
+
signal: pathSignal,
|
|
1246
1353
|
});
|
|
1247
1354
|
streams[idx] = { stream, aborted: false };
|
|
1248
1355
|
if (winnerIdx !== -1 && winnerIdx !== idx) {
|
|
@@ -1259,8 +1366,8 @@ export async function raceMultiPath(args) {
|
|
|
1259
1366
|
throw new Error('multipath: stream returned in closed state');
|
|
1260
1367
|
}
|
|
1261
1368
|
stream.send(data);
|
|
1262
|
-
await stream.close({ signal });
|
|
1263
|
-
return await readAllWithSignal(stream, maxReadBytes,
|
|
1369
|
+
await stream.close({ signal: pathSignal });
|
|
1370
|
+
return await readAllWithSignal(stream, maxReadBytes, pathSignal);
|
|
1264
1371
|
});
|
|
1265
1372
|
let winnerResponse = null;
|
|
1266
1373
|
try {
|
|
@@ -1308,6 +1415,9 @@ export async function raceMultiPath(args) {
|
|
|
1308
1415
|
throw asAbortError(signal.reason);
|
|
1309
1416
|
return null;
|
|
1310
1417
|
}
|
|
1418
|
+
finally {
|
|
1419
|
+
signal.removeEventListener('abort', forwardShared);
|
|
1420
|
+
}
|
|
1311
1421
|
return { response: winnerResponse, attemptedPaths: picked.length };
|
|
1312
1422
|
}
|
|
1313
1423
|
/**
|
|
@@ -1373,6 +1483,11 @@ export async function readAllWithSignal(stream, maxBytes, signal) {
|
|
|
1373
1483
|
* Uses Node's `AbortSignal.any` when available (Node 20.3+, current
|
|
1374
1484
|
* production target). Falls back to a manual composer for older Node
|
|
1375
1485
|
* — keeps tests / older sandboxes working.
|
|
1486
|
+
*
|
|
1487
|
+
* Not for per-request use against a long-lived signal: when an input is an
|
|
1488
|
+
* `AbortSignal.timeout` signal, Node keeps the result, and its entry on the
|
|
1489
|
+
* other input, alive until that input is collected (#2812). The router's
|
|
1490
|
+
* own requests use `startRequestAbortLifecycle` instead.
|
|
1376
1491
|
*/
|
|
1377
1492
|
export function composeAbortSignals(primary, secondary) {
|
|
1378
1493
|
if (!primary)
|