@peerbit/pubsub 5.3.8 → 5.3.10
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/index.d.ts +15 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +271 -90
- package/dist/src/index.js.map +1 -1
- package/dist/src/topic-root-control-plane.d.ts +11 -8
- package/dist/src/topic-root-control-plane.d.ts.map +1 -1
- package/dist/src/topic-root-control-plane.js +51 -15
- package/dist/src/topic-root-control-plane.js.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +379 -118
- package/src/topic-root-control-plane.ts +89 -15
package/dist/src/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {} from "@libp2p/interface";
|
|
2
2
|
import { PublicSignKey, getPublicKeyFromPeerId } from "@peerbit/crypto";
|
|
3
3
|
import { logger as loggerFn } from "@peerbit/logger";
|
|
4
|
-
import { DataEvent, GetSubscribers, PeerUnavailable, PubSubData, PubSubMessage, PublishEvent, Subscribe, SubscriptionData, SubscriptionEvent, TopicRootCandidates, TopicRootQuery, TopicRootQueryResponse, UnsubcriptionEvent, Unsubscribe, } from "@peerbit/pubsub-interface";
|
|
4
|
+
import { assertCanonicalTopicRootCandidates, assertTopicRootCandidatesFrame, DataEvent, GetSubscribers, isCanonicalTopicRootCandidate, PeerUnavailable, PubSubData, PubSubMessage, PublishEvent, Subscribe, SubscriptionData, SubscriptionEvent, TOPIC_ROOT_CANDIDATES_MAX, TopicRootCandidates, TopicRootQuery, TopicRootQueryResponse, UnsubcriptionEvent, Unsubscribe, } from "@peerbit/pubsub-interface";
|
|
5
5
|
import { DirectStream, } from "@peerbit/stream";
|
|
6
6
|
import { AcknowledgeAnyWhere, AcknowledgeDelivery, AnyWhere, DataMessage, DeliveryError, MessageHeader, NotStartedError, SilentDelivery, deliveryModeHasReceiver, getMsgId, isAcknowledgeAnyWhereDeliveryMode, isAcknowledgeDeliveryMode, isAnyWhereDeliveryMode, isSilentDeliveryMode, } from "@peerbit/stream-interface";
|
|
7
7
|
import { AbortError, TimeoutError, delay } from "@peerbit/time";
|
|
@@ -9,7 +9,7 @@ import { Uint8ArrayList } from "uint8arraylist";
|
|
|
9
9
|
import { toUint8Array } from "./bytes.js";
|
|
10
10
|
import { debouncedAccumulatorSetCounter, } from "./debounced-set.js";
|
|
11
11
|
import { FanoutChannel } from "./fanout-channel.js";
|
|
12
|
-
import { TopicRootControlPlane } from "./topic-root-control-plane.js";
|
|
12
|
+
import { TopicRootControlPlane, } from "./topic-root-control-plane.js";
|
|
13
13
|
export * from "./fanout-tree.js";
|
|
14
14
|
// The complete /peerbit/fanout-tree/0.5.0 wire codec and the
|
|
15
15
|
// parent-upgrade decision module (the TS references the native rust-core
|
|
@@ -43,6 +43,7 @@ const withAbort = async (promise, signal) => {
|
|
|
43
43
|
if (!signal)
|
|
44
44
|
return promise;
|
|
45
45
|
if (signal.aborted) {
|
|
46
|
+
void promise.catch(() => { });
|
|
46
47
|
throw signal.reason ?? new AbortError("Operation was aborted");
|
|
47
48
|
}
|
|
48
49
|
return new Promise((resolve, reject) => {
|
|
@@ -68,6 +69,41 @@ const withAbort = async (promise, signal) => {
|
|
|
68
69
|
});
|
|
69
70
|
});
|
|
70
71
|
};
|
|
72
|
+
const throwIfAborted = (signal) => {
|
|
73
|
+
if (signal?.aborted) {
|
|
74
|
+
throw signal.reason ?? new AbortError("Operation was aborted");
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const linkAbortSignals = (signals) => {
|
|
78
|
+
const unique = [...new Set(signals.filter((signal) => signal !== undefined))];
|
|
79
|
+
if (unique.length === 1) {
|
|
80
|
+
return { signal: unique[0], clear: () => { } };
|
|
81
|
+
}
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
const listeners = new Map();
|
|
84
|
+
const clear = () => {
|
|
85
|
+
for (const [signal, listener] of listeners) {
|
|
86
|
+
signal.removeEventListener("abort", listener);
|
|
87
|
+
}
|
|
88
|
+
listeners.clear();
|
|
89
|
+
};
|
|
90
|
+
const abortFrom = (signal) => {
|
|
91
|
+
clear();
|
|
92
|
+
controller.abort(signal.reason ?? new AbortError("Topic-root resolution aborted"));
|
|
93
|
+
};
|
|
94
|
+
const aborted = unique.find((signal) => signal.aborted);
|
|
95
|
+
if (aborted) {
|
|
96
|
+
abortFrom(aborted);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
for (const signal of unique) {
|
|
100
|
+
const listener = () => abortFrom(signal);
|
|
101
|
+
listeners.set(signal, listener);
|
|
102
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { signal: controller.signal, clear };
|
|
106
|
+
};
|
|
71
107
|
const SUBSCRIBER_CACHE_MAX_ENTRIES_HARD_CAP = 100_000;
|
|
72
108
|
const SUBSCRIBER_CACHE_DEFAULT_MAX_ENTRIES = 4_096;
|
|
73
109
|
const DEFAULT_FANOUT_PUBLISH_IDLE_CLOSE_MS = 60_000;
|
|
@@ -75,7 +111,9 @@ const DEFAULT_FANOUT_PUBLISH_MAX_EPHEMERAL_CHANNELS = 64;
|
|
|
75
111
|
const DEFAULT_PUBSUB_SHARD_COUNT = 256;
|
|
76
112
|
const PUBSUB_SHARD_COUNT_HARD_CAP = 16_384;
|
|
77
113
|
const DEFAULT_PUBSUB_SHARD_TOPIC_PREFIX = "/peerbit/pubsub-shard/1/";
|
|
78
|
-
const
|
|
114
|
+
const AUTO_TOPIC_ROOT_CANDIDATE_UPDATE_COOLDOWN_MS = 2_000;
|
|
115
|
+
const sameCandidates = (left, right) => left.length === right.length &&
|
|
116
|
+
left.every((candidate, index) => candidate === right[index]);
|
|
79
117
|
// Topic-root queries may need to wait for the responder to finish opening an
|
|
80
118
|
// outbound stream back to the requester after an inbound-only dial.
|
|
81
119
|
const DEFAULT_TOPIC_ROOT_QUERY_TIMEOUT_MS = 12_000;
|
|
@@ -147,10 +185,15 @@ export class TopicControlPlane extends DirectStream {
|
|
|
147
185
|
// This keeps small ad-hoc networks working without explicit bootstraps.
|
|
148
186
|
autoTopicRootCandidates = false;
|
|
149
187
|
autoTopicRootCandidateSet;
|
|
188
|
+
pendingAutoTopicRootCandidates;
|
|
189
|
+
autoTopicRootCandidateUpdateTimer;
|
|
150
190
|
reconcileShardOverlaysInFlight;
|
|
151
191
|
reconcileShardOverlaysDirty = false;
|
|
152
192
|
topicControlPlaneStopping = false;
|
|
153
193
|
topicControlPlaneLifecycleRevision = 0;
|
|
194
|
+
topicRootResolutionAbortController = new AbortController();
|
|
195
|
+
topicRootCandidateResolutionGeneration;
|
|
196
|
+
topicRootCandidateResolutionAbortController = new AbortController();
|
|
154
197
|
hostOwnedShardRootsInFlight;
|
|
155
198
|
hostOwnedShardRootsDirty = false;
|
|
156
199
|
autoCandidatesBroadcastTimers = [];
|
|
@@ -249,6 +292,7 @@ export class TopicControlPlane extends DirectStream {
|
|
|
249
292
|
// Disable auto mode and stop its background gossip/timers.
|
|
250
293
|
this.autoTopicRootCandidates = false;
|
|
251
294
|
this.autoTopicRootCandidateSet = undefined;
|
|
295
|
+
this.clearAutoTopicRootCandidateUpdateSchedule();
|
|
252
296
|
for (const t of this.autoCandidatesBroadcastTimers)
|
|
253
297
|
clearTimeout(t);
|
|
254
298
|
this.autoCandidatesBroadcastTimers = [];
|
|
@@ -268,6 +312,9 @@ export class TopicControlPlane extends DirectStream {
|
|
|
268
312
|
}
|
|
269
313
|
async start() {
|
|
270
314
|
this.topicControlPlaneStopping = false;
|
|
315
|
+
if (this.topicRootResolutionAbortController.signal.aborted) {
|
|
316
|
+
this.topicRootResolutionAbortController = new AbortController();
|
|
317
|
+
}
|
|
271
318
|
await this.fanout.start();
|
|
272
319
|
this._onFanoutPeerUnreachable =
|
|
273
320
|
this._onFanoutPeerUnreachable ||
|
|
@@ -286,6 +333,8 @@ export class TopicControlPlane extends DirectStream {
|
|
|
286
333
|
}
|
|
287
334
|
async stop() {
|
|
288
335
|
this.topicControlPlaneStopping = true;
|
|
336
|
+
this.topicRootResolutionAbortController.abort(new AbortError("topic control plane stopped"));
|
|
337
|
+
this.clearAutoTopicRootCandidateUpdateSchedule();
|
|
289
338
|
this.topicControlPlaneLifecycleRevision += 1;
|
|
290
339
|
this.reconcileShardOverlaysDirty = false;
|
|
291
340
|
for (const opening of this.ensureFanoutChannelInFlight.values()) {
|
|
@@ -397,6 +446,7 @@ export class TopicControlPlane extends DirectStream {
|
|
|
397
446
|
// intact and reconcile shard overlays under the new mapping.
|
|
398
447
|
this.autoTopicRootCandidates = false;
|
|
399
448
|
this.autoTopicRootCandidateSet = undefined;
|
|
449
|
+
this.clearAutoTopicRootCandidateUpdateSchedule();
|
|
400
450
|
this.shardRootCache.clear();
|
|
401
451
|
// Ensure we host any shard roots we're now responsible for. This is important
|
|
402
452
|
// in tests where candidates may be configured before protocol streams have
|
|
@@ -409,43 +459,25 @@ export class TopicControlPlane extends DirectStream {
|
|
|
409
459
|
maybeUpdateAutoTopicRootCandidates(peerHash) {
|
|
410
460
|
if (!this.autoTopicRootCandidates)
|
|
411
461
|
return;
|
|
412
|
-
if (!peerHash ||
|
|
413
|
-
|
|
414
|
-
if (this.maybeDisableAutoTopicRootCandidatesIfExternallyConfigured())
|
|
462
|
+
if (!isCanonicalTopicRootCandidate(peerHash) ||
|
|
463
|
+
peerHash === this.publicKeyHash)
|
|
415
464
|
return;
|
|
416
|
-
|
|
417
|
-
const managed = this.autoTopicRootCandidateSet;
|
|
418
|
-
if (current.includes(peerHash))
|
|
419
|
-
return;
|
|
420
|
-
managed?.add(peerHash);
|
|
421
|
-
const next = this.normalizeAutoTopicRootCandidates(managed ? [...managed] : [...current, peerHash]);
|
|
422
|
-
this.autoTopicRootCandidateSet = new Set(next);
|
|
423
|
-
this.topicRootControlPlane.setTopicRootCandidates(next);
|
|
424
|
-
this.shardRootCache.clear();
|
|
425
|
-
this.scheduleReconcileShardOverlays();
|
|
426
|
-
// In auto-candidate mode, shard roots are selected deterministically across
|
|
427
|
-
// *all* connected peers (not just those currently subscribed to a shard).
|
|
428
|
-
// That means a peer can be selected as root for shards it isn't using yet.
|
|
429
|
-
// Ensure we proactively host the shard roots we're responsible for so other
|
|
430
|
-
// peers can join without timing out in small ad-hoc networks.
|
|
431
|
-
this.scheduleHostOwnedShardRoots();
|
|
432
|
-
// Share the updated candidate set so other peers converge on the same
|
|
433
|
-
// deterministic mapping even in partially connected topologies.
|
|
434
|
-
this.scheduleAutoTopicRootCandidatesBroadcast();
|
|
465
|
+
this.queueAutoTopicRootCandidateUpdate([peerHash]);
|
|
435
466
|
}
|
|
436
467
|
normalizeAutoTopicRootCandidates(candidates) {
|
|
468
|
+
const canonicalCandidates = candidates.filter(isCanonicalTopicRootCandidate);
|
|
437
469
|
if (this.nativeTopicControl) {
|
|
438
|
-
return this.nativeTopicControl.normalizeAutoCandidates(
|
|
470
|
+
return this.nativeTopicControl.normalizeAutoCandidates(canonicalCandidates, this.publicKeyHash);
|
|
439
471
|
}
|
|
440
472
|
const unique = new Set();
|
|
441
|
-
for (const c of
|
|
442
|
-
if (!c)
|
|
443
|
-
continue;
|
|
473
|
+
for (const c of canonicalCandidates) {
|
|
444
474
|
unique.add(c);
|
|
445
475
|
}
|
|
446
|
-
|
|
476
|
+
if (isCanonicalTopicRootCandidate(this.publicKeyHash)) {
|
|
477
|
+
unique.add(this.publicKeyHash);
|
|
478
|
+
}
|
|
447
479
|
const sorted = [...unique].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
448
|
-
return sorted.slice(0,
|
|
480
|
+
return sorted.slice(0, TOPIC_ROOT_CANDIDATES_MAX);
|
|
449
481
|
}
|
|
450
482
|
scheduleAutoTopicRootCandidatesBroadcast(targets) {
|
|
451
483
|
if (!this.autoTopicRootCandidates)
|
|
@@ -515,29 +547,78 @@ export class TopicControlPlane extends DirectStream {
|
|
|
515
547
|
mergeAutoTopicRootCandidatesFromPeer(candidates) {
|
|
516
548
|
if (!this.autoTopicRootCandidates)
|
|
517
549
|
return false;
|
|
550
|
+
return this.queueAutoTopicRootCandidateUpdate(candidates);
|
|
551
|
+
}
|
|
552
|
+
queueAutoTopicRootCandidateUpdate(candidates) {
|
|
553
|
+
if (!this.autoTopicRootCandidates ||
|
|
554
|
+
this.stopping ||
|
|
555
|
+
this.topicControlPlaneStopping)
|
|
556
|
+
return false;
|
|
518
557
|
if (this.maybeDisableAutoTopicRootCandidatesIfExternallyConfigured())
|
|
519
558
|
return false;
|
|
520
559
|
const managed = this.autoTopicRootCandidateSet;
|
|
521
560
|
if (!managed)
|
|
522
561
|
return false;
|
|
523
|
-
const before = this.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const next = this.normalizeAutoTopicRootCandidates([...managed]);
|
|
530
|
-
if (before.length === next.length &&
|
|
531
|
-
before.every((c, i) => c === next[i])) {
|
|
562
|
+
const before = this.pendingAutoTopicRootCandidates ?? [...managed];
|
|
563
|
+
const next = this.normalizeAutoTopicRootCandidates([
|
|
564
|
+
...before,
|
|
565
|
+
...candidates,
|
|
566
|
+
]);
|
|
567
|
+
if (sameCandidates(before, next))
|
|
532
568
|
return false;
|
|
569
|
+
if (this.autoTopicRootCandidateUpdateTimer) {
|
|
570
|
+
this.pendingAutoTopicRootCandidates = next;
|
|
571
|
+
return true;
|
|
533
572
|
}
|
|
573
|
+
this.scheduleAutoTopicRootCandidateUpdateCooldown();
|
|
574
|
+
this.applyAutoTopicRootCandidates(next);
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
applyAutoTopicRootCandidates(next) {
|
|
534
578
|
this.autoTopicRootCandidateSet = new Set(next);
|
|
535
579
|
this.topicRootControlPlane.setTopicRootCandidates(next);
|
|
536
580
|
this.shardRootCache.clear();
|
|
537
581
|
this.scheduleReconcileShardOverlays();
|
|
538
582
|
this.scheduleHostOwnedShardRoots();
|
|
539
583
|
this.scheduleAutoTopicRootCandidatesBroadcast();
|
|
540
|
-
|
|
584
|
+
}
|
|
585
|
+
scheduleAutoTopicRootCandidateUpdateCooldown() {
|
|
586
|
+
if (this.autoTopicRootCandidateUpdateTimer)
|
|
587
|
+
return;
|
|
588
|
+
const timer = setTimeout(() => {
|
|
589
|
+
if (this.autoTopicRootCandidateUpdateTimer !== timer)
|
|
590
|
+
return;
|
|
591
|
+
this.flushPendingAutoTopicRootCandidateUpdate();
|
|
592
|
+
}, AUTO_TOPIC_ROOT_CANDIDATE_UPDATE_COOLDOWN_MS);
|
|
593
|
+
timer.unref?.();
|
|
594
|
+
this.autoTopicRootCandidateUpdateTimer = timer;
|
|
595
|
+
}
|
|
596
|
+
flushPendingAutoTopicRootCandidateUpdate() {
|
|
597
|
+
if (this.autoTopicRootCandidateUpdateTimer) {
|
|
598
|
+
clearTimeout(this.autoTopicRootCandidateUpdateTimer);
|
|
599
|
+
this.autoTopicRootCandidateUpdateTimer = undefined;
|
|
600
|
+
}
|
|
601
|
+
const next = this.pendingAutoTopicRootCandidates;
|
|
602
|
+
this.pendingAutoTopicRootCandidates = undefined;
|
|
603
|
+
if (!next)
|
|
604
|
+
return;
|
|
605
|
+
if (!this.autoTopicRootCandidates ||
|
|
606
|
+
this.stopping ||
|
|
607
|
+
this.topicControlPlaneStopping)
|
|
608
|
+
return;
|
|
609
|
+
if (this.maybeDisableAutoTopicRootCandidatesIfExternallyConfigured())
|
|
610
|
+
return;
|
|
611
|
+
// The trailing update starts its own fixed window so another inbound update
|
|
612
|
+
// cannot create a second candidate generation immediately afterwards.
|
|
613
|
+
this.scheduleAutoTopicRootCandidateUpdateCooldown();
|
|
614
|
+
this.applyAutoTopicRootCandidates(next);
|
|
615
|
+
}
|
|
616
|
+
clearAutoTopicRootCandidateUpdateSchedule() {
|
|
617
|
+
if (this.autoTopicRootCandidateUpdateTimer) {
|
|
618
|
+
clearTimeout(this.autoTopicRootCandidateUpdateTimer);
|
|
619
|
+
this.autoTopicRootCandidateUpdateTimer = undefined;
|
|
620
|
+
}
|
|
621
|
+
this.pendingAutoTopicRootCandidates = undefined;
|
|
541
622
|
}
|
|
542
623
|
scheduleHostOwnedShardRoots() {
|
|
543
624
|
this.hostOwnedShardRootsDirty = true;
|
|
@@ -571,6 +652,7 @@ export class TopicControlPlane extends DirectStream {
|
|
|
571
652
|
}
|
|
572
653
|
scheduleReconcileShardOverlays() {
|
|
573
654
|
const candidateGeneration = this.getTopicRootCandidateGeneration();
|
|
655
|
+
this.syncTopicRootCandidateResolutionSignal(candidateGeneration);
|
|
574
656
|
for (const opening of this.ensureFanoutChannelInFlight.values()) {
|
|
575
657
|
if (opening.candidateGeneration !== candidateGeneration) {
|
|
576
658
|
opening.abortController.abort(new AbortError("topic root candidates changed"));
|
|
@@ -821,6 +903,9 @@ export class TopicControlPlane extends DirectStream {
|
|
|
821
903
|
* their fallback behavior).
|
|
822
904
|
*/
|
|
823
905
|
decodePubSubMessage(bytes) {
|
|
906
|
+
if (bytes[0] === 4) {
|
|
907
|
+
assertTopicRootCandidatesFrame(bytes);
|
|
908
|
+
}
|
|
824
909
|
const native = this.nativeTopicControl;
|
|
825
910
|
if (native) {
|
|
826
911
|
const decoded = native.decodePubSubMessage(bytes);
|
|
@@ -841,6 +926,7 @@ export class TopicControlPlane extends DirectStream {
|
|
|
841
926
|
case "get-subscribers":
|
|
842
927
|
return new GetSubscribers({ topics: decoded.topics });
|
|
843
928
|
case "topic-root-candidates":
|
|
929
|
+
assertCanonicalTopicRootCandidates(decoded.candidates);
|
|
844
930
|
return new TopicRootCandidates({ candidates: decoded.candidates });
|
|
845
931
|
case "peer-unavailable":
|
|
846
932
|
return new PeerUnavailable({
|
|
@@ -878,6 +964,53 @@ export class TopicControlPlane extends DirectStream {
|
|
|
878
964
|
throw new NotStartedError();
|
|
879
965
|
}
|
|
880
966
|
}
|
|
967
|
+
syncTopicRootCandidateResolutionSignal(candidateGeneration = this.getTopicRootCandidateGeneration()) {
|
|
968
|
+
if (this.topicRootCandidateResolutionGeneration === undefined) {
|
|
969
|
+
this.topicRootCandidateResolutionGeneration = candidateGeneration;
|
|
970
|
+
}
|
|
971
|
+
else if (candidateGeneration !== this.topicRootCandidateResolutionGeneration) {
|
|
972
|
+
this.topicRootCandidateResolutionAbortController.abort(new AbortError("topic root candidates changed"));
|
|
973
|
+
this.topicRootCandidateResolutionAbortController = new AbortController();
|
|
974
|
+
this.topicRootCandidateResolutionGeneration = candidateGeneration;
|
|
975
|
+
}
|
|
976
|
+
return this.topicRootCandidateResolutionAbortController.signal;
|
|
977
|
+
}
|
|
978
|
+
async withTopicRootCandidateResolution(operation, terminalSignals = []) {
|
|
979
|
+
for (;;) {
|
|
980
|
+
for (const signal of terminalSignals)
|
|
981
|
+
throwIfAborted(signal);
|
|
982
|
+
const candidateGeneration = this.getTopicRootCandidateGeneration();
|
|
983
|
+
const candidateSignal = this.syncTopicRootCandidateResolutionSignal(candidateGeneration);
|
|
984
|
+
const linked = linkAbortSignals([...terminalSignals, candidateSignal]);
|
|
985
|
+
try {
|
|
986
|
+
const result = await operation({
|
|
987
|
+
candidateGeneration,
|
|
988
|
+
signal: linked.signal,
|
|
989
|
+
});
|
|
990
|
+
for (const signal of terminalSignals)
|
|
991
|
+
throwIfAborted(signal);
|
|
992
|
+
if (candidateSignal.aborted ||
|
|
993
|
+
candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
return result;
|
|
997
|
+
}
|
|
998
|
+
catch (error) {
|
|
999
|
+
for (const signal of terminalSignals) {
|
|
1000
|
+
if (signal?.aborted)
|
|
1001
|
+
throw signal.reason ?? error;
|
|
1002
|
+
}
|
|
1003
|
+
if (candidateSignal.aborted ||
|
|
1004
|
+
candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
throw error;
|
|
1008
|
+
}
|
|
1009
|
+
finally {
|
|
1010
|
+
linked.clear();
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
881
1014
|
normalizePeerTopicRootState(topic, root) {
|
|
882
1015
|
const deterministic = this.topicRootControlPlane.resolveDeterministicTopicRoot(topic);
|
|
883
1016
|
if (this.autoTopicRootCandidates &&
|
|
@@ -888,12 +1021,13 @@ export class TopicControlPlane extends DirectStream {
|
|
|
888
1021
|
}
|
|
889
1022
|
return { root, authoritative: true };
|
|
890
1023
|
}
|
|
891
|
-
async resolveTopicRootState(topic) {
|
|
892
|
-
|
|
1024
|
+
async resolveTopicRootState(topic, options) {
|
|
1025
|
+
throwIfAborted(options?.signal);
|
|
1026
|
+
const tracked = await this.topicRootControlPlane.resolveTrackedTopicRoot(topic, options);
|
|
893
1027
|
if (tracked) {
|
|
894
1028
|
return { root: tracked, authoritative: true };
|
|
895
1029
|
}
|
|
896
|
-
const resolvedThroughPeers = await this.resolveTopicRootThroughPeers(topic);
|
|
1030
|
+
const resolvedThroughPeers = await this.resolveTopicRootThroughPeers(topic, options);
|
|
897
1031
|
if (resolvedThroughPeers) {
|
|
898
1032
|
// Unconfigured peer-query replies cannot override the locally
|
|
899
1033
|
// deterministic root for internal shards in auto mode. Roots, resolvers,
|
|
@@ -907,8 +1041,8 @@ export class TopicControlPlane extends DirectStream {
|
|
|
907
1041
|
this.autoTopicRootCandidates &&
|
|
908
1042
|
this.getConnectedTopicRootTrackers().length > 0) {
|
|
909
1043
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
910
|
-
await delay(150 * (attempt < 4 ? 1 : 2));
|
|
911
|
-
const retried = await this.resolveTopicRootThroughPeers(topic);
|
|
1044
|
+
await withAbort(delay(150 * (attempt < 4 ? 1 : 2), options), options?.signal);
|
|
1045
|
+
const retried = await this.resolveTopicRootThroughPeers(topic, options);
|
|
912
1046
|
if (retried) {
|
|
913
1047
|
return this.normalizePeerTopicRootState(topic, retried);
|
|
914
1048
|
}
|
|
@@ -919,12 +1053,16 @@ export class TopicControlPlane extends DirectStream {
|
|
|
919
1053
|
authoritative: false,
|
|
920
1054
|
};
|
|
921
1055
|
}
|
|
922
|
-
async resolveTopicRoot(topic) {
|
|
923
|
-
|
|
1056
|
+
async resolveTopicRoot(topic, options) {
|
|
1057
|
+
const lifecycleSignal = this.started && !this.stopping && !this.topicControlPlaneStopping
|
|
1058
|
+
? this.topicRootResolutionAbortController.signal
|
|
1059
|
+
: undefined;
|
|
1060
|
+
return this.withTopicRootCandidateResolution(async ({ signal }) => (await this.resolveTopicRootState(topic, { signal })).root, [lifecycleSignal, options?.signal]);
|
|
924
1061
|
}
|
|
925
|
-
async resolveShardRootState(shardTopic) {
|
|
1062
|
+
async resolveShardRootState(shardTopic, options) {
|
|
926
1063
|
const lifecycleRevision = this.topicControlPlaneLifecycleRevision;
|
|
927
1064
|
for (;;) {
|
|
1065
|
+
throwIfAborted(options?.signal);
|
|
928
1066
|
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
929
1067
|
// If someone configured topic-root candidates externally (e.g.
|
|
930
1068
|
// TestSession router selection or Peerbit.bootstrap) after this peer
|
|
@@ -938,7 +1076,7 @@ export class TopicControlPlane extends DirectStream {
|
|
|
938
1076
|
if (cached && (cached.authoritative || !hasConnectedTrackers)) {
|
|
939
1077
|
return { root: cached.root, candidateGeneration };
|
|
940
1078
|
}
|
|
941
|
-
const resolved = await this.resolveTopicRootState(shardTopic);
|
|
1079
|
+
const resolved = await this.resolveTopicRootState(shardTopic, options);
|
|
942
1080
|
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
943
1081
|
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
944
1082
|
continue;
|
|
@@ -998,36 +1136,57 @@ export class TopicControlPlane extends DirectStream {
|
|
|
998
1136
|
pending.resolve(message.root);
|
|
999
1137
|
return true;
|
|
1000
1138
|
}
|
|
1001
|
-
async queryTopicRootFromPeer(peer, topic, timeoutMs = DEFAULT_TOPIC_ROOT_QUERY_TIMEOUT_MS) {
|
|
1139
|
+
async queryTopicRootFromPeer(peer, topic, timeoutMs = DEFAULT_TOPIC_ROOT_QUERY_TIMEOUT_MS, signal) {
|
|
1140
|
+
throwIfAborted(signal);
|
|
1002
1141
|
if (!this.started || this.stopping)
|
|
1003
1142
|
return undefined;
|
|
1004
1143
|
const expectedPeerHash = peer.publicKey.hashcode();
|
|
1005
1144
|
const requestId = this.nextTopicRootRequestIdValue();
|
|
1006
1145
|
const responsePromise = new Promise((resolve) => {
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1146
|
+
let settled = false;
|
|
1147
|
+
const settle = (root) => {
|
|
1148
|
+
if (settled)
|
|
1149
|
+
return;
|
|
1150
|
+
settled = true;
|
|
1151
|
+
const pending = this.pendingTopicRootQueries.get(requestId);
|
|
1152
|
+
if (pending?.resolve === settle) {
|
|
1153
|
+
this.pendingTopicRootQueries.delete(requestId);
|
|
1154
|
+
}
|
|
1155
|
+
clearTimeout(timer);
|
|
1156
|
+
if (signal && onAbort) {
|
|
1157
|
+
signal.removeEventListener("abort", onAbort);
|
|
1158
|
+
}
|
|
1159
|
+
resolve(root);
|
|
1160
|
+
};
|
|
1161
|
+
const onAbort = signal ? () => settle(undefined) : undefined;
|
|
1162
|
+
const timer = setTimeout(() => settle(undefined), Math.max(1, Math.floor(timeoutMs)));
|
|
1011
1163
|
timer.unref?.();
|
|
1012
1164
|
this.pendingTopicRootQueries.set(requestId, {
|
|
1013
1165
|
expectedPeerHash,
|
|
1014
1166
|
topic,
|
|
1015
|
-
resolve,
|
|
1167
|
+
resolve: settle,
|
|
1016
1168
|
timer,
|
|
1017
1169
|
});
|
|
1170
|
+
if (signal && onAbort) {
|
|
1171
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1172
|
+
if (signal.aborted)
|
|
1173
|
+
onAbort();
|
|
1174
|
+
}
|
|
1018
1175
|
});
|
|
1019
1176
|
try {
|
|
1020
|
-
await this.sendDirectControlMessage(peer, new TopicRootQuery({ requestId, topic }));
|
|
1177
|
+
await withAbort(this.sendDirectControlMessage(peer, new TopicRootQuery({ requestId, topic })), signal);
|
|
1021
1178
|
}
|
|
1022
|
-
catch {
|
|
1179
|
+
catch (error) {
|
|
1023
1180
|
const pending = this.pendingTopicRootQueries.get(requestId);
|
|
1024
1181
|
if (pending) {
|
|
1025
1182
|
this.pendingTopicRootQueries.delete(requestId);
|
|
1026
1183
|
clearTimeout(pending.timer);
|
|
1027
1184
|
pending.resolve(undefined);
|
|
1028
1185
|
}
|
|
1186
|
+
if (signal?.aborted)
|
|
1187
|
+
throw error;
|
|
1029
1188
|
}
|
|
1030
|
-
return responsePromise;
|
|
1189
|
+
return withAbort(responsePromise, signal);
|
|
1031
1190
|
}
|
|
1032
1191
|
async confirmDirectShardRoot(shardTopic, root, candidateGeneration, lifecycleRevision, signal) {
|
|
1033
1192
|
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
@@ -1036,7 +1195,7 @@ export class TopicControlPlane extends DirectStream {
|
|
|
1036
1195
|
const rootPeer = this.peers.get(root);
|
|
1037
1196
|
if (!rootPeer)
|
|
1038
1197
|
return root;
|
|
1039
|
-
const confirmation = await withAbort(this.queryTopicRootFromPeer(rootPeer, shardTopic, DIRECT_SHARD_ROOT_CONFIRM_TIMEOUT_MS), signal);
|
|
1198
|
+
const confirmation = await withAbort(this.queryTopicRootFromPeer(rootPeer, shardTopic, DIRECT_SHARD_ROOT_CONFIRM_TIMEOUT_MS, signal), signal);
|
|
1040
1199
|
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1041
1200
|
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1042
1201
|
return root;
|
|
@@ -1057,14 +1216,14 @@ export class TopicControlPlane extends DirectStream {
|
|
|
1057
1216
|
}
|
|
1058
1217
|
async resolveQueryableTopicRoot(topic) {
|
|
1059
1218
|
const lifecycleRevision = this.topicControlPlaneLifecycleRevision;
|
|
1060
|
-
|
|
1219
|
+
const lifecycleSignal = this.topicRootResolutionAbortController.signal;
|
|
1220
|
+
return this.withTopicRootCandidateResolution(async ({ candidateGeneration, signal }) => {
|
|
1221
|
+
throwIfAborted(signal);
|
|
1061
1222
|
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1223
|
+
const root = await this.topicRootControlPlane.resolveCanonicalTopicRoot(topic, {
|
|
1224
|
+
signal,
|
|
1225
|
+
});
|
|
1064
1226
|
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1065
|
-
if (rootCandidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1066
|
-
continue;
|
|
1067
|
-
}
|
|
1068
1227
|
if (root !== this.publicKeyHash) {
|
|
1069
1228
|
return root;
|
|
1070
1229
|
}
|
|
@@ -1075,34 +1234,37 @@ export class TopicControlPlane extends DirectStream {
|
|
|
1075
1234
|
await this.ensureFanoutChannel(topic, {
|
|
1076
1235
|
pin: true,
|
|
1077
1236
|
root,
|
|
1078
|
-
rootCandidateGeneration,
|
|
1237
|
+
rootCandidateGeneration: candidateGeneration,
|
|
1238
|
+
signal,
|
|
1079
1239
|
});
|
|
1080
1240
|
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1081
|
-
if (rootCandidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1082
|
-
continue;
|
|
1083
|
-
}
|
|
1084
1241
|
return root;
|
|
1085
1242
|
}
|
|
1086
1243
|
catch (error) {
|
|
1244
|
+
if (signal.aborted ||
|
|
1245
|
+
candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1246
|
+
throw error;
|
|
1247
|
+
}
|
|
1087
1248
|
warn(`Failed to host shard root ${topic} before answering root query: ${error instanceof Error ? error.message : String(error)}`);
|
|
1088
1249
|
return undefined;
|
|
1089
1250
|
}
|
|
1090
|
-
}
|
|
1251
|
+
}, [lifecycleSignal]);
|
|
1091
1252
|
}
|
|
1092
|
-
async resolveTopicRootThroughPeers(topic) {
|
|
1253
|
+
async resolveTopicRootThroughPeers(topic, options) {
|
|
1254
|
+
throwIfAborted(options?.signal);
|
|
1093
1255
|
const peers = this.getConnectedTopicRootTrackers();
|
|
1094
1256
|
if (peers.length === 0) {
|
|
1095
1257
|
return undefined;
|
|
1096
1258
|
}
|
|
1097
1259
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1098
1260
|
for (const peer of peers) {
|
|
1099
|
-
const resolved = await this.queryTopicRootFromPeer(peer, topic);
|
|
1261
|
+
const resolved = await this.queryTopicRootFromPeer(peer, topic, DEFAULT_TOPIC_ROOT_QUERY_TIMEOUT_MS, options?.signal);
|
|
1100
1262
|
if (resolved) {
|
|
1101
1263
|
return resolved;
|
|
1102
1264
|
}
|
|
1103
1265
|
}
|
|
1104
1266
|
if (attempt < 2) {
|
|
1105
|
-
await delay(150 * (attempt + 1));
|
|
1267
|
+
await withAbort(delay(150 * (attempt + 1), options), options?.signal);
|
|
1106
1268
|
}
|
|
1107
1269
|
}
|
|
1108
1270
|
return undefined;
|
|
@@ -1226,7 +1388,9 @@ export class TopicControlPlane extends DirectStream {
|
|
|
1226
1388
|
const existing = this.fanoutChannels.get(t);
|
|
1227
1389
|
if (existing) {
|
|
1228
1390
|
if (!root) {
|
|
1229
|
-
const resolved = await this.resolveShardRootState(t
|
|
1391
|
+
const resolved = await this.resolveShardRootState(t, {
|
|
1392
|
+
signal: options?.signal,
|
|
1393
|
+
});
|
|
1230
1394
|
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1231
1395
|
root = resolved.root;
|
|
1232
1396
|
resolvedGeneration = resolved.candidateGeneration;
|
|
@@ -1263,7 +1427,9 @@ export class TopicControlPlane extends DirectStream {
|
|
|
1263
1427
|
}
|
|
1264
1428
|
}
|
|
1265
1429
|
if (!root) {
|
|
1266
|
-
const resolved = await this.resolveShardRootState(t
|
|
1430
|
+
const resolved = await this.resolveShardRootState(t, {
|
|
1431
|
+
signal: options?.signal,
|
|
1432
|
+
});
|
|
1267
1433
|
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1268
1434
|
root = resolved.root;
|
|
1269
1435
|
resolvedGeneration = resolved.candidateGeneration;
|
|
@@ -1481,22 +1647,37 @@ export class TopicControlPlane extends DirectStream {
|
|
|
1481
1647
|
}
|
|
1482
1648
|
}
|
|
1483
1649
|
}
|
|
1484
|
-
async hostShardRootsNow() {
|
|
1650
|
+
async hostShardRootsNow(options) {
|
|
1485
1651
|
if (!this.started)
|
|
1486
1652
|
throw new NotStartedError();
|
|
1487
|
-
const
|
|
1488
|
-
|
|
1489
|
-
const
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1653
|
+
const lifecycleSignal = this.topicRootResolutionAbortController.signal;
|
|
1654
|
+
return this.withTopicRootCandidateResolution(async ({ candidateGeneration, signal }) => {
|
|
1655
|
+
const joins = [];
|
|
1656
|
+
try {
|
|
1657
|
+
for (let i = 0; i < this.shardCount; i++) {
|
|
1658
|
+
throwIfAborted(signal);
|
|
1659
|
+
const shardTopic = `${this.shardTopicPrefix}${i}`;
|
|
1660
|
+
const resolved = await this.resolveShardRootState(shardTopic, {
|
|
1661
|
+
signal,
|
|
1662
|
+
});
|
|
1663
|
+
if (resolved.root !== this.publicKeyHash)
|
|
1664
|
+
continue;
|
|
1665
|
+
const joining = this.ensureFanoutChannel(shardTopic, {
|
|
1666
|
+
pin: true,
|
|
1667
|
+
root: resolved.root,
|
|
1668
|
+
rootCandidateGeneration: resolved.candidateGeneration,
|
|
1669
|
+
signal,
|
|
1670
|
+
});
|
|
1671
|
+
void joining.catch(() => { });
|
|
1672
|
+
joins.push(joining);
|
|
1673
|
+
}
|
|
1674
|
+
await Promise.all(joins);
|
|
1675
|
+
}
|
|
1676
|
+
catch (error) {
|
|
1677
|
+
await Promise.allSettled(joins);
|
|
1678
|
+
throw error;
|
|
1679
|
+
}
|
|
1680
|
+
}, [lifecycleSignal, options?.signal]);
|
|
1500
1681
|
}
|
|
1501
1682
|
async subscribe(topic) {
|
|
1502
1683
|
this.pendingSubscriptions.add(topic);
|