@peerbit/pubsub 5.3.5 → 5.3.7
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 +9 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +300 -81
- package/dist/src/index.js.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +385 -92
package/dist/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { PublicSignKey, getPublicKeyFromPeerId } from "@peerbit/crypto";
|
|
|
3
3
|
import { logger as loggerFn } from "@peerbit/logger";
|
|
4
4
|
import { DataEvent, GetSubscribers, PeerUnavailable, PubSubData, PubSubMessage, PublishEvent, Subscribe, SubscriptionData, SubscriptionEvent, TopicRootCandidates, TopicRootQuery, TopicRootQueryResponse, UnsubcriptionEvent, Unsubscribe, } from "@peerbit/pubsub-interface";
|
|
5
5
|
import { DirectStream, } from "@peerbit/stream";
|
|
6
|
-
import { AcknowledgeAnyWhere, AcknowledgeDelivery, AnyWhere, DataMessage, DeliveryError, MessageHeader, NotStartedError, SilentDelivery, deliveryModeHasReceiver, getMsgId, } from "@peerbit/stream-interface";
|
|
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";
|
|
8
8
|
import { Uint8ArrayList } from "uint8arraylist";
|
|
9
9
|
import { toUint8Array } from "./bytes.js";
|
|
@@ -148,6 +148,9 @@ export class TopicControlPlane extends DirectStream {
|
|
|
148
148
|
autoTopicRootCandidates = false;
|
|
149
149
|
autoTopicRootCandidateSet;
|
|
150
150
|
reconcileShardOverlaysInFlight;
|
|
151
|
+
reconcileShardOverlaysDirty = false;
|
|
152
|
+
topicControlPlaneStopping = false;
|
|
153
|
+
topicControlPlaneLifecycleRevision = 0;
|
|
151
154
|
hostOwnedShardRootsInFlight;
|
|
152
155
|
hostOwnedShardRootsDirty = false;
|
|
153
156
|
autoCandidatesBroadcastTimers = [];
|
|
@@ -155,6 +158,7 @@ export class TopicControlPlane extends DirectStream {
|
|
|
155
158
|
autoCandidatesGossipUntil = 0;
|
|
156
159
|
_onFanoutPeerUnreachable;
|
|
157
160
|
fanoutChannels = new Map();
|
|
161
|
+
ensureFanoutChannelInFlight = new Map();
|
|
158
162
|
pendingTopicRootQueries = new Map();
|
|
159
163
|
nextTopicRootQueryId = 1;
|
|
160
164
|
constructor(components, props) {
|
|
@@ -263,6 +267,7 @@ export class TopicControlPlane extends DirectStream {
|
|
|
263
267
|
this.scheduleReconcileShardOverlays();
|
|
264
268
|
}
|
|
265
269
|
async start() {
|
|
270
|
+
this.topicControlPlaneStopping = false;
|
|
266
271
|
await this.fanout.start();
|
|
267
272
|
this._onFanoutPeerUnreachable =
|
|
268
273
|
this._onFanoutPeerUnreachable ||
|
|
@@ -275,8 +280,15 @@ export class TopicControlPlane extends DirectStream {
|
|
|
275
280
|
if (this.hostOwnedShardRootsDirty) {
|
|
276
281
|
this.scheduleHostOwnedShardRoots();
|
|
277
282
|
}
|
|
283
|
+
if (this.reconcileShardOverlaysDirty) {
|
|
284
|
+
this.scheduleReconcileShardOverlays();
|
|
285
|
+
}
|
|
278
286
|
}
|
|
279
287
|
async stop() {
|
|
288
|
+
this.topicControlPlaneStopping = true;
|
|
289
|
+
this.topicControlPlaneLifecycleRevision += 1;
|
|
290
|
+
this.reconcileShardOverlaysDirty = false;
|
|
291
|
+
this.ensureFanoutChannelInFlight.clear();
|
|
280
292
|
if (this._onFanoutPeerUnreachable) {
|
|
281
293
|
this.fanout.removeEventListener("fanout:peer-unreachable", this._onFanoutPeerUnreachable);
|
|
282
294
|
}
|
|
@@ -338,7 +350,8 @@ export class TopicControlPlane extends DirectStream {
|
|
|
338
350
|
this.pendingTopicRootQueries.clear();
|
|
339
351
|
this.debounceSubscribeAggregator.close();
|
|
340
352
|
this.debounceUnsubscribeAggregator.close();
|
|
341
|
-
|
|
353
|
+
await super.stop();
|
|
354
|
+
this.reconcileShardOverlaysDirty = false;
|
|
342
355
|
}
|
|
343
356
|
async onPeerConnected(peerId, connection) {
|
|
344
357
|
await super.onPeerConnected(peerId, connection);
|
|
@@ -554,22 +567,45 @@ export class TopicControlPlane extends DirectStream {
|
|
|
554
567
|
});
|
|
555
568
|
}
|
|
556
569
|
scheduleReconcileShardOverlays() {
|
|
570
|
+
this.reconcileShardOverlaysDirty = true;
|
|
571
|
+
if (!this.started || this.stopping || this.topicControlPlaneStopping)
|
|
572
|
+
return;
|
|
557
573
|
if (this.reconcileShardOverlaysInFlight)
|
|
558
574
|
return;
|
|
559
|
-
this.reconcileShardOverlaysInFlight =
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
575
|
+
this.reconcileShardOverlaysInFlight = (async () => {
|
|
576
|
+
for (;;) {
|
|
577
|
+
if (!this.started || this.stopping || this.topicControlPlaneStopping)
|
|
578
|
+
return;
|
|
579
|
+
if (!this.reconcileShardOverlaysDirty)
|
|
580
|
+
return;
|
|
581
|
+
this.reconcileShardOverlaysDirty = false;
|
|
582
|
+
try {
|
|
583
|
+
await this.reconcileShardOverlays();
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
// Fanout streams or roots might not be ready yet. Preserve both
|
|
587
|
+
// failures and root changes that arrived during the active pass.
|
|
588
|
+
if (!this.started || this.stopping || this.topicControlPlaneStopping)
|
|
589
|
+
return;
|
|
590
|
+
this.reconcileShardOverlaysDirty = true;
|
|
591
|
+
await new Promise((resolve) => {
|
|
592
|
+
const timer = setTimeout(resolve, 250);
|
|
593
|
+
timer.unref?.();
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
})().finally(() => {
|
|
568
598
|
this.reconcileShardOverlaysInFlight = undefined;
|
|
599
|
+
if (this.reconcileShardOverlaysDirty &&
|
|
600
|
+
this.started &&
|
|
601
|
+
!this.stopping &&
|
|
602
|
+
!this.topicControlPlaneStopping) {
|
|
603
|
+
this.scheduleReconcileShardOverlays();
|
|
604
|
+
}
|
|
569
605
|
});
|
|
570
606
|
}
|
|
571
607
|
async reconcileShardOverlays() {
|
|
572
|
-
if (!this.started)
|
|
608
|
+
if (!this.started || this.topicControlPlaneStopping)
|
|
573
609
|
return;
|
|
574
610
|
const byShard = new Map();
|
|
575
611
|
for (const topic of this.subscriptions.keys()) {
|
|
@@ -819,6 +855,30 @@ export class TopicControlPlane extends DirectStream {
|
|
|
819
855
|
}
|
|
820
856
|
return PubSubMessage.from(bytes);
|
|
821
857
|
}
|
|
858
|
+
getTopicRootCandidateGeneration() {
|
|
859
|
+
return JSON.stringify([
|
|
860
|
+
this.autoTopicRootCandidates,
|
|
861
|
+
this.topicRootControlPlane.getTopicRootCandidates(),
|
|
862
|
+
]);
|
|
863
|
+
}
|
|
864
|
+
assertTopicControlPlaneActive(lifecycleRevision) {
|
|
865
|
+
if (lifecycleRevision !== this.topicControlPlaneLifecycleRevision ||
|
|
866
|
+
!this.started ||
|
|
867
|
+
this.stopping ||
|
|
868
|
+
this.topicControlPlaneStopping) {
|
|
869
|
+
throw new NotStartedError();
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
normalizePeerTopicRootState(topic, root) {
|
|
873
|
+
const deterministic = this.topicRootControlPlane.resolveDeterministicTopicRoot(topic);
|
|
874
|
+
if (this.autoTopicRootCandidates &&
|
|
875
|
+
topic.startsWith(this.shardTopicPrefix) &&
|
|
876
|
+
deterministic &&
|
|
877
|
+
root !== deterministic) {
|
|
878
|
+
return { root: deterministic, authoritative: false };
|
|
879
|
+
}
|
|
880
|
+
return { root, authoritative: true };
|
|
881
|
+
}
|
|
822
882
|
async resolveTopicRootState(topic) {
|
|
823
883
|
const tracked = await this.topicRootControlPlane.resolveTrackedTopicRoot(topic);
|
|
824
884
|
if (tracked) {
|
|
@@ -826,7 +886,12 @@ export class TopicControlPlane extends DirectStream {
|
|
|
826
886
|
}
|
|
827
887
|
const resolvedThroughPeers = await this.resolveTopicRootThroughPeers(topic);
|
|
828
888
|
if (resolvedThroughPeers) {
|
|
829
|
-
|
|
889
|
+
// Unconfigured peer-query replies cannot override the locally
|
|
890
|
+
// deterministic root for internal shards in auto mode. Roots, resolvers,
|
|
891
|
+
// and trackers explicitly configured on this control plane are resolved
|
|
892
|
+
// above and remain authoritative. A leaf relying on a queried gateway for
|
|
893
|
+
// shard roots must first disable auto mode with setTopicRootCandidates([]).
|
|
894
|
+
return this.normalizePeerTopicRootState(topic, resolvedThroughPeers);
|
|
830
895
|
}
|
|
831
896
|
const deterministic = this.topicRootControlPlane.resolveDeterministicTopicRoot(topic);
|
|
832
897
|
if (deterministic === this.publicKeyHash &&
|
|
@@ -836,38 +901,50 @@ export class TopicControlPlane extends DirectStream {
|
|
|
836
901
|
await delay(150 * (attempt < 4 ? 1 : 2));
|
|
837
902
|
const retried = await this.resolveTopicRootThroughPeers(topic);
|
|
838
903
|
if (retried) {
|
|
839
|
-
return
|
|
904
|
+
return this.normalizePeerTopicRootState(topic, retried);
|
|
840
905
|
}
|
|
841
906
|
}
|
|
842
907
|
}
|
|
843
|
-
return {
|
|
908
|
+
return {
|
|
909
|
+
root: this.topicRootControlPlane.resolveDeterministicTopicRoot(topic),
|
|
910
|
+
authoritative: false,
|
|
911
|
+
};
|
|
844
912
|
}
|
|
845
913
|
async resolveTopicRoot(topic) {
|
|
846
914
|
return (await this.resolveTopicRootState(topic)).root;
|
|
847
915
|
}
|
|
848
|
-
async
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
this.
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
916
|
+
async resolveShardRootState(shardTopic) {
|
|
917
|
+
const lifecycleRevision = this.topicControlPlaneLifecycleRevision;
|
|
918
|
+
for (;;) {
|
|
919
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
920
|
+
// If someone configured topic-root candidates externally (e.g.
|
|
921
|
+
// TestSession router selection or Peerbit.bootstrap) after this peer
|
|
922
|
+
// entered auto mode, disable auto mode before caching a root.
|
|
923
|
+
if (this.autoTopicRootCandidates) {
|
|
924
|
+
this.maybeDisableAutoTopicRootCandidatesIfExternallyConfigured();
|
|
925
|
+
}
|
|
926
|
+
const candidateGeneration = this.getTopicRootCandidateGeneration();
|
|
927
|
+
const hasConnectedTrackers = this.getConnectedTopicRootTrackers().length > 0;
|
|
928
|
+
const cached = this.shardRootCache.get(shardTopic);
|
|
929
|
+
if (cached && (cached.authoritative || !hasConnectedTrackers)) {
|
|
930
|
+
return { root: cached.root, candidateGeneration };
|
|
931
|
+
}
|
|
932
|
+
const resolved = await this.resolveTopicRootState(shardTopic);
|
|
933
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
934
|
+
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
if (!resolved.root) {
|
|
938
|
+
throw new Error(`No root resolved for shard topic ${shardTopic}. Configure TopicRootControlPlane candidates/resolver/trackers, or dial/bootstrap a peer that can resolve shard roots.`);
|
|
939
|
+
}
|
|
940
|
+
if (resolved.authoritative || !hasConnectedTrackers) {
|
|
941
|
+
this.shardRootCache.set(shardTopic, resolved);
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
this.shardRootCache.delete(shardTopic);
|
|
945
|
+
}
|
|
946
|
+
return { root: resolved.root, candidateGeneration };
|
|
869
947
|
}
|
|
870
|
-
return resolved.root;
|
|
871
948
|
}
|
|
872
949
|
getConnectedTopicRootTrackers() {
|
|
873
950
|
return [...this.peers.values()]
|
|
@@ -900,9 +977,11 @@ export class TopicControlPlane extends DirectStream {
|
|
|
900
977
|
});
|
|
901
978
|
await this.publishMessage(this.publicKey, embedded, [peer]);
|
|
902
979
|
}
|
|
903
|
-
resolvePendingTopicRootQuery(message) {
|
|
980
|
+
resolvePendingTopicRootQuery(message, responderPeerHash) {
|
|
904
981
|
const pending = this.pendingTopicRootQueries.get(message.requestId);
|
|
905
|
-
if (!pending ||
|
|
982
|
+
if (!pending ||
|
|
983
|
+
pending.topic !== message.topic ||
|
|
984
|
+
pending.expectedPeerHash !== responderPeerHash) {
|
|
906
985
|
return false;
|
|
907
986
|
}
|
|
908
987
|
this.pendingTopicRootQueries.delete(message.requestId);
|
|
@@ -913,6 +992,7 @@ export class TopicControlPlane extends DirectStream {
|
|
|
913
992
|
async queryTopicRootFromPeer(peer, topic, timeoutMs = DEFAULT_TOPIC_ROOT_QUERY_TIMEOUT_MS) {
|
|
914
993
|
if (!this.started || this.stopping)
|
|
915
994
|
return undefined;
|
|
995
|
+
const expectedPeerHash = peer.publicKey.hashcode();
|
|
916
996
|
const requestId = this.nextTopicRootRequestIdValue();
|
|
917
997
|
const responsePromise = new Promise((resolve) => {
|
|
918
998
|
const timer = setTimeout(() => {
|
|
@@ -920,7 +1000,12 @@ export class TopicControlPlane extends DirectStream {
|
|
|
920
1000
|
resolve(undefined);
|
|
921
1001
|
}, Math.max(1, Math.floor(timeoutMs)));
|
|
922
1002
|
timer.unref?.();
|
|
923
|
-
this.pendingTopicRootQueries.set(requestId, {
|
|
1003
|
+
this.pendingTopicRootQueries.set(requestId, {
|
|
1004
|
+
expectedPeerHash,
|
|
1005
|
+
topic,
|
|
1006
|
+
resolve,
|
|
1007
|
+
timer,
|
|
1008
|
+
});
|
|
924
1009
|
});
|
|
925
1010
|
try {
|
|
926
1011
|
await this.sendDirectControlMessage(peer, new TopicRootQuery({ requestId, topic }));
|
|
@@ -935,38 +1020,64 @@ export class TopicControlPlane extends DirectStream {
|
|
|
935
1020
|
}
|
|
936
1021
|
return responsePromise;
|
|
937
1022
|
}
|
|
938
|
-
async confirmDirectShardRoot(shardTopic, root, signal) {
|
|
1023
|
+
async confirmDirectShardRoot(shardTopic, root, candidateGeneration, lifecycleRevision, signal) {
|
|
1024
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
939
1025
|
if (root === this.publicKeyHash)
|
|
940
1026
|
return root;
|
|
941
1027
|
const rootPeer = this.peers.get(root);
|
|
942
1028
|
if (!rootPeer)
|
|
943
1029
|
return root;
|
|
944
1030
|
const confirmation = await withAbort(this.queryTopicRootFromPeer(rootPeer, shardTopic, DIRECT_SHARD_ROOT_CONFIRM_TIMEOUT_MS), signal);
|
|
1031
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1032
|
+
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1033
|
+
return root;
|
|
1034
|
+
}
|
|
945
1035
|
if (!confirmation)
|
|
946
1036
|
return root;
|
|
947
|
-
|
|
1037
|
+
const resolved = this.normalizePeerTopicRootState(shardTopic, confirmation);
|
|
1038
|
+
if (!resolved.authoritative) {
|
|
1039
|
+
this.shardRootCache.delete(shardTopic);
|
|
1040
|
+
}
|
|
1041
|
+
else if (resolved.root !== root) {
|
|
948
1042
|
this.shardRootCache.set(shardTopic, {
|
|
949
|
-
root:
|
|
1043
|
+
root: resolved.root,
|
|
950
1044
|
authoritative: true,
|
|
951
1045
|
});
|
|
952
1046
|
}
|
|
953
|
-
return
|
|
1047
|
+
return resolved.root;
|
|
954
1048
|
}
|
|
955
1049
|
async resolveQueryableTopicRoot(topic) {
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1050
|
+
const lifecycleRevision = this.topicControlPlaneLifecycleRevision;
|
|
1051
|
+
for (;;) {
|
|
1052
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1053
|
+
const rootCandidateGeneration = this.getTopicRootCandidateGeneration();
|
|
1054
|
+
const root = await this.topicRootControlPlane.resolveCanonicalTopicRoot(topic);
|
|
1055
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1056
|
+
if (rootCandidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
if (root !== this.publicKeyHash) {
|
|
1060
|
+
return root;
|
|
1061
|
+
}
|
|
1062
|
+
if (!topic.startsWith(this.shardTopicPrefix)) {
|
|
1063
|
+
return root;
|
|
1064
|
+
}
|
|
1065
|
+
try {
|
|
1066
|
+
await this.ensureFanoutChannel(topic, {
|
|
1067
|
+
pin: true,
|
|
1068
|
+
root,
|
|
1069
|
+
rootCandidateGeneration,
|
|
1070
|
+
});
|
|
1071
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1072
|
+
if (rootCandidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
return root;
|
|
1076
|
+
}
|
|
1077
|
+
catch (error) {
|
|
1078
|
+
warn(`Failed to host shard root ${topic} before answering root query: ${error instanceof Error ? error.message : String(error)}`);
|
|
1079
|
+
return undefined;
|
|
1080
|
+
}
|
|
970
1081
|
}
|
|
971
1082
|
}
|
|
972
1083
|
async resolveTopicRootThroughPeers(topic) {
|
|
@@ -988,18 +1099,86 @@ export class TopicControlPlane extends DirectStream {
|
|
|
988
1099
|
return undefined;
|
|
989
1100
|
}
|
|
990
1101
|
async ensureFanoutChannel(shardTopic, options) {
|
|
991
|
-
|
|
992
|
-
|
|
1102
|
+
const topic = shardTopic.toString();
|
|
1103
|
+
const lifecycleRevision = this.topicControlPlaneLifecycleRevision;
|
|
1104
|
+
for (;;) {
|
|
1105
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1106
|
+
const candidateGeneration = this.getTopicRootCandidateGeneration();
|
|
1107
|
+
const active = this.ensureFanoutChannelInFlight.get(topic);
|
|
1108
|
+
if (active?.candidateGeneration === candidateGeneration &&
|
|
1109
|
+
active.lifecycleRevision === lifecycleRevision) {
|
|
1110
|
+
try {
|
|
1111
|
+
await withAbort(active.opening, options?.signal);
|
|
1112
|
+
}
|
|
1113
|
+
catch (error) {
|
|
1114
|
+
if (options?.signal?.aborted)
|
|
1115
|
+
throw error;
|
|
1116
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1117
|
+
}
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
const opening = this.ensureFanoutChannelOnce(topic, options, lifecycleRevision, candidateGeneration);
|
|
1121
|
+
const inFlight = {
|
|
1122
|
+
candidateGeneration,
|
|
1123
|
+
lifecycleRevision,
|
|
1124
|
+
opening,
|
|
1125
|
+
};
|
|
1126
|
+
this.ensureFanoutChannelInFlight.set(topic, inFlight);
|
|
1127
|
+
try {
|
|
1128
|
+
await opening;
|
|
1129
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1130
|
+
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
catch (error) {
|
|
1136
|
+
if (options?.signal?.aborted)
|
|
1137
|
+
throw error;
|
|
1138
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1139
|
+
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
throw error;
|
|
1143
|
+
}
|
|
1144
|
+
finally {
|
|
1145
|
+
if (this.ensureFanoutChannelInFlight.get(topic) === inFlight) {
|
|
1146
|
+
this.ensureFanoutChannelInFlight.delete(topic);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
async ensureFanoutChannelOnce(shardTopic, options, lifecycleRevision, candidateGeneration) {
|
|
1152
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1153
|
+
if (candidateGeneration !== this.getTopicRootCandidateGeneration())
|
|
1154
|
+
return;
|
|
993
1155
|
const t = shardTopic.toString();
|
|
994
1156
|
const pin = options?.pin === true;
|
|
995
1157
|
const wantEphemeral = options?.ephemeral === true;
|
|
996
1158
|
// Allow callers that already resolved the shard root (e.g. hostShardRootsNow)
|
|
997
1159
|
// to pass it through to avoid a race where the candidate set changes between
|
|
998
1160
|
// two resolve calls, causing an unnecessary (and potentially slow) join.
|
|
999
|
-
|
|
1161
|
+
const suppliedRoot = options?.root;
|
|
1162
|
+
const suppliedRootGeneration = options?.rootCandidateGeneration;
|
|
1163
|
+
if (suppliedRoot !== undefined &&
|
|
1164
|
+
suppliedRootGeneration !== candidateGeneration) {
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1168
|
+
let resolvedGeneration = candidateGeneration;
|
|
1169
|
+
let root = suppliedRoot;
|
|
1000
1170
|
const existing = this.fanoutChannels.get(t);
|
|
1001
1171
|
if (existing) {
|
|
1002
|
-
|
|
1172
|
+
if (!root) {
|
|
1173
|
+
const resolved = await this.resolveShardRootState(t);
|
|
1174
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1175
|
+
root = resolved.root;
|
|
1176
|
+
resolvedGeneration = resolved.candidateGeneration;
|
|
1177
|
+
}
|
|
1178
|
+
if (resolvedGeneration !== candidateGeneration ||
|
|
1179
|
+
candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1003
1182
|
if (root === existing.root) {
|
|
1004
1183
|
existing.lastUsedAt = Date.now();
|
|
1005
1184
|
if (existing.ephemeral && !wantEphemeral) {
|
|
@@ -1012,13 +1191,33 @@ export class TopicControlPlane extends DirectStream {
|
|
|
1012
1191
|
if (pin)
|
|
1013
1192
|
this.pinnedShards.add(t);
|
|
1014
1193
|
await withAbort(existing.join, options?.signal);
|
|
1194
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1195
|
+
if (resolvedGeneration !== candidateGeneration ||
|
|
1196
|
+
candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1015
1199
|
return;
|
|
1016
1200
|
}
|
|
1017
|
-
// Root mapping changed (candidate set updated): migrate to the new
|
|
1201
|
+
// Root mapping changed (candidate set updated): migrate to the new
|
|
1202
|
+
// overlay.
|
|
1018
1203
|
await withAbort(this.closeFanoutChannel(t, { force: true }), options?.signal);
|
|
1204
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1205
|
+
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
if (!root) {
|
|
1210
|
+
const resolved = await this.resolveShardRootState(t);
|
|
1211
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1212
|
+
root = resolved.root;
|
|
1213
|
+
resolvedGeneration = resolved.candidateGeneration;
|
|
1214
|
+
}
|
|
1215
|
+
root = await this.confirmDirectShardRoot(t, root, resolvedGeneration, lifecycleRevision, options?.signal);
|
|
1216
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1217
|
+
if (resolvedGeneration !== candidateGeneration ||
|
|
1218
|
+
candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1219
|
+
return;
|
|
1019
1220
|
}
|
|
1020
|
-
root = root ?? (await this.resolveShardRoot(t));
|
|
1021
|
-
root = await this.confirmDirectShardRoot(t, root, options?.signal);
|
|
1022
1221
|
const channel = new FanoutChannel(this.fanout, { topic: t, root });
|
|
1023
1222
|
const onPayload = (payload) => {
|
|
1024
1223
|
let dm;
|
|
@@ -1160,7 +1359,7 @@ export class TopicControlPlane extends DirectStream {
|
|
|
1160
1359
|
const lastUsedAt = Date.now();
|
|
1161
1360
|
if (pin)
|
|
1162
1361
|
this.pinnedShards.add(t);
|
|
1163
|
-
|
|
1362
|
+
const channelState = {
|
|
1164
1363
|
root,
|
|
1165
1364
|
channel,
|
|
1166
1365
|
join,
|
|
@@ -1168,15 +1367,19 @@ export class TopicControlPlane extends DirectStream {
|
|
|
1168
1367
|
onUnicast,
|
|
1169
1368
|
ephemeral: wantEphemeral,
|
|
1170
1369
|
lastUsedAt,
|
|
1171
|
-
}
|
|
1370
|
+
};
|
|
1371
|
+
this.fanoutChannels.set(t, channelState);
|
|
1172
1372
|
join.catch(() => {
|
|
1173
|
-
this.fanoutChannels.
|
|
1373
|
+
if (this.fanoutChannels.get(t) === channelState) {
|
|
1374
|
+
this.fanoutChannels.delete(t);
|
|
1375
|
+
}
|
|
1174
1376
|
});
|
|
1175
1377
|
join
|
|
1176
1378
|
.then(() => {
|
|
1177
1379
|
const st = this.fanoutChannels.get(t);
|
|
1178
|
-
if (st
|
|
1380
|
+
if (st === channelState && st.ephemeral) {
|
|
1179
1381
|
this.scheduleFanoutIdleClose(t);
|
|
1382
|
+
}
|
|
1180
1383
|
})
|
|
1181
1384
|
.catch(() => {
|
|
1182
1385
|
// ignore
|
|
@@ -1185,6 +1388,7 @@ export class TopicControlPlane extends DirectStream {
|
|
|
1185
1388
|
this.evictEphemeralFanoutChannels(t);
|
|
1186
1389
|
}
|
|
1187
1390
|
await withAbort(join, options?.signal);
|
|
1391
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1188
1392
|
}
|
|
1189
1393
|
async closeFanoutChannel(shardTopic, options) {
|
|
1190
1394
|
const t = shardTopic.toString();
|
|
@@ -1227,10 +1431,14 @@ export class TopicControlPlane extends DirectStream {
|
|
|
1227
1431
|
const joins = [];
|
|
1228
1432
|
for (let i = 0; i < this.shardCount; i++) {
|
|
1229
1433
|
const shardTopic = `${this.shardTopicPrefix}${i}`;
|
|
1230
|
-
const
|
|
1231
|
-
if (root !== this.publicKeyHash)
|
|
1434
|
+
const resolved = await this.resolveShardRootState(shardTopic);
|
|
1435
|
+
if (resolved.root !== this.publicKeyHash)
|
|
1232
1436
|
continue;
|
|
1233
|
-
joins.push(this.ensureFanoutChannel(shardTopic, {
|
|
1437
|
+
joins.push(this.ensureFanoutChannel(shardTopic, {
|
|
1438
|
+
pin: true,
|
|
1439
|
+
root: resolved.root,
|
|
1440
|
+
rootCandidateGeneration: resolved.candidateGeneration,
|
|
1441
|
+
}));
|
|
1234
1442
|
}
|
|
1235
1443
|
await Promise.all(joins);
|
|
1236
1444
|
}
|
|
@@ -1563,7 +1771,7 @@ export class TopicControlPlane extends DirectStream {
|
|
|
1563
1771
|
}),
|
|
1564
1772
|
}));
|
|
1565
1773
|
}
|
|
1566
|
-
const silentDelivery = options?.mode
|
|
1774
|
+
const silentDelivery = options?.mode != null && isSilentDeliveryMode(options.mode);
|
|
1567
1775
|
try {
|
|
1568
1776
|
await this.publishMessage(this.publicKey, message, undefined, undefined, options?.signal);
|
|
1569
1777
|
}
|
|
@@ -1665,7 +1873,7 @@ export class TopicControlPlane extends DirectStream {
|
|
|
1665
1873
|
message,
|
|
1666
1874
|
}),
|
|
1667
1875
|
}));
|
|
1668
|
-
const silentDelivery = options.mode
|
|
1876
|
+
const silentDelivery = isSilentDeliveryMode(options.mode);
|
|
1669
1877
|
try {
|
|
1670
1878
|
await this.publishMessage(this.publicKey, message, undefined, undefined, options?.signal);
|
|
1671
1879
|
}
|
|
@@ -1932,6 +2140,16 @@ export class TopicControlPlane extends DirectStream {
|
|
|
1932
2140
|
}
|
|
1933
2141
|
async processDirectPubSubMessage(input) {
|
|
1934
2142
|
const { pubsubMessage, message, from, stream } = input;
|
|
2143
|
+
const isDirectRootControl = pubsubMessage instanceof TopicRootCandidates ||
|
|
2144
|
+
pubsubMessage instanceof TopicRootQuery ||
|
|
2145
|
+
pubsubMessage instanceof TopicRootQueryResponse;
|
|
2146
|
+
const directRootSigner = isDirectRootControl
|
|
2147
|
+
? message.header.signatures?.publicKeys[0]
|
|
2148
|
+
: undefined;
|
|
2149
|
+
if (isDirectRootControl &&
|
|
2150
|
+
(!directRootSigner || !directRootSigner.equals(stream.publicKey))) {
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
1935
2153
|
if (pubsubMessage instanceof TopicRootCandidates) {
|
|
1936
2154
|
// Used only to converge deterministic shard-root candidates in auto mode.
|
|
1937
2155
|
this.mergeAutoTopicRootCandidatesFromPeer(pubsubMessage.candidates);
|
|
@@ -1947,7 +2165,8 @@ export class TopicControlPlane extends DirectStream {
|
|
|
1947
2165
|
return;
|
|
1948
2166
|
}
|
|
1949
2167
|
if (pubsubMessage instanceof TopicRootQueryResponse) {
|
|
1950
|
-
|
|
2168
|
+
const senderHash = directRootSigner.hashcode();
|
|
2169
|
+
this.resolvePendingTopicRootQuery(pubsubMessage, senderHash);
|
|
1951
2170
|
return;
|
|
1952
2171
|
}
|
|
1953
2172
|
if (pubsubMessage instanceof Subscribe) {
|
|
@@ -2197,8 +2416,8 @@ export class TopicControlPlane extends DirectStream {
|
|
|
2197
2416
|
if (deliveryModeHasReceiver(message.header.mode)) {
|
|
2198
2417
|
isForMe = message.header.mode.to.includes(this.publicKeyHash);
|
|
2199
2418
|
}
|
|
2200
|
-
else if (message.header.mode
|
|
2201
|
-
message.header.mode
|
|
2419
|
+
else if (isAnyWhereDeliveryMode(message.header.mode) ||
|
|
2420
|
+
isAcknowledgeAnyWhereDeliveryMode(message.header.mode)) {
|
|
2202
2421
|
isForMe = true;
|
|
2203
2422
|
}
|
|
2204
2423
|
if (pubsubMessage instanceof PubSubData) {
|
|
@@ -2222,16 +2441,16 @@ export class TopicControlPlane extends DirectStream {
|
|
|
2222
2441
|
if (!(pubsubMessage instanceof PubSubData)) {
|
|
2223
2442
|
return true;
|
|
2224
2443
|
}
|
|
2225
|
-
if (message.header.mode
|
|
2226
|
-
message.header.mode
|
|
2444
|
+
if (isSilentDeliveryMode(message.header.mode) ||
|
|
2445
|
+
isAcknowledgeDeliveryMode(message.header.mode)) {
|
|
2227
2446
|
if (message.header.mode.to.length === 1 &&
|
|
2228
2447
|
message.header.mode.to[0] === this.publicKeyHash) {
|
|
2229
2448
|
return true;
|
|
2230
2449
|
}
|
|
2231
2450
|
}
|
|
2232
2451
|
const shouldForward = seenBefore === 0 ||
|
|
2233
|
-
((message.header.mode
|
|
2234
|
-
message.header.mode
|
|
2452
|
+
((isAcknowledgeDeliveryMode(message.header.mode) ||
|
|
2453
|
+
isAcknowledgeAnyWhereDeliveryMode(message.header.mode)) &&
|
|
2235
2454
|
seenBefore < message.header.mode.redundancy);
|
|
2236
2455
|
if (shouldForward) {
|
|
2237
2456
|
this.relayMessage(from, message).catch(logErrorIfStarted);
|