@peerbit/pubsub 5.4.4 → 5.4.5
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/src/fanout-tree.d.ts +7 -2
- package/dist/src/fanout-tree.d.ts.map +1 -1
- package/dist/src/fanout-tree.js +339 -81
- package/dist/src/fanout-tree.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +299 -77
- package/dist/src/index.js.map +1 -1
- package/package.json +2 -2
- package/src/fanout-tree.ts +462 -111
- package/src/index.ts +375 -96
package/src/fanout-tree.ts
CHANGED
|
@@ -586,8 +586,9 @@ export type FanoutTreeJoinOptions = {
|
|
|
586
586
|
/**
|
|
587
587
|
* Candidate scoring mode for selecting parent join targets.
|
|
588
588
|
*
|
|
589
|
-
* - `ranked-shuffle` (default): rank by (level, freeSlots, bid, source)
|
|
590
|
-
* shuffle within `candidateShuffleTopK` to spread load
|
|
589
|
+
* - `ranked-shuffle` (default): rank by (level, freeSlots, bid, source),
|
|
590
|
+
* shuffle within `candidateShuffleTopK` to spread load, then promote at
|
|
591
|
+
* most one already-usable peer for a low-latency first attempt.
|
|
591
592
|
* - `ranked-strict`: try ranked candidates in order (no shuffle).
|
|
592
593
|
* - `weighted`: weighted shuffle within `candidateShuffleTopK` using
|
|
593
594
|
* `candidateScoringWeights` (defaults bias low level + free slots).
|
|
@@ -730,6 +731,18 @@ export type FanoutTreeChannelMetrics = {
|
|
|
730
731
|
joinRejectSent: number;
|
|
731
732
|
joinRejectReceived: number;
|
|
732
733
|
joinPeerResets: number;
|
|
734
|
+
/**
|
|
735
|
+
* Cold-join diagnostics. Implementations populate every counter, while the
|
|
736
|
+
* optional boundary keeps older external metrics fixtures source-compatible.
|
|
737
|
+
*/
|
|
738
|
+
joinBootstrapDialAttempts?: number;
|
|
739
|
+
joinBootstrapDialFailures?: number;
|
|
740
|
+
joinCandidateDialAttempts?: number;
|
|
741
|
+
joinCandidateDialFailures?: number;
|
|
742
|
+
joinConnectedCandidateAttempts?: number;
|
|
743
|
+
joinUnconnectedCandidateAttempts?: number;
|
|
744
|
+
joinReqTimeouts?: number;
|
|
745
|
+
joinDeadlineExpirations?: number;
|
|
733
746
|
kickSent: number;
|
|
734
747
|
kickReceived: number;
|
|
735
748
|
reparentDisconnect: number;
|
|
@@ -808,6 +821,17 @@ export type FanoutTreeChannelMetrics = {
|
|
|
808
821
|
routeProxyRejected: number;
|
|
809
822
|
};
|
|
810
823
|
|
|
824
|
+
type InternalFanoutTreeChannelMetrics = FanoutTreeChannelMetrics & {
|
|
825
|
+
joinBootstrapDialAttempts: number;
|
|
826
|
+
joinBootstrapDialFailures: number;
|
|
827
|
+
joinCandidateDialAttempts: number;
|
|
828
|
+
joinCandidateDialFailures: number;
|
|
829
|
+
joinConnectedCandidateAttempts: number;
|
|
830
|
+
joinUnconnectedCandidateAttempts: number;
|
|
831
|
+
joinReqTimeouts: number;
|
|
832
|
+
joinDeadlineExpirations: number;
|
|
833
|
+
};
|
|
834
|
+
|
|
811
835
|
export interface FanoutTreeEvents extends StreamEvents {
|
|
812
836
|
"fanout:data": CustomEvent<FanoutTreeDataEvent>;
|
|
813
837
|
"fanout:unicast": CustomEvent<FanoutTreeUnicastEvent>;
|
|
@@ -990,6 +1014,13 @@ type JoinAttemptResult = {
|
|
|
990
1014
|
redirects?: Array<{ hash: string; addrs: Multiaddr[] }>;
|
|
991
1015
|
};
|
|
992
1016
|
|
|
1017
|
+
type JoinDialDiagnostics = {
|
|
1018
|
+
metrics: InternalFanoutTreeChannelMetrics;
|
|
1019
|
+
deadlineAt?: number;
|
|
1020
|
+
preferConnected?: boolean;
|
|
1021
|
+
excludeReadyPeerHashes?: Set<string>;
|
|
1022
|
+
};
|
|
1023
|
+
|
|
993
1024
|
type PendingUnicastAck = {
|
|
994
1025
|
expectedOrigin: string;
|
|
995
1026
|
resolve: () => void;
|
|
@@ -1001,7 +1032,7 @@ type PendingUnicastAck = {
|
|
|
1001
1032
|
|
|
1002
1033
|
type ChannelState = {
|
|
1003
1034
|
id: FanoutTreeChannelId;
|
|
1004
|
-
metrics:
|
|
1035
|
+
metrics: InternalFanoutTreeChannelMetrics;
|
|
1005
1036
|
level: number;
|
|
1006
1037
|
isRoot: boolean;
|
|
1007
1038
|
closeController: AbortController;
|
|
@@ -1212,7 +1243,7 @@ const createDeferred = (): {
|
|
|
1212
1243
|
return { resolve, reject, promise };
|
|
1213
1244
|
};
|
|
1214
1245
|
|
|
1215
|
-
const createEmptyMetrics = ():
|
|
1246
|
+
const createEmptyMetrics = (): InternalFanoutTreeChannelMetrics => ({
|
|
1216
1247
|
controlSends: 0,
|
|
1217
1248
|
controlBytesSent: 0,
|
|
1218
1249
|
controlReceives: 0,
|
|
@@ -1238,6 +1269,14 @@ const createEmptyMetrics = (): FanoutTreeChannelMetrics => ({
|
|
|
1238
1269
|
joinRejectSent: 0,
|
|
1239
1270
|
joinRejectReceived: 0,
|
|
1240
1271
|
joinPeerResets: 0,
|
|
1272
|
+
joinBootstrapDialAttempts: 0,
|
|
1273
|
+
joinBootstrapDialFailures: 0,
|
|
1274
|
+
joinCandidateDialAttempts: 0,
|
|
1275
|
+
joinCandidateDialFailures: 0,
|
|
1276
|
+
joinConnectedCandidateAttempts: 0,
|
|
1277
|
+
joinUnconnectedCandidateAttempts: 0,
|
|
1278
|
+
joinReqTimeouts: 0,
|
|
1279
|
+
joinDeadlineExpirations: 0,
|
|
1241
1280
|
kickSent: 0,
|
|
1242
1281
|
kickReceived: 0,
|
|
1243
1282
|
reparentDisconnect: 0,
|
|
@@ -1324,7 +1363,7 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
1324
1363
|
private readonly cachedSuffixKey = new WeakMap<Uint8Array, string>();
|
|
1325
1364
|
private readonly metricsBySuffixKey = new Map<
|
|
1326
1365
|
string,
|
|
1327
|
-
|
|
1366
|
+
InternalFanoutTreeChannelMetrics
|
|
1328
1367
|
>();
|
|
1329
1368
|
private readonly joinTimeoutStreakByPeer = new Map<string, number>();
|
|
1330
1369
|
private readonly joinResetCooldownUntilByPeer = new Map<string, number>();
|
|
@@ -3943,7 +3982,9 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
3943
3982
|
return key;
|
|
3944
3983
|
}
|
|
3945
3984
|
|
|
3946
|
-
private getMetricsForSuffixKey(
|
|
3985
|
+
private getMetricsForSuffixKey(
|
|
3986
|
+
suffixKey: string,
|
|
3987
|
+
): InternalFanoutTreeChannelMetrics {
|
|
3947
3988
|
let m = this.metricsBySuffixKey.get(suffixKey);
|
|
3948
3989
|
if (!m) {
|
|
3949
3990
|
m = createEmptyMetrics();
|
|
@@ -4160,7 +4201,14 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
4160
4201
|
}
|
|
4161
4202
|
}
|
|
4162
4203
|
|
|
4163
|
-
private async _sendControl(
|
|
4204
|
+
private async _sendControl(
|
|
4205
|
+
to: string,
|
|
4206
|
+
bytes: Uint8Array,
|
|
4207
|
+
signal?: AbortSignal,
|
|
4208
|
+
) {
|
|
4209
|
+
if (signal?.aborted) {
|
|
4210
|
+
throw signal.reason ?? new AbortError("fanout control send aborted");
|
|
4211
|
+
}
|
|
4164
4212
|
const stream = this.peers.get(to);
|
|
4165
4213
|
if (!stream) return;
|
|
4166
4214
|
this.recordControlSend(bytes, 1);
|
|
@@ -4168,7 +4216,16 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
4168
4216
|
mode: new AnyWhere(),
|
|
4169
4217
|
priority: CONTROL_PRIORITY,
|
|
4170
4218
|
} as any);
|
|
4171
|
-
|
|
4219
|
+
if (signal?.aborted) {
|
|
4220
|
+
throw signal.reason ?? new AbortError("fanout control send aborted");
|
|
4221
|
+
}
|
|
4222
|
+
await this.publishMessageMaybe(
|
|
4223
|
+
this.publicKey,
|
|
4224
|
+
message,
|
|
4225
|
+
[stream],
|
|
4226
|
+
undefined,
|
|
4227
|
+
signal,
|
|
4228
|
+
);
|
|
4172
4229
|
}
|
|
4173
4230
|
|
|
4174
4231
|
private async _sendControlMany(to: string[], bytes: Uint8Array) {
|
|
@@ -4894,35 +4951,157 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
4894
4951
|
return out;
|
|
4895
4952
|
}
|
|
4896
4953
|
|
|
4954
|
+
private isPeerReadyForJoin(hash: string): boolean {
|
|
4955
|
+
const stream = this.peers.get(hash);
|
|
4956
|
+
if (!stream || !stream.isReadable || !stream.isWritable) return false;
|
|
4957
|
+
try {
|
|
4958
|
+
return (
|
|
4959
|
+
this.components.connectionManager.getConnections(stream.peerId).length > 0
|
|
4960
|
+
);
|
|
4961
|
+
} catch {
|
|
4962
|
+
// Test/mocked connection managers may not expose peer-scoped snapshots.
|
|
4963
|
+
return true;
|
|
4964
|
+
}
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4967
|
+
private connectedPeerHashForBootstrap(
|
|
4968
|
+
address: Multiaddr,
|
|
4969
|
+
): string | undefined {
|
|
4970
|
+
const peerId = address
|
|
4971
|
+
.getComponents()
|
|
4972
|
+
.filter((component) => component.name === "p2p")
|
|
4973
|
+
.at(-1)?.value;
|
|
4974
|
+
if (!peerId) return;
|
|
4975
|
+
for (const [hash, stream] of this.peers) {
|
|
4976
|
+
if (stream.peerId.toString() !== peerId) continue;
|
|
4977
|
+
if (this.isPeerReadyForJoin(hash)) return hash;
|
|
4978
|
+
}
|
|
4979
|
+
return;
|
|
4980
|
+
}
|
|
4981
|
+
|
|
4982
|
+
private createBoundedDialAttempt(
|
|
4983
|
+
signal: AbortSignal,
|
|
4984
|
+
timeoutMs: number,
|
|
4985
|
+
deadlineAt?: number,
|
|
4986
|
+
):
|
|
4987
|
+
| {
|
|
4988
|
+
signal: AbortSignal;
|
|
4989
|
+
timeoutMs: number;
|
|
4990
|
+
clear: () => void;
|
|
4991
|
+
}
|
|
4992
|
+
| undefined {
|
|
4993
|
+
if (signal.aborted) return;
|
|
4994
|
+
const remainingMs =
|
|
4995
|
+
deadlineAt == null
|
|
4996
|
+
? Number.POSITIVE_INFINITY
|
|
4997
|
+
: Math.max(0, deadlineAt - Date.now());
|
|
4998
|
+
if (remainingMs <= 0) return;
|
|
4999
|
+
const boundedTimeoutMs = Math.max(
|
|
5000
|
+
1,
|
|
5001
|
+
Math.min(Math.max(1, Math.floor(timeoutMs)), remainingMs),
|
|
5002
|
+
);
|
|
5003
|
+
const timeoutSignal = AbortSignal.timeout(boundedTimeoutMs);
|
|
5004
|
+
const combined = anySignal([signal, timeoutSignal]) as AbortSignal & {
|
|
5005
|
+
clear?: () => void;
|
|
5006
|
+
};
|
|
5007
|
+
return {
|
|
5008
|
+
signal: combined,
|
|
5009
|
+
timeoutMs: boundedTimeoutMs,
|
|
5010
|
+
clear: () => combined.clear?.(),
|
|
5011
|
+
};
|
|
5012
|
+
}
|
|
5013
|
+
|
|
4897
5014
|
private async ensureBootstrapPeers(
|
|
4898
5015
|
addrs: Multiaddr[],
|
|
4899
5016
|
timeoutMs: number,
|
|
4900
5017
|
signal: AbortSignal,
|
|
4901
5018
|
maxPeers = 0,
|
|
5019
|
+
diagnostics?: JoinDialDiagnostics,
|
|
4902
5020
|
): Promise<string[]> {
|
|
4903
5021
|
if (addrs.length === 0) return [];
|
|
4904
5022
|
const max = Math.max(0, Math.floor(maxPeers));
|
|
4905
|
-
const
|
|
5023
|
+
const connected: string[] = [];
|
|
5024
|
+
const disconnected: Multiaddr[] = [];
|
|
5025
|
+
const connectedSeen = new Set<string>();
|
|
5026
|
+
for (const address of addrs) {
|
|
5027
|
+
const readyHash = diagnostics
|
|
5028
|
+
? this.connectedPeerHashForBootstrap(address)
|
|
5029
|
+
: undefined;
|
|
5030
|
+
if (
|
|
5031
|
+
readyHash &&
|
|
5032
|
+
diagnostics?.excludeReadyPeerHashes?.has(readyHash)
|
|
5033
|
+
) {
|
|
5034
|
+
continue;
|
|
5035
|
+
}
|
|
5036
|
+
const hash = diagnostics?.preferConnected ? readyHash : undefined;
|
|
5037
|
+
if (!hash) {
|
|
5038
|
+
disconnected.push(address);
|
|
5039
|
+
continue;
|
|
5040
|
+
}
|
|
5041
|
+
if (!connectedSeen.has(hash)) {
|
|
5042
|
+
connectedSeen.add(hash);
|
|
5043
|
+
connected.push(hash);
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
5046
|
+
const connectedLimit =
|
|
5047
|
+
max > 0 ? Math.min(max, connected.length) : connected.length;
|
|
5048
|
+
const out = connected.slice(0, connectedLimit);
|
|
5049
|
+
if (diagnostics?.preferConnected && out.length > 0) return out;
|
|
5050
|
+
|
|
5051
|
+
const shuffled = disconnected.slice();
|
|
4906
5052
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
4907
5053
|
const j = Math.floor(this.random() * (i + 1));
|
|
4908
5054
|
const tmp = shuffled[i]!;
|
|
4909
5055
|
shuffled[i] = shuffled[j]!;
|
|
4910
5056
|
shuffled[j] = tmp;
|
|
4911
5057
|
}
|
|
4912
|
-
const target = max > 0 ? Math.min(max,
|
|
4913
|
-
const out: string[] = [];
|
|
5058
|
+
const target = max > 0 ? Math.min(max, addrs.length) : addrs.length;
|
|
4914
5059
|
for (const a of shuffled) {
|
|
4915
5060
|
if (signal.aborted) break;
|
|
4916
5061
|
if (target > 0 && out.length >= target) break;
|
|
5062
|
+
const attempt = diagnostics
|
|
5063
|
+
? this.createBoundedDialAttempt(
|
|
5064
|
+
signal,
|
|
5065
|
+
timeoutMs,
|
|
5066
|
+
diagnostics.deadlineAt,
|
|
5067
|
+
)
|
|
5068
|
+
: undefined;
|
|
5069
|
+
if (diagnostics && !attempt) break;
|
|
5070
|
+
if (diagnostics) diagnostics.metrics.joinBootstrapDialAttempts += 1;
|
|
5071
|
+
let ready = false;
|
|
5072
|
+
let skippedExcluded = false;
|
|
4917
5073
|
try {
|
|
4918
|
-
const conn =
|
|
5074
|
+
const conn = attempt
|
|
5075
|
+
? await this.components.connectionManager.openConnection(a, {
|
|
5076
|
+
signal: attempt.signal,
|
|
5077
|
+
})
|
|
5078
|
+
: await this.components.connectionManager.openConnection(a);
|
|
4919
5079
|
const h = getPublicKeyFromPeerId(conn.remotePeer).hashcode();
|
|
4920
|
-
await this.waitFor(h, {
|
|
4921
|
-
|
|
5080
|
+
await this.waitFor(h, {
|
|
5081
|
+
seek: "present",
|
|
5082
|
+
timeout: attempt?.timeoutMs ?? timeoutMs,
|
|
5083
|
+
signal: attempt?.signal ?? signal,
|
|
5084
|
+
});
|
|
5085
|
+
skippedExcluded =
|
|
5086
|
+
diagnostics?.excludeReadyPeerHashes?.has(h) === true;
|
|
5087
|
+
ready =
|
|
5088
|
+
!skippedExcluded &&
|
|
5089
|
+
(diagnostics ? this.isPeerReadyForJoin(h) : true);
|
|
5090
|
+
if (ready) out.push(h);
|
|
4922
5091
|
} catch {
|
|
4923
5092
|
// ignore dial failures
|
|
5093
|
+
} finally {
|
|
5094
|
+
attempt?.clear();
|
|
5095
|
+
if (!ready && !skippedExcluded && diagnostics) {
|
|
5096
|
+
diagnostics.metrics.joinBootstrapDialFailures += 1;
|
|
5097
|
+
}
|
|
4924
5098
|
}
|
|
5099
|
+
if (ready && diagnostics?.preferConnected) break;
|
|
4925
5100
|
}
|
|
5101
|
+
// Exhaustion completes one bounded pass over the configured bootstrap set.
|
|
5102
|
+
// Allow a later round to revisit ready trackers because their candidate view
|
|
5103
|
+
// may have changed while this cold join was progressing.
|
|
5104
|
+
if (out.length === 0) diagnostics?.excludeReadyPeerHashes?.clear();
|
|
4926
5105
|
return [...new Set(out)];
|
|
4927
5106
|
}
|
|
4928
5107
|
|
|
@@ -5351,21 +5530,44 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
5351
5530
|
addrs: Multiaddr[],
|
|
5352
5531
|
timeoutMs: number,
|
|
5353
5532
|
signal: AbortSignal,
|
|
5533
|
+
diagnostics?: JoinDialDiagnostics,
|
|
5354
5534
|
): Promise<boolean> {
|
|
5355
|
-
if (this.
|
|
5535
|
+
if (this.isPeerReadyForJoin(hash)) return true;
|
|
5356
5536
|
for (const a of addrs) {
|
|
5357
5537
|
if (signal.aborted) return false;
|
|
5538
|
+
const attempt = diagnostics
|
|
5539
|
+
? this.createBoundedDialAttempt(
|
|
5540
|
+
signal,
|
|
5541
|
+
timeoutMs,
|
|
5542
|
+
diagnostics.deadlineAt,
|
|
5543
|
+
)
|
|
5544
|
+
: undefined;
|
|
5545
|
+
if (diagnostics && !attempt) return false;
|
|
5546
|
+
if (diagnostics) diagnostics.metrics.joinCandidateDialAttempts += 1;
|
|
5547
|
+
let ready = false;
|
|
5358
5548
|
try {
|
|
5359
|
-
|
|
5549
|
+
if (attempt) {
|
|
5550
|
+
await this.components.connectionManager.openConnection(a, {
|
|
5551
|
+
signal: attempt.signal,
|
|
5552
|
+
});
|
|
5553
|
+
} else {
|
|
5554
|
+
await this.components.connectionManager.openConnection(a);
|
|
5555
|
+
}
|
|
5360
5556
|
await this.waitFor(hash, {
|
|
5361
5557
|
seek: "present",
|
|
5362
|
-
timeout: timeoutMs,
|
|
5363
|
-
signal,
|
|
5558
|
+
timeout: attempt?.timeoutMs ?? timeoutMs,
|
|
5559
|
+
signal: attempt?.signal ?? signal,
|
|
5364
5560
|
});
|
|
5365
|
-
|
|
5561
|
+
ready = diagnostics ? this.isPeerReadyForJoin(hash) : true;
|
|
5366
5562
|
} catch {
|
|
5367
5563
|
// ignore and try next
|
|
5564
|
+
} finally {
|
|
5565
|
+
attempt?.clear();
|
|
5566
|
+
if (!ready && diagnostics) {
|
|
5567
|
+
diagnostics.metrics.joinCandidateDialFailures += 1;
|
|
5568
|
+
}
|
|
5368
5569
|
}
|
|
5570
|
+
if (ready) return true;
|
|
5369
5571
|
}
|
|
5370
5572
|
return false;
|
|
5371
5573
|
}
|
|
@@ -5503,6 +5705,22 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
5503
5705
|
);
|
|
5504
5706
|
}
|
|
5505
5707
|
|
|
5708
|
+
private sendTrackerFeedbackBestEffort(
|
|
5709
|
+
ch: ChannelState,
|
|
5710
|
+
trackerPeers: string[],
|
|
5711
|
+
candidateHash: string,
|
|
5712
|
+
event: number,
|
|
5713
|
+
reason = 0,
|
|
5714
|
+
): void {
|
|
5715
|
+
void this.sendTrackerFeedback(
|
|
5716
|
+
ch,
|
|
5717
|
+
trackerPeers,
|
|
5718
|
+
candidateHash,
|
|
5719
|
+
event,
|
|
5720
|
+
reason,
|
|
5721
|
+
).catch(() => {});
|
|
5722
|
+
}
|
|
5723
|
+
|
|
5506
5724
|
private pruneParentUpgradeReservations(ch: ChannelState, now = Date.now()) {
|
|
5507
5725
|
for (const [hash, reservation] of ch.parentUpgradeReservationsByHash) {
|
|
5508
5726
|
if (reservation.expiresAt <= now) {
|
|
@@ -5767,14 +5985,45 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
5767
5985
|
source: Number(joinOpts.candidateScoringWeights?.source ?? 0.25),
|
|
5768
5986
|
};
|
|
5769
5987
|
const start = Date.now();
|
|
5988
|
+
const initialJoinDeadlineAt = timeoutMs > 0 ? start + timeoutMs : undefined;
|
|
5770
5989
|
const cooldownUntilByHash = new Map<string, number>();
|
|
5771
5990
|
const combinedSignal = joinOpts.signal
|
|
5772
5991
|
? anySignal([ch.closeController.signal, joinOpts.signal])
|
|
5773
5992
|
: ch.closeController.signal;
|
|
5774
5993
|
const signal = combinedSignal as AbortSignal & { clear?: () => void };
|
|
5994
|
+
const initialJoinRemainingMs = () =>
|
|
5995
|
+
!ch.joinedAtLeastOnce && initialJoinDeadlineAt != null
|
|
5996
|
+
? Math.max(0, initialJoinDeadlineAt - Date.now())
|
|
5997
|
+
: undefined;
|
|
5998
|
+
const clampInitialJoinWait = (requestedMs: number) => {
|
|
5999
|
+
const remainingMs = initialJoinRemainingMs();
|
|
6000
|
+
return remainingMs == null
|
|
6001
|
+
? requestedMs
|
|
6002
|
+
: Math.min(requestedMs, remainingMs);
|
|
6003
|
+
};
|
|
6004
|
+
const throwInitialJoinTimeout = (): never => {
|
|
6005
|
+
ch.metrics.joinDeadlineExpirations += 1;
|
|
6006
|
+
const bootstrapsCount = this.getBootstrapsForChannel(ch).length;
|
|
6007
|
+
const rootPeer = this.peers.get(ch.id.root);
|
|
6008
|
+
const rootNeighbor = Boolean(
|
|
6009
|
+
rootPeer && rootPeer.isReadable && rootPeer.isWritable,
|
|
6010
|
+
);
|
|
6011
|
+
const bootstrapHint =
|
|
6012
|
+
bootstrapsCount === 0 && !rootNeighbor
|
|
6013
|
+
? " No fanout bootstraps are configured for this channel, and the root is not a direct neighbor. If this peer reached the network via a bootstrap or relay node, initialize it with Peerbit.bootstrap(...) instead of Peerbit.dial(...), or configure FanoutTree.setBootstraps(...) before joining sharded topics."
|
|
6014
|
+
: "";
|
|
6015
|
+
throw new Error(
|
|
6016
|
+
`fanout join timed out after ${timeoutMs}ms (topic=${ch.id.topic} root=${ch.id.root} self=${this.publicKeyHash} rootNeighbor=${rootNeighbor} peers=${this.peers.size} bootstraps=${bootstrapsCount} joinReqSent=${ch.metrics.joinReqSent} joinAcceptReceived=${ch.metrics.joinAcceptReceived} joinRejectReceived=${ch.metrics.joinRejectReceived} peerResets=${ch.metrics.joinPeerResets}).${bootstrapHint}`,
|
|
6017
|
+
);
|
|
6018
|
+
};
|
|
6019
|
+
const throwIfInitialJoinTimedOut = () => {
|
|
6020
|
+
if (initialJoinRemainingMs() === 0) throwInitialJoinTimeout();
|
|
6021
|
+
};
|
|
5775
6022
|
let nextParentUpgradeCheckAt = 0;
|
|
5776
6023
|
let parentUpgradeCheckSeq = 0;
|
|
5777
6024
|
let parentUpgradeActiveGuardBackoffMs = 0;
|
|
6025
|
+
const unsuccessfulColdBootstrapPeers = new Set<string>();
|
|
6026
|
+
let bootstrapFallbackRetryAt = 0;
|
|
5778
6027
|
const scheduleNextParentUpgradeCheck = (
|
|
5779
6028
|
now: number,
|
|
5780
6029
|
first = false,
|
|
@@ -6036,30 +6285,16 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
6036
6285
|
continue;
|
|
6037
6286
|
}
|
|
6038
6287
|
|
|
6039
|
-
// `timeoutMs`
|
|
6040
|
-
//
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
timeoutMs > 0 &&
|
|
6044
|
-
Date.now() - start > timeoutMs
|
|
6045
|
-
) {
|
|
6046
|
-
const bootstrapsCount = this.getBootstrapsForChannel(ch).length;
|
|
6047
|
-
const rootPeer = this.peers.get(ch.id.root);
|
|
6048
|
-
const rootNeighbor = Boolean(
|
|
6049
|
-
rootPeer && rootPeer.isReadable && rootPeer.isWritable,
|
|
6050
|
-
);
|
|
6051
|
-
const bootstrapHint =
|
|
6052
|
-
bootstrapsCount === 0 && !rootNeighbor
|
|
6053
|
-
? " No fanout bootstraps are configured for this channel, and the root is not a direct neighbor. If this peer reached the network via a bootstrap or relay node, initialize it with Peerbit.bootstrap(...) instead of Peerbit.dial(...), or configure FanoutTree.setBootstraps(...) before joining sharded topics."
|
|
6054
|
-
: "";
|
|
6055
|
-
throw new Error(
|
|
6056
|
-
`fanout join timed out after ${timeoutMs}ms (topic=${ch.id.topic} root=${ch.id.root} self=${this.publicKeyHash} rootNeighbor=${rootNeighbor} peers=${this.peers.size} bootstraps=${bootstrapsCount} joinReqSent=${ch.metrics.joinReqSent} joinAcceptReceived=${ch.metrics.joinAcceptReceived} joinRejectReceived=${ch.metrics.joinRejectReceived} peerResets=${ch.metrics.joinPeerResets}).${bootstrapHint}`,
|
|
6057
|
-
);
|
|
6058
|
-
}
|
|
6288
|
+
// `timeoutMs` bounds only the initial `joinChannel()` await. Re-parenting
|
|
6289
|
+
// remains unbounded after the first attachment, while every cold-open wait
|
|
6290
|
+
// below is clamped to this same absolute deadline.
|
|
6291
|
+
throwIfInitialJoinTimedOut();
|
|
6059
6292
|
|
|
6060
6293
|
const cooldownMs = ch.rejoinCooldownUntil - Date.now();
|
|
6061
6294
|
if (cooldownMs > 0) {
|
|
6062
|
-
|
|
6295
|
+
const waitMs = clampInitialJoinWait(cooldownMs);
|
|
6296
|
+
if (waitMs <= 0) throwInitialJoinTimeout();
|
|
6297
|
+
await delay(waitMs, { signal });
|
|
6063
6298
|
continue;
|
|
6064
6299
|
}
|
|
6065
6300
|
|
|
@@ -6067,10 +6302,18 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
6067
6302
|
let bootstrapPeers: string[] = [];
|
|
6068
6303
|
if (bootstraps.length > 0) {
|
|
6069
6304
|
const now = Date.now();
|
|
6070
|
-
const connectedCached = ch.cachedBootstrapPeers.filter(
|
|
6071
|
-
|
|
6305
|
+
const connectedCached = ch.cachedBootstrapPeers.filter(
|
|
6306
|
+
(h) =>
|
|
6307
|
+
this.isPeerReadyForJoin(h) &&
|
|
6308
|
+
!unsuccessfulColdBootstrapPeers.has(h),
|
|
6309
|
+
);
|
|
6310
|
+
const hasExcludedReadyCached = ch.cachedBootstrapPeers.some(
|
|
6311
|
+
(h) =>
|
|
6312
|
+
this.isPeerReadyForJoin(h) &&
|
|
6313
|
+
unsuccessfulColdBootstrapPeers.has(h),
|
|
6072
6314
|
);
|
|
6073
6315
|
const due =
|
|
6316
|
+
(hasExcludedReadyCached && now >= bootstrapFallbackRetryAt) ||
|
|
6074
6317
|
ch.lastBootstrapEnsureAt === 0 ||
|
|
6075
6318
|
bootstrapEnsureIntervalMs === 0 ||
|
|
6076
6319
|
now - ch.lastBootstrapEnsureAt >= bootstrapEnsureIntervalMs;
|
|
@@ -6080,18 +6323,45 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
6080
6323
|
: false;
|
|
6081
6324
|
if (due && !haveEnough) {
|
|
6082
6325
|
ch.lastBootstrapEnsureAt = now;
|
|
6326
|
+
const wasFallbackPass = unsuccessfulColdBootstrapPeers.size > 0;
|
|
6327
|
+
const diagnostics = !ch.joinedAtLeastOnce
|
|
6328
|
+
? {
|
|
6329
|
+
metrics: ch.metrics,
|
|
6330
|
+
deadlineAt: initialJoinDeadlineAt,
|
|
6331
|
+
preferConnected: true,
|
|
6332
|
+
excludeReadyPeerHashes: unsuccessfulColdBootstrapPeers,
|
|
6333
|
+
}
|
|
6334
|
+
: undefined;
|
|
6083
6335
|
const peers = await this.ensureBootstrapPeers(
|
|
6084
6336
|
bootstraps,
|
|
6085
6337
|
bootstrapDialTimeoutMs,
|
|
6086
6338
|
signal,
|
|
6087
6339
|
bootstrapMaxPeers,
|
|
6340
|
+
diagnostics,
|
|
6088
6341
|
);
|
|
6089
|
-
if (peers.length > 0)
|
|
6342
|
+
if (peers.length > 0) {
|
|
6343
|
+
const cohortChanged =
|
|
6344
|
+
peers.length !== ch.cachedBootstrapPeers.length ||
|
|
6345
|
+
peers.some(
|
|
6346
|
+
(hash, index) => ch.cachedBootstrapPeers[index] !== hash,
|
|
6347
|
+
);
|
|
6348
|
+
ch.cachedBootstrapPeers = peers;
|
|
6349
|
+
if (cohortChanged) {
|
|
6350
|
+
ch.lastTrackerQueryAt = 0;
|
|
6351
|
+
ch.cachedTrackerCandidates = [];
|
|
6352
|
+
}
|
|
6353
|
+
} else if (wasFallbackPass) {
|
|
6354
|
+
bootstrapFallbackRetryAt =
|
|
6355
|
+
Date.now() + Math.max(1, bootstrapEnsureIntervalMs);
|
|
6356
|
+
}
|
|
6090
6357
|
}
|
|
6091
|
-
bootstrapPeers = ch.cachedBootstrapPeers.filter(
|
|
6092
|
-
|
|
6358
|
+
bootstrapPeers = ch.cachedBootstrapPeers.filter(
|
|
6359
|
+
(h) =>
|
|
6360
|
+
this.isPeerReadyForJoin(h) &&
|
|
6361
|
+
!unsuccessfulColdBootstrapPeers.has(h),
|
|
6093
6362
|
);
|
|
6094
6363
|
}
|
|
6364
|
+
throwIfInitialJoinTimedOut();
|
|
6095
6365
|
|
|
6096
6366
|
let tracker: TrackerCandidate[] = [];
|
|
6097
6367
|
if (bootstrapPeers.length > 0 && trackerCandidates > 0) {
|
|
@@ -6102,17 +6372,22 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
6102
6372
|
now - ch.lastTrackerQueryAt >= trackerQueryIntervalMs;
|
|
6103
6373
|
if (due) {
|
|
6104
6374
|
ch.lastTrackerQueryAt = now;
|
|
6375
|
+
const queryTimeoutMs = clampInitialJoinWait(
|
|
6376
|
+
Math.max(1, trackerQueryTimeoutMs),
|
|
6377
|
+
);
|
|
6378
|
+
if (queryTimeoutMs <= 0) throwInitialJoinTimeout();
|
|
6105
6379
|
const res = await this.queryTrackers(
|
|
6106
6380
|
ch,
|
|
6107
6381
|
bootstrapPeers,
|
|
6108
6382
|
trackerCandidates,
|
|
6109
|
-
|
|
6383
|
+
queryTimeoutMs,
|
|
6110
6384
|
signal,
|
|
6111
6385
|
);
|
|
6112
6386
|
if (res.length > 0) ch.cachedTrackerCandidates = res;
|
|
6113
6387
|
}
|
|
6114
6388
|
tracker = ch.cachedTrackerCandidates;
|
|
6115
6389
|
}
|
|
6390
|
+
throwIfInitialJoinTimedOut();
|
|
6116
6391
|
|
|
6117
6392
|
const candidatesByHash = new Map<
|
|
6118
6393
|
string,
|
|
@@ -6150,7 +6425,10 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
6150
6425
|
// Fast path: if the designated root is already a direct neighbor, try it first.
|
|
6151
6426
|
// Without this, large join storms can repeatedly time out on arbitrary peers
|
|
6152
6427
|
// that don't host the channel yet, starving the real root candidate.
|
|
6153
|
-
if (
|
|
6428
|
+
if (
|
|
6429
|
+
ch.id.root !== this.publicKeyHash &&
|
|
6430
|
+
this.isPeerReadyForJoin(ch.id.root)
|
|
6431
|
+
) {
|
|
6154
6432
|
upsertCandidate({
|
|
6155
6433
|
hash: ch.id.root,
|
|
6156
6434
|
addrs: [],
|
|
@@ -6198,6 +6476,7 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
6198
6476
|
const connectedFallbackMax = 64;
|
|
6199
6477
|
for (const h of this.peers.keys()) {
|
|
6200
6478
|
if (h === this.publicKeyHash) continue;
|
|
6479
|
+
if (!this.isPeerReadyForJoin(h)) continue;
|
|
6201
6480
|
if (bootstrapPeerSet.has(h) && candidatesByHash.has(h)) continue;
|
|
6202
6481
|
upsertCandidate({
|
|
6203
6482
|
hash: h,
|
|
@@ -6245,10 +6524,16 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
6245
6524
|
retryMs,
|
|
6246
6525
|
trackerQueryIntervalMs > 0 ? trackerQueryIntervalMs : retryMs,
|
|
6247
6526
|
);
|
|
6248
|
-
|
|
6527
|
+
const boundedWaitMs = clampInitialJoinWait(
|
|
6528
|
+
Math.max(1, Math.min(waitMs, capMs)),
|
|
6529
|
+
);
|
|
6530
|
+
if (boundedWaitMs <= 0) throwInitialJoinTimeout();
|
|
6531
|
+
await delay(boundedWaitMs, { signal });
|
|
6249
6532
|
continue;
|
|
6250
6533
|
}
|
|
6251
|
-
|
|
6534
|
+
const waitMs = clampInitialJoinWait(retryMs);
|
|
6535
|
+
if (waitMs <= 0) throwInitialJoinTimeout();
|
|
6536
|
+
await delay(waitMs, { signal });
|
|
6252
6537
|
continue;
|
|
6253
6538
|
}
|
|
6254
6539
|
|
|
@@ -6265,6 +6550,16 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
6265
6550
|
ordered[j] = tmp;
|
|
6266
6551
|
}
|
|
6267
6552
|
}
|
|
6553
|
+
|
|
6554
|
+
// Give one usable peer a latency advantage without moving every
|
|
6555
|
+
// connected fallback ahead of better-ranked dialable candidates.
|
|
6556
|
+
const firstReadyIndex = ordered.findIndex((candidate) =>
|
|
6557
|
+
this.isPeerReadyForJoin(candidate.hash),
|
|
6558
|
+
);
|
|
6559
|
+
if (firstReadyIndex > 0) {
|
|
6560
|
+
const [firstReady] = ordered.splice(firstReadyIndex, 1);
|
|
6561
|
+
ordered.unshift(firstReady!);
|
|
6562
|
+
}
|
|
6268
6563
|
} else if (candidateScoringMode === "weighted") {
|
|
6269
6564
|
const wLevel = Number.isFinite(candidateScoringWeights.level)
|
|
6270
6565
|
? Math.max(0, candidateScoringWeights.level)
|
|
@@ -6348,9 +6643,15 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
6348
6643
|
) {
|
|
6349
6644
|
if (signal.aborted) break;
|
|
6350
6645
|
if (attempts >= joinAttemptsPerRound) break;
|
|
6646
|
+
throwIfInitialJoinTimedOut();
|
|
6351
6647
|
const c = queue[i]!;
|
|
6352
6648
|
attempts += 1;
|
|
6353
|
-
const wasConnected =
|
|
6649
|
+
const wasConnected = this.isPeerReadyForJoin(c.hash);
|
|
6650
|
+
if (wasConnected) {
|
|
6651
|
+
ch.metrics.joinConnectedCandidateAttempts += 1;
|
|
6652
|
+
} else {
|
|
6653
|
+
ch.metrics.joinUnconnectedCandidateAttempts += 1;
|
|
6654
|
+
}
|
|
6354
6655
|
let dialOk = wasConnected;
|
|
6355
6656
|
if (!dialOk && c.addrs.length > 0) {
|
|
6356
6657
|
dialOk = await this.ensurePeerConnection(
|
|
@@ -6358,19 +6659,18 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
6358
6659
|
c.addrs,
|
|
6359
6660
|
bootstrapDialTimeoutMs,
|
|
6360
6661
|
signal,
|
|
6662
|
+
!ch.joinedAtLeastOnce
|
|
6663
|
+
? { metrics: ch.metrics, deadlineAt: initialJoinDeadlineAt }
|
|
6664
|
+
: undefined,
|
|
6361
6665
|
);
|
|
6362
6666
|
}
|
|
6363
6667
|
if (!dialOk) {
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
);
|
|
6371
|
-
} catch {
|
|
6372
|
-
// ignore
|
|
6373
|
-
}
|
|
6668
|
+
this.sendTrackerFeedbackBestEffort(
|
|
6669
|
+
ch,
|
|
6670
|
+
bootstrapPeers,
|
|
6671
|
+
c.hash,
|
|
6672
|
+
TRACKER_FEEDBACK_DIAL_FAILED,
|
|
6673
|
+
);
|
|
6374
6674
|
if (candidateCooldownMs > 0) {
|
|
6375
6675
|
cooldownUntilByHash.set(
|
|
6376
6676
|
c.hash,
|
|
@@ -6383,11 +6683,15 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
6383
6683
|
dialedNew.add(c.hash);
|
|
6384
6684
|
}
|
|
6385
6685
|
const reqId = (this.random() * 0xffffffff) >>> 0;
|
|
6686
|
+
const requestTimeoutMs = clampInitialJoinWait(
|
|
6687
|
+
Math.max(1, joinReqTimeoutMs),
|
|
6688
|
+
);
|
|
6689
|
+
if (requestTimeoutMs <= 0) throwInitialJoinTimeout();
|
|
6386
6690
|
const res = await this.tryJoinOnce(
|
|
6387
6691
|
ch,
|
|
6388
6692
|
c.hash,
|
|
6389
6693
|
reqId,
|
|
6390
|
-
|
|
6694
|
+
requestTimeoutMs,
|
|
6391
6695
|
signal,
|
|
6392
6696
|
);
|
|
6393
6697
|
|
|
@@ -6412,16 +6716,12 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
6412
6716
|
}
|
|
6413
6717
|
|
|
6414
6718
|
if (res.ok) {
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
);
|
|
6422
|
-
} catch {
|
|
6423
|
-
// ignore
|
|
6424
|
-
}
|
|
6719
|
+
this.sendTrackerFeedbackBestEffort(
|
|
6720
|
+
ch,
|
|
6721
|
+
bootstrapPeers,
|
|
6722
|
+
c.hash,
|
|
6723
|
+
TRACKER_FEEDBACK_JOINED,
|
|
6724
|
+
);
|
|
6425
6725
|
cooldownUntilByHash.delete(c.hash);
|
|
6426
6726
|
break;
|
|
6427
6727
|
}
|
|
@@ -6439,17 +6739,14 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
6439
6739
|
}
|
|
6440
6740
|
|
|
6441
6741
|
if (res.timedOut) {
|
|
6742
|
+
ch.metrics.joinReqTimeouts += 1;
|
|
6442
6743
|
this.noteJoinTimeout(ch, c.hash);
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
);
|
|
6450
|
-
} catch {
|
|
6451
|
-
// ignore
|
|
6452
|
-
}
|
|
6744
|
+
this.sendTrackerFeedbackBestEffort(
|
|
6745
|
+
ch,
|
|
6746
|
+
bootstrapPeers,
|
|
6747
|
+
c.hash,
|
|
6748
|
+
TRACKER_FEEDBACK_JOIN_TIMEOUT,
|
|
6749
|
+
);
|
|
6453
6750
|
if (candidateCooldownMs > 0) {
|
|
6454
6751
|
cooldownUntilByHash.set(
|
|
6455
6752
|
c.hash,
|
|
@@ -6474,17 +6771,13 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
6474
6771
|
Date.now() + candidateCooldownMs * factor,
|
|
6475
6772
|
);
|
|
6476
6773
|
}
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
);
|
|
6485
|
-
} catch {
|
|
6486
|
-
// ignore
|
|
6487
|
-
}
|
|
6774
|
+
this.sendTrackerFeedbackBestEffort(
|
|
6775
|
+
ch,
|
|
6776
|
+
bootstrapPeers,
|
|
6777
|
+
c.hash,
|
|
6778
|
+
TRACKER_FEEDBACK_JOIN_REJECT,
|
|
6779
|
+
rejectReason,
|
|
6780
|
+
);
|
|
6488
6781
|
}
|
|
6489
6782
|
|
|
6490
6783
|
if (ch.parent) {
|
|
@@ -6501,7 +6794,17 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
6501
6794
|
}
|
|
6502
6795
|
continue;
|
|
6503
6796
|
}
|
|
6504
|
-
|
|
6797
|
+
if (
|
|
6798
|
+
bootstraps.length > 1 &&
|
|
6799
|
+
Date.now() >= bootstrapFallbackRetryAt
|
|
6800
|
+
) {
|
|
6801
|
+
for (const hash of bootstrapPeers) {
|
|
6802
|
+
unsuccessfulColdBootstrapPeers.add(hash);
|
|
6803
|
+
}
|
|
6804
|
+
}
|
|
6805
|
+
const waitMs = clampInitialJoinWait(retryMs);
|
|
6806
|
+
if (waitMs <= 0) throwInitialJoinTimeout();
|
|
6807
|
+
await delay(waitMs, { signal });
|
|
6505
6808
|
}
|
|
6506
6809
|
} finally {
|
|
6507
6810
|
signal.clear?.();
|
|
@@ -7652,13 +7955,50 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
7652
7955
|
): Promise<JoinAttemptResult> {
|
|
7653
7956
|
if (ch.parent && options?.allowReplace !== true) return { ok: true };
|
|
7654
7957
|
if (!this.peers.get(parentHash)) return { ok: false, timedOut: true };
|
|
7655
|
-
const
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7958
|
+
const attemptController = new AbortController();
|
|
7959
|
+
let settled = false;
|
|
7960
|
+
let resolveAttempt!: (result: JoinAttemptResult) => void;
|
|
7961
|
+
let rejectAttempt!: (error: unknown) => void;
|
|
7962
|
+
const attempt = new Promise<JoinAttemptResult>((resolve, reject) => {
|
|
7963
|
+
resolveAttempt = resolve;
|
|
7964
|
+
rejectAttempt = reject;
|
|
7660
7965
|
});
|
|
7661
|
-
|
|
7966
|
+
const settleResult = (result: JoinAttemptResult) => {
|
|
7967
|
+
if (settled) return;
|
|
7968
|
+
settled = true;
|
|
7969
|
+
resolveAttempt(result);
|
|
7970
|
+
};
|
|
7971
|
+
const settleError = (error: unknown) => {
|
|
7972
|
+
if (settled) return;
|
|
7973
|
+
settled = true;
|
|
7974
|
+
rejectAttempt(error);
|
|
7975
|
+
};
|
|
7976
|
+
ch.pendingJoin.set(reqId, {
|
|
7977
|
+
resolve: settleResult,
|
|
7978
|
+
shadowAttach: options?.shadowAttach === true,
|
|
7979
|
+
});
|
|
7980
|
+
|
|
7981
|
+
const onAbort = () => {
|
|
7982
|
+
const reason = signal.reason ?? new AbortError("fanout join aborted");
|
|
7983
|
+
if (!attemptController.signal.aborted) {
|
|
7984
|
+
attemptController.abort(reason);
|
|
7985
|
+
}
|
|
7986
|
+
settleError(reason);
|
|
7987
|
+
};
|
|
7988
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
7989
|
+
if (signal.aborted) onAbort();
|
|
7990
|
+
|
|
7991
|
+
const timer = setTimeout(() => {
|
|
7992
|
+
if (!attemptController.signal.aborted) {
|
|
7993
|
+
attemptController.abort(
|
|
7994
|
+
new AbortError("fanout join attempt timed out"),
|
|
7995
|
+
);
|
|
7996
|
+
}
|
|
7997
|
+
settleResult({ ok: false, timedOut: true });
|
|
7998
|
+
}, Math.max(1, timeoutMs));
|
|
7999
|
+
timer.unref?.();
|
|
8000
|
+
|
|
8001
|
+
const send = this._sendControl(
|
|
7662
8002
|
parentHash,
|
|
7663
8003
|
this.codec.encodeJoinReq(
|
|
7664
8004
|
ch.id.key,
|
|
@@ -7666,18 +8006,29 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
7666
8006
|
ch.bidPerByte,
|
|
7667
8007
|
options?.parentUpgradeReservationToken,
|
|
7668
8008
|
),
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
(
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
8009
|
+
attemptController.signal,
|
|
8010
|
+
).catch((error) => {
|
|
8011
|
+
if (settled) return;
|
|
8012
|
+
if (signal.aborted) {
|
|
8013
|
+
settleError(signal.reason ?? error);
|
|
8014
|
+
return;
|
|
8015
|
+
}
|
|
8016
|
+
if (!attemptController.signal.aborted) settleError(error);
|
|
8017
|
+
});
|
|
8018
|
+
|
|
8019
|
+
try {
|
|
8020
|
+
return await attempt;
|
|
8021
|
+
} finally {
|
|
8022
|
+
clearTimeout(timer);
|
|
8023
|
+
signal.removeEventListener("abort", onAbort);
|
|
8024
|
+
ch.pendingJoin.delete(reqId);
|
|
8025
|
+
if (!attemptController.signal.aborted) {
|
|
8026
|
+
attemptController.abort(
|
|
8027
|
+
new AbortError("fanout join attempt settled"),
|
|
8028
|
+
);
|
|
8029
|
+
}
|
|
8030
|
+
void send;
|
|
8031
|
+
}
|
|
7681
8032
|
}
|
|
7682
8033
|
|
|
7683
8034
|
private async kickChildHashes(
|