@peerbit/pubsub 4.1.4-cb91e7b → 5.0.0
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 +2 -1
- package/dist/src/fanout-tree.d.ts.map +1 -1
- package/dist/src/fanout-tree.js +38 -1
- package/dist/src/fanout-tree.js.map +1 -1
- package/dist/src/index.d.ts +8 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +70 -8
- package/dist/src/index.js.map +1 -1
- package/package.json +10 -10
- package/src/fanout-tree.ts +50 -1
- package/src/index.ts +93 -8
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
SubscriptionEvent,
|
|
18
18
|
TopicRootCandidates,
|
|
19
19
|
UnsubcriptionEvent,
|
|
20
|
+
type UnsubscriptionReason,
|
|
20
21
|
Unsubscribe,
|
|
21
22
|
} from "@peerbit/pubsub-interface";
|
|
22
23
|
import {
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
MessageHeader,
|
|
37
38
|
NotStartedError,
|
|
38
39
|
type PriorityOptions,
|
|
40
|
+
type RouteHint,
|
|
39
41
|
SilentDelivery,
|
|
40
42
|
type WithExtraSigners,
|
|
41
43
|
deliveryModeHasReceiver,
|
|
@@ -228,6 +230,8 @@ export class TopicControlPlane
|
|
|
228
230
|
public peerToTopic: Map<string, Set<string>>;
|
|
229
231
|
// Local topic -> reference count.
|
|
230
232
|
public subscriptions: Map<string, { counter: number }>;
|
|
233
|
+
// Local topics requested via debounced subscribe, not yet applied in `subscriptions`.
|
|
234
|
+
private pendingSubscriptions: Set<string>;
|
|
231
235
|
public lastSubscriptionMessages: Map<string, Map<string, bigint>> = new Map();
|
|
232
236
|
public dispatchEventOnSelfPublish: boolean;
|
|
233
237
|
public readonly topicRootControlPlane: TopicRootControlPlane;
|
|
@@ -289,6 +293,7 @@ export class TopicControlPlane
|
|
|
289
293
|
) {
|
|
290
294
|
super(components, ["/peerbit/topic-control-plane/2.0.0"], props);
|
|
291
295
|
this.subscriptions = new Map();
|
|
296
|
+
this.pendingSubscriptions = new Set();
|
|
292
297
|
this.topics = new Map();
|
|
293
298
|
this.peerToTopic = new Map();
|
|
294
299
|
|
|
@@ -448,6 +453,7 @@ export class TopicControlPlane
|
|
|
448
453
|
this.autoCandidatesGossipUntil = 0;
|
|
449
454
|
|
|
450
455
|
this.subscriptions.clear();
|
|
456
|
+
this.pendingSubscriptions.clear();
|
|
451
457
|
this.topics.clear();
|
|
452
458
|
this.peerToTopic.clear();
|
|
453
459
|
this.lastSubscriptionMessages.clear();
|
|
@@ -760,11 +766,24 @@ export class TopicControlPlane
|
|
|
760
766
|
const subscriptions: string[] = [];
|
|
761
767
|
if (topics) {
|
|
762
768
|
for (const topic of topics) {
|
|
763
|
-
if (
|
|
769
|
+
if (
|
|
770
|
+
this.subscriptions.get(topic) ||
|
|
771
|
+
this.pendingSubscriptions.has(topic)
|
|
772
|
+
) {
|
|
773
|
+
subscriptions.push(topic);
|
|
774
|
+
}
|
|
764
775
|
}
|
|
765
776
|
return subscriptions;
|
|
766
777
|
}
|
|
767
|
-
|
|
778
|
+
const seen = new Set<string>();
|
|
779
|
+
for (const [topic] of this.subscriptions) {
|
|
780
|
+
subscriptions.push(topic);
|
|
781
|
+
seen.add(topic);
|
|
782
|
+
}
|
|
783
|
+
for (const topic of this.pendingSubscriptions) {
|
|
784
|
+
if (seen.has(topic)) continue;
|
|
785
|
+
subscriptions.push(topic);
|
|
786
|
+
}
|
|
768
787
|
return subscriptions;
|
|
769
788
|
}
|
|
770
789
|
|
|
@@ -1098,6 +1117,10 @@ export class TopicControlPlane
|
|
|
1098
1117
|
}
|
|
1099
1118
|
|
|
1100
1119
|
async subscribe(topic: string) {
|
|
1120
|
+
this.pendingSubscriptions.add(topic);
|
|
1121
|
+
// `subscribe()` is debounced; start tracking immediately to avoid dropping
|
|
1122
|
+
// inbound subscription traffic during the debounce window.
|
|
1123
|
+
this.initializeTopic(topic);
|
|
1101
1124
|
return this.debounceSubscribeAggregator.add({ key: topic });
|
|
1102
1125
|
}
|
|
1103
1126
|
|
|
@@ -1111,10 +1134,12 @@ export class TopicControlPlane
|
|
|
1111
1134
|
let prev = this.subscriptions.get(topic);
|
|
1112
1135
|
if (prev) {
|
|
1113
1136
|
prev.counter += counter;
|
|
1137
|
+
this.pendingSubscriptions.delete(topic);
|
|
1114
1138
|
continue;
|
|
1115
1139
|
}
|
|
1116
1140
|
this.subscriptions.set(topic, { counter });
|
|
1117
1141
|
this.initializeTopic(topic);
|
|
1142
|
+
this.pendingSubscriptions.delete(topic);
|
|
1118
1143
|
|
|
1119
1144
|
const shardTopic = this.getShardTopicForUserTopic(topic);
|
|
1120
1145
|
byShard.set(shardTopic, [...(byShard.get(shardTopic) ?? []), topic]);
|
|
@@ -1156,8 +1181,13 @@ export class TopicControlPlane
|
|
|
1156
1181
|
data?: Uint8Array;
|
|
1157
1182
|
},
|
|
1158
1183
|
) {
|
|
1184
|
+
this.pendingSubscriptions.delete(topic);
|
|
1185
|
+
|
|
1159
1186
|
if (this.debounceSubscribeAggregator.has(topic)) {
|
|
1160
1187
|
this.debounceSubscribeAggregator.delete(topic);
|
|
1188
|
+
if (!this.subscriptions.has(topic)) {
|
|
1189
|
+
this.untrackTopic(topic);
|
|
1190
|
+
}
|
|
1161
1191
|
return false;
|
|
1162
1192
|
}
|
|
1163
1193
|
|
|
@@ -1252,6 +1282,39 @@ export class TopicControlPlane
|
|
|
1252
1282
|
return ret;
|
|
1253
1283
|
}
|
|
1254
1284
|
|
|
1285
|
+
/**
|
|
1286
|
+
* Returns best-effort route hints for a target peer by combining:
|
|
1287
|
+
* - DirectStream ACK-learned routes
|
|
1288
|
+
* - Fanout route tokens for the topic's shard overlay
|
|
1289
|
+
*/
|
|
1290
|
+
getUnifiedRouteHints(topic: string, targetHash: string): RouteHint[] {
|
|
1291
|
+
const hints: RouteHint[] = [];
|
|
1292
|
+
const directHint = this.getBestRouteHint(targetHash);
|
|
1293
|
+
if (directHint) {
|
|
1294
|
+
hints.push(directHint);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const topicString = topic.toString();
|
|
1298
|
+
const shardTopic = topicString.startsWith(this.shardTopicPrefix)
|
|
1299
|
+
? topicString
|
|
1300
|
+
: this.getShardTopicForUserTopic(topicString);
|
|
1301
|
+
const shard = this.fanoutChannels.get(shardTopic);
|
|
1302
|
+
if (!shard) {
|
|
1303
|
+
return hints;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
const fanoutHint = this.fanout.getRouteHint(
|
|
1307
|
+
shardTopic,
|
|
1308
|
+
shard.root,
|
|
1309
|
+
targetHash,
|
|
1310
|
+
);
|
|
1311
|
+
if (fanoutHint) {
|
|
1312
|
+
hints.push(fanoutHint);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
return hints;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1255
1318
|
async requestSubscribers(
|
|
1256
1319
|
topic: string | string[],
|
|
1257
1320
|
to?: PublicSignKey,
|
|
@@ -1434,16 +1497,19 @@ export class TopicControlPlane
|
|
|
1434
1497
|
}
|
|
1435
1498
|
|
|
1436
1499
|
public onPeerSession(key: PublicSignKey, _session: number): void {
|
|
1437
|
-
this.removeSubscriptions(key);
|
|
1500
|
+
this.removeSubscriptions(key, "peer-session-reset");
|
|
1438
1501
|
}
|
|
1439
1502
|
|
|
1440
1503
|
public override onPeerUnreachable(publicKeyHash: string) {
|
|
1441
1504
|
super.onPeerUnreachable(publicKeyHash);
|
|
1442
1505
|
const key = this.peerKeyHashToPublicKey.get(publicKeyHash);
|
|
1443
|
-
if (key) this.removeSubscriptions(key);
|
|
1506
|
+
if (key) this.removeSubscriptions(key, "peer-unreachable");
|
|
1444
1507
|
}
|
|
1445
1508
|
|
|
1446
|
-
private removeSubscriptions(
|
|
1509
|
+
private removeSubscriptions(
|
|
1510
|
+
publicKey: PublicSignKey,
|
|
1511
|
+
reason: UnsubscriptionReason,
|
|
1512
|
+
) {
|
|
1447
1513
|
const peerHash = publicKey.hashcode();
|
|
1448
1514
|
const peerTopics = this.peerToTopic.get(peerHash);
|
|
1449
1515
|
const changed: string[] = [];
|
|
@@ -1462,7 +1528,7 @@ export class TopicControlPlane
|
|
|
1462
1528
|
if (changed.length > 0) {
|
|
1463
1529
|
this.dispatchEvent(
|
|
1464
1530
|
new CustomEvent<UnsubcriptionEvent>("unsubscribe", {
|
|
1465
|
-
detail: new UnsubcriptionEvent(publicKey, changed),
|
|
1531
|
+
detail: new UnsubcriptionEvent(publicKey, changed, reason),
|
|
1466
1532
|
}),
|
|
1467
1533
|
);
|
|
1468
1534
|
}
|
|
@@ -1504,6 +1570,21 @@ export class TopicControlPlane
|
|
|
1504
1570
|
) {
|
|
1505
1571
|
const st = this.fanoutChannels.get(shardTopic);
|
|
1506
1572
|
if (!st) return;
|
|
1573
|
+
const hints = this.getUnifiedRouteHints(shardTopic, targetHash);
|
|
1574
|
+
const fanoutHint = hints.find(
|
|
1575
|
+
(hint): hint is Extract<RouteHint, { kind: "fanout-token" }> =>
|
|
1576
|
+
hint.kind === "fanout-token",
|
|
1577
|
+
);
|
|
1578
|
+
if (fanoutHint) {
|
|
1579
|
+
try {
|
|
1580
|
+
await st.channel.unicastAck(fanoutHint.route, payload, {
|
|
1581
|
+
timeoutMs: 5_000,
|
|
1582
|
+
});
|
|
1583
|
+
return;
|
|
1584
|
+
} catch {
|
|
1585
|
+
// ignore and fall back
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1507
1588
|
try {
|
|
1508
1589
|
await st.channel.unicastToAck(targetHash, payload, { timeoutMs: 5_000 });
|
|
1509
1590
|
return;
|
|
@@ -1664,7 +1745,11 @@ export class TopicControlPlane
|
|
|
1664
1745
|
if (changed.length > 0) {
|
|
1665
1746
|
this.dispatchEvent(
|
|
1666
1747
|
new CustomEvent<UnsubcriptionEvent>("unsubscribe", {
|
|
1667
|
-
detail: new UnsubcriptionEvent(
|
|
1748
|
+
detail: new UnsubcriptionEvent(
|
|
1749
|
+
sender,
|
|
1750
|
+
changed,
|
|
1751
|
+
"remote-unsubscribe",
|
|
1752
|
+
),
|
|
1668
1753
|
}),
|
|
1669
1754
|
);
|
|
1670
1755
|
}
|
|
@@ -1736,7 +1821,7 @@ export class TopicControlPlane
|
|
|
1736
1821
|
|
|
1737
1822
|
if (pubsubMessage instanceof PubSubData) {
|
|
1738
1823
|
const wantsTopic = pubsubMessage.topics.some((t) =>
|
|
1739
|
-
this.subscriptions.has(t),
|
|
1824
|
+
this.subscriptions.has(t) || this.pendingSubscriptions.has(t),
|
|
1740
1825
|
);
|
|
1741
1826
|
isForMe = pubsubMessage.strict ? isForMe && wantsTopic : wantsTopic;
|
|
1742
1827
|
}
|