@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/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 (this.subscriptions.get(topic)) subscriptions.push(topic);
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
- for (const [topic] of this.subscriptions) subscriptions.push(topic);
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(publicKey: PublicSignKey) {
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(sender, changed),
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
  }