@peerbit/pubsub 5.0.8 → 5.1.1
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-channel.d.ts +1 -0
- package/dist/src/fanout-channel.d.ts.map +1 -1
- package/dist/src/fanout-channel.js +3 -0
- package/dist/src/fanout-channel.js.map +1 -1
- package/dist/src/fanout-tree.d.ts +2 -0
- package/dist/src/fanout-tree.d.ts.map +1 -1
- package/dist/src/fanout-tree.js +24 -3
- package/dist/src/fanout-tree.js.map +1 -1
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +232 -25
- package/dist/src/index.js.map +1 -1
- package/dist/src/topic-root-control-plane.d.ts +4 -0
- package/dist/src/topic-root-control-plane.d.ts.map +1 -1
- package/dist/src/topic-root-control-plane.js +17 -3
- package/dist/src/topic-root-control-plane.js.map +1 -1
- package/package.json +4 -4
- package/src/fanout-channel.ts +4 -0
- package/src/fanout-tree.ts +30 -4
- package/src/index.ts +307 -26
- package/src/topic-root-control-plane.ts +24 -3
package/src/index.ts
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
SubscriptionData,
|
|
18
18
|
SubscriptionEvent,
|
|
19
19
|
TopicRootCandidates,
|
|
20
|
+
TopicRootQuery,
|
|
21
|
+
TopicRootQueryResponse,
|
|
20
22
|
UnsubcriptionEvent,
|
|
21
23
|
type UnsubscriptionReason,
|
|
22
24
|
Unsubscribe,
|
|
@@ -26,7 +28,6 @@ import {
|
|
|
26
28
|
type DirectStreamComponents,
|
|
27
29
|
type DirectStreamOptions,
|
|
28
30
|
type PeerStreams,
|
|
29
|
-
dontThrowIfDeliveryError,
|
|
30
31
|
} from "@peerbit/stream";
|
|
31
32
|
import {
|
|
32
33
|
AcknowledgeAnyWhere,
|
|
@@ -116,6 +117,9 @@ const DEFAULT_PUBSUB_SHARD_COUNT = 256;
|
|
|
116
117
|
const PUBSUB_SHARD_COUNT_HARD_CAP = 16_384;
|
|
117
118
|
const DEFAULT_PUBSUB_SHARD_TOPIC_PREFIX = "/peerbit/pubsub-shard/1/";
|
|
118
119
|
const AUTO_TOPIC_ROOT_CANDIDATES_MAX = 64;
|
|
120
|
+
// Topic-root queries may need to wait for the responder to finish opening an
|
|
121
|
+
// outbound stream back to the requester after an inbound-only dial.
|
|
122
|
+
const DEFAULT_TOPIC_ROOT_QUERY_TIMEOUT_MS = 12_000;
|
|
119
123
|
|
|
120
124
|
const DEFAULT_PUBSUB_FANOUT_CHANNEL_OPTIONS: Omit<
|
|
121
125
|
FanoutTreeChannelOptions,
|
|
@@ -250,7 +254,10 @@ export class TopicControlPlane
|
|
|
250
254
|
private readonly shardCount: number;
|
|
251
255
|
private readonly shardTopicPrefix: string;
|
|
252
256
|
private readonly hostShards: boolean;
|
|
253
|
-
private readonly shardRootCache = new Map<
|
|
257
|
+
private readonly shardRootCache = new Map<
|
|
258
|
+
string,
|
|
259
|
+
{ root: string; authoritative: boolean }
|
|
260
|
+
>();
|
|
254
261
|
private readonly shardTopicCache = new Map<string, string>();
|
|
255
262
|
private readonly shardRefCounts = new Map<string, number>();
|
|
256
263
|
private readonly pinnedShards = new Set<string>();
|
|
@@ -297,6 +304,15 @@ export class TopicControlPlane
|
|
|
297
304
|
idleCloseTimeout?: ReturnType<typeof setTimeout>;
|
|
298
305
|
}
|
|
299
306
|
>();
|
|
307
|
+
private pendingTopicRootQueries = new Map<
|
|
308
|
+
number,
|
|
309
|
+
{
|
|
310
|
+
topic: string;
|
|
311
|
+
resolve: (root: string | undefined) => void;
|
|
312
|
+
timer: ReturnType<typeof setTimeout>;
|
|
313
|
+
}
|
|
314
|
+
>();
|
|
315
|
+
private nextTopicRootQueryId = 1;
|
|
300
316
|
|
|
301
317
|
constructor(
|
|
302
318
|
components: TopicControlPlaneComponents,
|
|
@@ -489,6 +505,11 @@ export class TopicControlPlane
|
|
|
489
505
|
this.shardTopicCache.clear();
|
|
490
506
|
this.shardRefCounts.clear();
|
|
491
507
|
this.pinnedShards.clear();
|
|
508
|
+
for (const pending of this.pendingTopicRootQueries.values()) {
|
|
509
|
+
clearTimeout(pending.timer);
|
|
510
|
+
pending.resolve(undefined);
|
|
511
|
+
}
|
|
512
|
+
this.pendingTopicRootQueries.clear();
|
|
492
513
|
|
|
493
514
|
this.debounceSubscribeAggregator.close();
|
|
494
515
|
this.debounceUnsubscribeAggregator.close();
|
|
@@ -665,9 +686,7 @@ export class TopicControlPlane
|
|
|
665
686
|
priority: 1,
|
|
666
687
|
skipRecipientValidation: true,
|
|
667
688
|
} as any);
|
|
668
|
-
await this.
|
|
669
|
-
dontThrowIfDeliveryError,
|
|
670
|
-
);
|
|
689
|
+
await this.publishMessageMaybe(this.publicKey, embedded, streams);
|
|
671
690
|
}
|
|
672
691
|
|
|
673
692
|
private mergeAutoTopicRootCandidatesFromPeer(candidates: string[]): boolean {
|
|
@@ -913,6 +932,41 @@ export class TopicControlPlane
|
|
|
913
932
|
return shardTopic;
|
|
914
933
|
}
|
|
915
934
|
|
|
935
|
+
private async resolveTopicRootState(
|
|
936
|
+
topic: string,
|
|
937
|
+
): Promise<{ root?: string; authoritative: boolean }> {
|
|
938
|
+
const tracked = await this.topicRootControlPlane.resolveTrackedTopicRoot(topic);
|
|
939
|
+
if (tracked) {
|
|
940
|
+
return { root: tracked, authoritative: true };
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const resolvedThroughPeers = await this.resolveTopicRootThroughPeers(topic);
|
|
944
|
+
if (resolvedThroughPeers) {
|
|
945
|
+
return { root: resolvedThroughPeers, authoritative: true };
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const deterministic = this.topicRootControlPlane.resolveDeterministicTopicRoot(topic);
|
|
949
|
+
if (
|
|
950
|
+
deterministic === this.publicKeyHash &&
|
|
951
|
+
this.autoTopicRootCandidates &&
|
|
952
|
+
this.getConnectedTopicRootTrackers().length > 0
|
|
953
|
+
) {
|
|
954
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
955
|
+
await delay(150 * (attempt < 4 ? 1 : 2));
|
|
956
|
+
const retried = await this.resolveTopicRootThroughPeers(topic);
|
|
957
|
+
if (retried) {
|
|
958
|
+
return { root: retried, authoritative: true };
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
return { root: deterministic, authoritative: false };
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
public async resolveTopicRoot(topic: string): Promise<string | undefined> {
|
|
967
|
+
return (await this.resolveTopicRootState(topic)).root;
|
|
968
|
+
}
|
|
969
|
+
|
|
916
970
|
private async resolveShardRoot(shardTopic: string): Promise<string> {
|
|
917
971
|
// If someone configured topic-root candidates externally (e.g. TestSession router
|
|
918
972
|
// selection or Peerbit.bootstrap) after this peer entered auto mode, disable auto
|
|
@@ -921,17 +975,134 @@ export class TopicControlPlane
|
|
|
921
975
|
this.maybeDisableAutoTopicRootCandidatesIfExternallyConfigured();
|
|
922
976
|
}
|
|
923
977
|
|
|
978
|
+
const hasConnectedTrackers = this.getConnectedTopicRootTrackers().length > 0;
|
|
924
979
|
const cached = this.shardRootCache.get(shardTopic);
|
|
925
|
-
if (cached
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
980
|
+
if (cached && (cached.authoritative || !hasConnectedTrackers)) {
|
|
981
|
+
return cached.root;
|
|
982
|
+
}
|
|
983
|
+
const resolved = await this.resolveTopicRootState(shardTopic);
|
|
984
|
+
if (!resolved.root) {
|
|
929
985
|
throw new Error(
|
|
930
|
-
`No root resolved for shard topic ${shardTopic}. Configure TopicRootControlPlane candidates/resolver/trackers.`,
|
|
986
|
+
`No root resolved for shard topic ${shardTopic}. Configure TopicRootControlPlane candidates/resolver/trackers, or dial/bootstrap a peer that can resolve shard roots.`,
|
|
931
987
|
);
|
|
932
988
|
}
|
|
933
|
-
|
|
934
|
-
|
|
989
|
+
if (resolved.authoritative || !hasConnectedTrackers) {
|
|
990
|
+
this.shardRootCache.set(shardTopic, resolved as {
|
|
991
|
+
root: string;
|
|
992
|
+
authoritative: boolean;
|
|
993
|
+
});
|
|
994
|
+
} else {
|
|
995
|
+
this.shardRootCache.delete(shardTopic);
|
|
996
|
+
}
|
|
997
|
+
return resolved.root;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
private getConnectedTopicRootTrackers(): PeerStreams[] {
|
|
1001
|
+
return [...this.peers.values()]
|
|
1002
|
+
.filter((peer) => peer.isReadable || peer.isWritable)
|
|
1003
|
+
.sort((a, b) =>
|
|
1004
|
+
a.publicKey.hashcode() < b.publicKey.hashcode()
|
|
1005
|
+
? -1
|
|
1006
|
+
: a.publicKey.hashcode() > b.publicKey.hashcode()
|
|
1007
|
+
? 1
|
|
1008
|
+
: 0,
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
private nextTopicRootRequestIdValue() {
|
|
1013
|
+
let requestId = this.nextTopicRootQueryId >>> 0;
|
|
1014
|
+
do {
|
|
1015
|
+
requestId = requestId === 0 ? 1 : requestId;
|
|
1016
|
+
this.nextTopicRootQueryId = ((requestId + 1) >>> 0) || 1;
|
|
1017
|
+
if (!this.pendingTopicRootQueries.has(requestId)) {
|
|
1018
|
+
return requestId;
|
|
1019
|
+
}
|
|
1020
|
+
requestId = this.nextTopicRootQueryId >>> 0;
|
|
1021
|
+
} while (true);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
private async sendDirectControlMessage(
|
|
1025
|
+
peer: PeerStreams,
|
|
1026
|
+
pubsubMessage: PubSubMessage,
|
|
1027
|
+
) {
|
|
1028
|
+
const embedded = await this.createMessage(toUint8Array(pubsubMessage.bytes()), {
|
|
1029
|
+
mode: new SilentDelivery({
|
|
1030
|
+
to: [peer.publicKey.hashcode()],
|
|
1031
|
+
redundancy: 1,
|
|
1032
|
+
}),
|
|
1033
|
+
priority: 1,
|
|
1034
|
+
skipRecipientValidation: true,
|
|
1035
|
+
} as any);
|
|
1036
|
+
await this.publishMessage(this.publicKey, embedded, [peer]);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
private resolvePendingTopicRootQuery(
|
|
1040
|
+
message: TopicRootQueryResponse,
|
|
1041
|
+
): boolean {
|
|
1042
|
+
const pending = this.pendingTopicRootQueries.get(message.requestId);
|
|
1043
|
+
if (!pending || pending.topic !== message.topic) {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
this.pendingTopicRootQueries.delete(message.requestId);
|
|
1047
|
+
clearTimeout(pending.timer);
|
|
1048
|
+
pending.resolve(message.root);
|
|
1049
|
+
return true;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
private async queryTopicRootFromPeer(
|
|
1053
|
+
peer: PeerStreams,
|
|
1054
|
+
topic: string,
|
|
1055
|
+
): Promise<string | undefined> {
|
|
1056
|
+
if (!this.started || this.stopping) return undefined;
|
|
1057
|
+
|
|
1058
|
+
const requestId = this.nextTopicRootRequestIdValue();
|
|
1059
|
+
const responsePromise = new Promise<string | undefined>((resolve) => {
|
|
1060
|
+
const timer = setTimeout(() => {
|
|
1061
|
+
this.pendingTopicRootQueries.delete(requestId);
|
|
1062
|
+
resolve(undefined);
|
|
1063
|
+
}, DEFAULT_TOPIC_ROOT_QUERY_TIMEOUT_MS);
|
|
1064
|
+
timer.unref?.();
|
|
1065
|
+
this.pendingTopicRootQueries.set(requestId, { topic, resolve, timer });
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
try {
|
|
1069
|
+
await this.sendDirectControlMessage(
|
|
1070
|
+
peer,
|
|
1071
|
+
new TopicRootQuery({ requestId, topic }),
|
|
1072
|
+
);
|
|
1073
|
+
} catch {
|
|
1074
|
+
const pending = this.pendingTopicRootQueries.get(requestId);
|
|
1075
|
+
if (pending) {
|
|
1076
|
+
this.pendingTopicRootQueries.delete(requestId);
|
|
1077
|
+
clearTimeout(pending.timer);
|
|
1078
|
+
pending.resolve(undefined);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
return responsePromise;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
private async resolveTopicRootThroughPeers(
|
|
1086
|
+
topic: string,
|
|
1087
|
+
): Promise<string | undefined> {
|
|
1088
|
+
const peers = this.getConnectedTopicRootTrackers();
|
|
1089
|
+
if (peers.length === 0) {
|
|
1090
|
+
return undefined;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1094
|
+
for (const peer of peers) {
|
|
1095
|
+
const resolved = await this.queryTopicRootFromPeer(peer, topic);
|
|
1096
|
+
if (resolved) {
|
|
1097
|
+
return resolved;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (attempt < 2) {
|
|
1102
|
+
await delay(150 * (attempt + 1));
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return undefined;
|
|
935
1106
|
}
|
|
936
1107
|
|
|
937
1108
|
private async ensureFanoutChannel(
|
|
@@ -1385,7 +1556,7 @@ export class TopicControlPlane
|
|
|
1385
1556
|
await this.ensureFanoutChannel(shardTopic, { ephemeral: true });
|
|
1386
1557
|
const st = this.fanoutChannels.get(shardTopic);
|
|
1387
1558
|
if (st) {
|
|
1388
|
-
void st.channel.
|
|
1559
|
+
void st.channel.publishMaybe(toUint8Array(embedded.bytes()));
|
|
1389
1560
|
this.touchFanoutChannel(shardTopic);
|
|
1390
1561
|
}
|
|
1391
1562
|
} catch {
|
|
@@ -1415,7 +1586,7 @@ export class TopicControlPlane
|
|
|
1415
1586
|
await this.ensureFanoutChannel(shardTopic, { ephemeral: true });
|
|
1416
1587
|
const st = this.fanoutChannels.get(shardTopic);
|
|
1417
1588
|
if (st) {
|
|
1418
|
-
void st.channel.
|
|
1589
|
+
void st.channel.publishMaybe(toUint8Array(embedded.bytes()));
|
|
1419
1590
|
this.touchFanoutChannel(shardTopic);
|
|
1420
1591
|
}
|
|
1421
1592
|
} catch {
|
|
@@ -1498,10 +1669,16 @@ export class TopicControlPlane
|
|
|
1498
1669
|
|
|
1499
1670
|
await Promise.all(
|
|
1500
1671
|
[...byShard.entries()].map(async ([shardTopic, userTopics]) => {
|
|
1672
|
+
const msg = new GetSubscribers({ topics: userTopics });
|
|
1673
|
+
const directPeer = to ? this.peers.get(to.hashcode()) : undefined;
|
|
1674
|
+
if (directPeer) {
|
|
1675
|
+
await this.sendDirectControlMessage(directPeer, msg);
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1501
1679
|
const persistent = (this.shardRefCounts.get(shardTopic) ?? 0) > 0;
|
|
1502
1680
|
await this.ensureFanoutChannel(shardTopic, { ephemeral: !persistent });
|
|
1503
1681
|
|
|
1504
|
-
const msg = new GetSubscribers({ topics: userTopics });
|
|
1505
1682
|
const embedded = await this.createMessage(toUint8Array(msg.bytes()), {
|
|
1506
1683
|
mode: new AnyWhere(),
|
|
1507
1684
|
priority: 1,
|
|
@@ -1519,7 +1696,7 @@ export class TopicControlPlane
|
|
|
1519
1696
|
timeoutMs: 5_000,
|
|
1520
1697
|
});
|
|
1521
1698
|
} catch {
|
|
1522
|
-
await st.channel.
|
|
1699
|
+
await st.channel.publishMaybe(payload);
|
|
1523
1700
|
}
|
|
1524
1701
|
} else {
|
|
1525
1702
|
await st.channel.publish(payload);
|
|
@@ -1874,18 +2051,16 @@ export class TopicControlPlane
|
|
|
1874
2051
|
} catch {
|
|
1875
2052
|
// ignore and fall back
|
|
1876
2053
|
}
|
|
1877
|
-
|
|
1878
|
-
await st.channel.publish(payload);
|
|
1879
|
-
} catch {
|
|
1880
|
-
// ignore
|
|
1881
|
-
}
|
|
2054
|
+
await st.channel.publishMaybe(payload);
|
|
1882
2055
|
}
|
|
1883
2056
|
|
|
1884
2057
|
private async processDirectPubSubMessage(input: {
|
|
1885
2058
|
pubsubMessage: PubSubMessage;
|
|
1886
2059
|
message: DataMessage;
|
|
2060
|
+
from: PublicSignKey;
|
|
2061
|
+
stream: PeerStreams;
|
|
1887
2062
|
}): Promise<void> {
|
|
1888
|
-
const { pubsubMessage, message } = input;
|
|
2063
|
+
const { pubsubMessage, message, from, stream } = input;
|
|
1889
2064
|
|
|
1890
2065
|
if (pubsubMessage instanceof TopicRootCandidates) {
|
|
1891
2066
|
// Used only to converge deterministic shard-root candidates in auto mode.
|
|
@@ -1893,6 +2068,103 @@ export class TopicControlPlane
|
|
|
1893
2068
|
return;
|
|
1894
2069
|
}
|
|
1895
2070
|
|
|
2071
|
+
if (pubsubMessage instanceof TopicRootQuery) {
|
|
2072
|
+
const root = await this.topicRootControlPlane.resolveCanonicalTopicRoot(
|
|
2073
|
+
pubsubMessage.topic,
|
|
2074
|
+
);
|
|
2075
|
+
await this.sendDirectControlMessage(
|
|
2076
|
+
stream,
|
|
2077
|
+
new TopicRootQueryResponse({
|
|
2078
|
+
requestId: pubsubMessage.requestId,
|
|
2079
|
+
topic: pubsubMessage.topic,
|
|
2080
|
+
root,
|
|
2081
|
+
}),
|
|
2082
|
+
).catch(() => {});
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
if (pubsubMessage instanceof TopicRootQueryResponse) {
|
|
2087
|
+
this.resolvePendingTopicRootQuery(pubsubMessage);
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
if (pubsubMessage instanceof Subscribe) {
|
|
2092
|
+
const sender = from;
|
|
2093
|
+
const senderKey = sender.hashcode();
|
|
2094
|
+
const relevantTopics = pubsubMessage.topics.filter((t) =>
|
|
2095
|
+
this.isTrackedTopic(t),
|
|
2096
|
+
);
|
|
2097
|
+
|
|
2098
|
+
if (
|
|
2099
|
+
relevantTopics.length > 0 &&
|
|
2100
|
+
this.subscriptionMessageIsLatest(message, pubsubMessage, relevantTopics)
|
|
2101
|
+
) {
|
|
2102
|
+
const changed: string[] = [];
|
|
2103
|
+
for (const topic of relevantTopics) {
|
|
2104
|
+
const peers = this.topics.get(topic);
|
|
2105
|
+
if (!peers) continue;
|
|
2106
|
+
this.initializePeer(sender);
|
|
2107
|
+
|
|
2108
|
+
const existing = peers.get(senderKey);
|
|
2109
|
+
if (!existing || existing.session < message.header.session) {
|
|
2110
|
+
peers.delete(senderKey);
|
|
2111
|
+
peers.set(
|
|
2112
|
+
senderKey,
|
|
2113
|
+
new SubscriptionData({
|
|
2114
|
+
session: message.header.session,
|
|
2115
|
+
timestamp: message.header.timestamp,
|
|
2116
|
+
publicKey: sender,
|
|
2117
|
+
}),
|
|
2118
|
+
);
|
|
2119
|
+
changed.push(topic);
|
|
2120
|
+
} else {
|
|
2121
|
+
peers.delete(senderKey);
|
|
2122
|
+
peers.set(senderKey, existing);
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
if (!existing) {
|
|
2126
|
+
this.peerToTopic.get(senderKey)!.add(topic);
|
|
2127
|
+
}
|
|
2128
|
+
this.pruneTopicSubscribers(topic);
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
if (changed.length > 0) {
|
|
2132
|
+
this.dispatchEvent(
|
|
2133
|
+
new CustomEvent<SubscriptionEvent>("subscribe", {
|
|
2134
|
+
detail: new SubscriptionEvent(sender, changed),
|
|
2135
|
+
}),
|
|
2136
|
+
);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
if (pubsubMessage.requestSubscribers) {
|
|
2141
|
+
const overlap = this.getSubscriptionOverlap(pubsubMessage.topics);
|
|
2142
|
+
if (overlap.length > 0) {
|
|
2143
|
+
await this.sendDirectControlMessage(
|
|
2144
|
+
stream,
|
|
2145
|
+
new Subscribe({
|
|
2146
|
+
topics: overlap,
|
|
2147
|
+
requestSubscribers: false,
|
|
2148
|
+
}),
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
if (pubsubMessage instanceof GetSubscribers) {
|
|
2156
|
+
const overlap = this.getSubscriptionOverlap(pubsubMessage.topics);
|
|
2157
|
+
if (overlap.length === 0) return;
|
|
2158
|
+
await this.sendDirectControlMessage(
|
|
2159
|
+
stream,
|
|
2160
|
+
new Subscribe({
|
|
2161
|
+
topics: overlap,
|
|
2162
|
+
requestSubscribers: false,
|
|
2163
|
+
}),
|
|
2164
|
+
);
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
1896
2168
|
if (pubsubMessage instanceof PubSubData) {
|
|
1897
2169
|
this.dispatchEvent(
|
|
1898
2170
|
new CustomEvent("data", {
|
|
@@ -2145,11 +2417,15 @@ export class TopicControlPlane
|
|
|
2145
2417
|
return super.onDataMessage(from, stream, message, seenBefore);
|
|
2146
2418
|
}
|
|
2147
2419
|
|
|
2148
|
-
// DirectStream
|
|
2149
|
-
//
|
|
2420
|
+
// DirectStream supports targeted pubsub data plus targeted subscriber snapshot
|
|
2421
|
+
// traffic used by Program.waitFor() when a peer is already connected directly.
|
|
2150
2422
|
if (
|
|
2151
2423
|
!(pubsubMessage instanceof PubSubData) &&
|
|
2152
|
-
!(pubsubMessage instanceof TopicRootCandidates)
|
|
2424
|
+
!(pubsubMessage instanceof TopicRootCandidates) &&
|
|
2425
|
+
!(pubsubMessage instanceof TopicRootQuery) &&
|
|
2426
|
+
!(pubsubMessage instanceof GetSubscribers) &&
|
|
2427
|
+
!(pubsubMessage instanceof Subscribe) &&
|
|
2428
|
+
!(pubsubMessage instanceof TopicRootQueryResponse)
|
|
2153
2429
|
) {
|
|
2154
2430
|
return true;
|
|
2155
2431
|
}
|
|
@@ -2176,7 +2452,12 @@ export class TopicControlPlane
|
|
|
2176
2452
|
if ((await this.verifyAndProcess(message)) === false) return false;
|
|
2177
2453
|
await this.maybeAcknowledgeMessage(stream, message, seenBefore);
|
|
2178
2454
|
if (seenBefore === 0) {
|
|
2179
|
-
await this.processDirectPubSubMessage({
|
|
2455
|
+
await this.processDirectPubSubMessage({
|
|
2456
|
+
pubsubMessage,
|
|
2457
|
+
message,
|
|
2458
|
+
from,
|
|
2459
|
+
stream,
|
|
2460
|
+
});
|
|
2180
2461
|
}
|
|
2181
2462
|
}
|
|
2182
2463
|
|
|
@@ -135,11 +135,30 @@ export class TopicRootControlPlane {
|
|
|
135
135
|
return [...this.trackers];
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
public resolveLocalTopicRoot(topic: string) {
|
|
139
|
+
return this.directory.resolveLocal(topic);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public resolveDeterministicTopicRoot(topic: string) {
|
|
143
|
+
return this.directory.resolveDeterministicCandidate(topic);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public resolveCanonicalTopicRoot(topic: string) {
|
|
147
|
+
return this.directory.resolveRoot(topic);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public resolveTrackedTopicRoot(topic: string) {
|
|
151
|
+
return this.resolveWithTrackers(topic, false);
|
|
152
|
+
}
|
|
153
|
+
|
|
138
154
|
public resolveTopicRoot(topic: string) {
|
|
139
|
-
return this.resolveWithTrackers(topic);
|
|
155
|
+
return this.resolveWithTrackers(topic, true);
|
|
140
156
|
}
|
|
141
157
|
|
|
142
|
-
private async resolveWithTrackers(
|
|
158
|
+
private async resolveWithTrackers(
|
|
159
|
+
topic: string,
|
|
160
|
+
fallbackToDeterministic = true,
|
|
161
|
+
): Promise<string | undefined> {
|
|
143
162
|
const local = await this.directory.resolveLocal(topic);
|
|
144
163
|
if (local) {
|
|
145
164
|
return local;
|
|
@@ -155,6 +174,8 @@ export class TopicRootControlPlane {
|
|
|
155
174
|
// ignore tracker failures and continue with remaining trackers
|
|
156
175
|
}
|
|
157
176
|
}
|
|
158
|
-
return
|
|
177
|
+
return fallbackToDeterministic
|
|
178
|
+
? this.directory.resolveDeterministicCandidate(topic)
|
|
179
|
+
: undefined;
|
|
159
180
|
}
|
|
160
181
|
}
|