@peerbit/pubsub 5.0.2 → 5.0.3-3dcfc85
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/benchmark/fanout-tree-sim-lib.d.ts.map +1 -1
- package/dist/benchmark/fanout-tree-sim-lib.js +28 -4
- package/dist/benchmark/fanout-tree-sim-lib.js.map +1 -1
- package/dist/src/fanout-tree.d.ts +5 -0
- package/dist/src/fanout-tree.d.ts.map +1 -1
- package/dist/src/fanout-tree.js +7 -0
- package/dist/src/fanout-tree.js.map +1 -1
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +205 -13
- package/dist/src/index.js.map +1 -1
- package/package.json +12 -11
- package/src/fanout-tree.ts +14 -0
- package/src/index.ts +260 -14
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { logger as loggerFn } from "@peerbit/logger";
|
|
|
7
7
|
import {
|
|
8
8
|
DataEvent,
|
|
9
9
|
GetSubscribers,
|
|
10
|
+
PeerUnavailable,
|
|
10
11
|
type PubSub,
|
|
11
12
|
PubSubData,
|
|
12
13
|
type PubSubEvents,
|
|
@@ -279,6 +280,9 @@ export class TopicControlPlane
|
|
|
279
280
|
[];
|
|
280
281
|
private autoCandidatesGossipInterval?: ReturnType<typeof setInterval>;
|
|
281
282
|
private autoCandidatesGossipUntil = 0;
|
|
283
|
+
private _onFanoutPeerUnreachable?: (
|
|
284
|
+
ev: CustomEvent<{ topic: string; root: string; publicKeyHash: string }>,
|
|
285
|
+
) => void;
|
|
282
286
|
|
|
283
287
|
private fanoutChannels = new Map<
|
|
284
288
|
string,
|
|
@@ -419,6 +423,13 @@ export class TopicControlPlane
|
|
|
419
423
|
|
|
420
424
|
public override async start() {
|
|
421
425
|
await this.fanout.start();
|
|
426
|
+
this._onFanoutPeerUnreachable =
|
|
427
|
+
this._onFanoutPeerUnreachable ||
|
|
428
|
+
this.onFanoutPeerUnreachable.bind(this);
|
|
429
|
+
await this.fanout.addEventListener(
|
|
430
|
+
"fanout:peer-unreachable",
|
|
431
|
+
this._onFanoutPeerUnreachable as any,
|
|
432
|
+
);
|
|
422
433
|
await super.start();
|
|
423
434
|
|
|
424
435
|
if (this.hostShards) {
|
|
@@ -430,6 +441,12 @@ export class TopicControlPlane
|
|
|
430
441
|
}
|
|
431
442
|
|
|
432
443
|
public override async stop() {
|
|
444
|
+
if (this._onFanoutPeerUnreachable) {
|
|
445
|
+
this.fanout.removeEventListener(
|
|
446
|
+
"fanout:peer-unreachable",
|
|
447
|
+
this._onFanoutPeerUnreachable as any,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
433
450
|
for (const st of this.fanoutChannels.values()) {
|
|
434
451
|
if (st.idleCloseTimeout) clearTimeout(st.idleCloseTimeout);
|
|
435
452
|
try {
|
|
@@ -1003,6 +1020,14 @@ export class TopicControlPlane
|
|
|
1003
1020
|
this.subscriptions.has(x),
|
|
1004
1021
|
);
|
|
1005
1022
|
if (!overlap) return;
|
|
1023
|
+
} else if (pubsubMessage instanceof PeerUnavailable) {
|
|
1024
|
+
const relevant =
|
|
1025
|
+
pubsubMessage.topics.length > 0
|
|
1026
|
+
? pubsubMessage.topics.some((x) => this.isTrackedTopic(x))
|
|
1027
|
+
: [...this.topics.keys()].some(
|
|
1028
|
+
(topic) => this.getShardTopicForUserTopic(topic) === t,
|
|
1029
|
+
);
|
|
1030
|
+
if (!relevant) return;
|
|
1006
1031
|
} else {
|
|
1007
1032
|
return;
|
|
1008
1033
|
}
|
|
@@ -1309,6 +1334,103 @@ export class TopicControlPlane
|
|
|
1309
1334
|
);
|
|
1310
1335
|
}
|
|
1311
1336
|
|
|
1337
|
+
private async announcePeerUnavailable(
|
|
1338
|
+
publicKeyHash: string,
|
|
1339
|
+
batches: { session: bigint; timestamp: bigint; topics: string[] }[],
|
|
1340
|
+
) {
|
|
1341
|
+
if (!this.started) throw new NotStartedError();
|
|
1342
|
+
|
|
1343
|
+
const byShard = new Map<
|
|
1344
|
+
string,
|
|
1345
|
+
{ session: bigint; timestamp: bigint; topics: string[] }
|
|
1346
|
+
>();
|
|
1347
|
+
for (const batch of batches) {
|
|
1348
|
+
for (const topic of batch.topics) {
|
|
1349
|
+
if (!this.isTrackedTopic(topic)) {
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
const shardTopic = this.getShardTopicForUserTopic(topic);
|
|
1353
|
+
const key = `${shardTopic}:${batch.session}:${batch.timestamp}`;
|
|
1354
|
+
const existing = byShard.get(key);
|
|
1355
|
+
if (existing) {
|
|
1356
|
+
existing.topics.push(topic);
|
|
1357
|
+
} else {
|
|
1358
|
+
byShard.set(key, {
|
|
1359
|
+
session: batch.session,
|
|
1360
|
+
timestamp: batch.timestamp,
|
|
1361
|
+
topics: [topic],
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
await Promise.all(
|
|
1368
|
+
[...byShard.entries()].map(async ([key, batch]) => {
|
|
1369
|
+
const [shardTopic] = key.split(":");
|
|
1370
|
+
if (!shardTopic || batch.topics.length === 0) {
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
try {
|
|
1374
|
+
const msg = new PeerUnavailable({
|
|
1375
|
+
publicKeyHash,
|
|
1376
|
+
session: batch.session,
|
|
1377
|
+
timestamp: batch.timestamp,
|
|
1378
|
+
topics: batch.topics,
|
|
1379
|
+
});
|
|
1380
|
+
const embedded = await this.createMessage(toUint8Array(msg.bytes()), {
|
|
1381
|
+
mode: new AnyWhere(),
|
|
1382
|
+
priority: 1,
|
|
1383
|
+
skipRecipientValidation: true,
|
|
1384
|
+
} as any);
|
|
1385
|
+
await this.ensureFanoutChannel(shardTopic, { ephemeral: true });
|
|
1386
|
+
const st = this.fanoutChannels.get(shardTopic);
|
|
1387
|
+
if (st) {
|
|
1388
|
+
void st.channel.publish(toUint8Array(embedded.bytes())).catch(() => {});
|
|
1389
|
+
this.touchFanoutChannel(shardTopic);
|
|
1390
|
+
}
|
|
1391
|
+
} catch {
|
|
1392
|
+
// best-effort
|
|
1393
|
+
}
|
|
1394
|
+
}),
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
private async announcePeerUnavailableOnShard(
|
|
1399
|
+
publicKeyHash: string,
|
|
1400
|
+
shardTopic: string,
|
|
1401
|
+
) {
|
|
1402
|
+
if (!this.started) throw new NotStartedError();
|
|
1403
|
+
try {
|
|
1404
|
+
const msg = new PeerUnavailable({
|
|
1405
|
+
publicKeyHash,
|
|
1406
|
+
session: 0n,
|
|
1407
|
+
timestamp: 0n,
|
|
1408
|
+
topics: [],
|
|
1409
|
+
});
|
|
1410
|
+
const embedded = await this.createMessage(toUint8Array(msg.bytes()), {
|
|
1411
|
+
mode: new AnyWhere(),
|
|
1412
|
+
priority: 1,
|
|
1413
|
+
skipRecipientValidation: true,
|
|
1414
|
+
} as any);
|
|
1415
|
+
await this.ensureFanoutChannel(shardTopic, { ephemeral: true });
|
|
1416
|
+
const st = this.fanoutChannels.get(shardTopic);
|
|
1417
|
+
if (st) {
|
|
1418
|
+
void st.channel.publish(toUint8Array(embedded.bytes())).catch(() => {});
|
|
1419
|
+
this.touchFanoutChannel(shardTopic);
|
|
1420
|
+
}
|
|
1421
|
+
} catch {
|
|
1422
|
+
// best-effort
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
private onFanoutPeerUnreachable(
|
|
1427
|
+
ev: CustomEvent<{ topic: string; root: string; publicKeyHash: string }>,
|
|
1428
|
+
) {
|
|
1429
|
+
void this
|
|
1430
|
+
.announcePeerUnavailableOnShard(ev.detail.publicKeyHash, ev.detail.topic)
|
|
1431
|
+
.catch(logErrorIfStarted);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1312
1434
|
getSubscribers(topic: string): PublicSignKey[] | undefined {
|
|
1313
1435
|
const t = topic.toString();
|
|
1314
1436
|
const remote = this.topics.get(t);
|
|
@@ -1554,7 +1676,55 @@ export class TopicControlPlane
|
|
|
1554
1676
|
public override onPeerUnreachable(publicKeyHash: string) {
|
|
1555
1677
|
super.onPeerUnreachable(publicKeyHash);
|
|
1556
1678
|
const key = this.peerKeyHashToPublicKey.get(publicKeyHash);
|
|
1557
|
-
if (key)
|
|
1679
|
+
if (!key) {
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
const removed = this.collectSubscriptionState(publicKeyHash);
|
|
1684
|
+
const changed = this.removeSubscriptions(key, "peer-unreachable");
|
|
1685
|
+
if (changed.length === 0) {
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
const changedSet = new Set(changed);
|
|
1690
|
+
const batches = removed
|
|
1691
|
+
.map((batch) => ({
|
|
1692
|
+
...batch,
|
|
1693
|
+
topics: batch.topics.filter((topic) => changedSet.has(topic)),
|
|
1694
|
+
}))
|
|
1695
|
+
.filter((batch) => batch.topics.length > 0);
|
|
1696
|
+
if (batches.length > 0) {
|
|
1697
|
+
void this
|
|
1698
|
+
.announcePeerUnavailable(publicKeyHash, batches)
|
|
1699
|
+
.catch(logErrorIfStarted);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
private collectSubscriptionState(peerHash: string) {
|
|
1704
|
+
const peerTopics = this.peerToTopic.get(peerHash);
|
|
1705
|
+
if (!peerTopics) {
|
|
1706
|
+
return [];
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
const grouped = new Map<string, { session: bigint; timestamp: bigint; topics: string[] }>();
|
|
1710
|
+
for (const topic of peerTopics) {
|
|
1711
|
+
const existing = this.topics.get(topic)?.get(peerHash);
|
|
1712
|
+
if (!existing) {
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
const key = `${existing.session}:${existing.timestamp}`;
|
|
1716
|
+
const batch = grouped.get(key);
|
|
1717
|
+
if (batch) {
|
|
1718
|
+
batch.topics.push(topic);
|
|
1719
|
+
} else {
|
|
1720
|
+
grouped.set(key, {
|
|
1721
|
+
session: existing.session,
|
|
1722
|
+
timestamp: existing.timestamp,
|
|
1723
|
+
topics: [topic],
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
return [...grouped.values()];
|
|
1558
1728
|
}
|
|
1559
1729
|
|
|
1560
1730
|
private removeSubscriptionsBeforeSession(
|
|
@@ -1619,18 +1789,16 @@ export class TopicControlPlane
|
|
|
1619
1789
|
}),
|
|
1620
1790
|
);
|
|
1621
1791
|
}
|
|
1792
|
+
|
|
1793
|
+
return changed;
|
|
1622
1794
|
}
|
|
1623
1795
|
|
|
1624
|
-
private
|
|
1625
|
-
|
|
1626
|
-
|
|
1796
|
+
private subscriptionStateIsLatest(
|
|
1797
|
+
subscriberKey: string,
|
|
1798
|
+
session: bigint,
|
|
1799
|
+
timestamp: bigint,
|
|
1627
1800
|
relevantTopics: string[],
|
|
1628
1801
|
) {
|
|
1629
|
-
const subscriber = message.header.signatures!.signatures[0].publicKey!;
|
|
1630
|
-
const subscriberKey = subscriber.hashcode();
|
|
1631
|
-
const messageSession = message.header.session;
|
|
1632
|
-
const messageTimestamp = message.header.timestamp;
|
|
1633
|
-
|
|
1634
1802
|
for (const topic of relevantTopics) {
|
|
1635
1803
|
const last = this.lastSubscriptionMessages
|
|
1636
1804
|
.get(subscriberKey)
|
|
@@ -1638,12 +1806,13 @@ export class TopicControlPlane
|
|
|
1638
1806
|
if (!last) {
|
|
1639
1807
|
continue;
|
|
1640
1808
|
}
|
|
1641
|
-
if (last.session >
|
|
1809
|
+
if (last.session > session) {
|
|
1642
1810
|
return false;
|
|
1643
1811
|
}
|
|
1644
1812
|
if (
|
|
1645
|
-
|
|
1646
|
-
last.
|
|
1813
|
+
timestamp !== 0n &&
|
|
1814
|
+
last.session === session &&
|
|
1815
|
+
last.timestamp > timestamp
|
|
1647
1816
|
) {
|
|
1648
1817
|
return false;
|
|
1649
1818
|
}
|
|
@@ -1656,13 +1825,27 @@ export class TopicControlPlane
|
|
|
1656
1825
|
this.lastSubscriptionMessages
|
|
1657
1826
|
.get(subscriberKey)!
|
|
1658
1827
|
.set(topic, {
|
|
1659
|
-
session
|
|
1660
|
-
timestamp
|
|
1828
|
+
session,
|
|
1829
|
+
timestamp,
|
|
1661
1830
|
});
|
|
1662
1831
|
}
|
|
1663
1832
|
return true;
|
|
1664
1833
|
}
|
|
1665
1834
|
|
|
1835
|
+
private subscriptionMessageIsLatest(
|
|
1836
|
+
message: DataMessage,
|
|
1837
|
+
_pubsubMessage: Subscribe | Unsubscribe,
|
|
1838
|
+
relevantTopics: string[],
|
|
1839
|
+
) {
|
|
1840
|
+
const subscriber = message.header.signatures!.signatures[0].publicKey!;
|
|
1841
|
+
return this.subscriptionStateIsLatest(
|
|
1842
|
+
subscriber.hashcode(),
|
|
1843
|
+
message.header.session,
|
|
1844
|
+
message.header.timestamp,
|
|
1845
|
+
relevantTopics,
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1666
1849
|
private async sendFanoutUnicastOrBroadcast(
|
|
1667
1850
|
shardTopic: string,
|
|
1668
1851
|
targetHash: string,
|
|
@@ -1857,6 +2040,69 @@ export class TopicControlPlane
|
|
|
1857
2040
|
return;
|
|
1858
2041
|
}
|
|
1859
2042
|
|
|
2043
|
+
if (pubsubMessage instanceof PeerUnavailable) {
|
|
2044
|
+
const peerHash = pubsubMessage.publicKeyHash;
|
|
2045
|
+
// Relay-originated shard deltas are keyed only by shard membership, not by
|
|
2046
|
+
// per-topic subscription watermarks. They are emitted immediately when the
|
|
2047
|
+
// relay loses a child so downstream peers can shed stale membership without
|
|
2048
|
+
// waiting for the slower shared-log liveness sweep.
|
|
2049
|
+
const isShardFastPath =
|
|
2050
|
+
pubsubMessage.topics.length === 0 && pubsubMessage.timestamp === 0n;
|
|
2051
|
+
const relevantTopics =
|
|
2052
|
+
pubsubMessage.topics.length > 0
|
|
2053
|
+
? pubsubMessage.topics.filter((topic) => {
|
|
2054
|
+
if (!this.isTrackedTopic(topic)) {
|
|
2055
|
+
return false;
|
|
2056
|
+
}
|
|
2057
|
+
return this.topics.get(topic)?.has(peerHash) ?? false;
|
|
2058
|
+
})
|
|
2059
|
+
: [...this.topics.keys()].filter(
|
|
2060
|
+
(topic) =>
|
|
2061
|
+
this.getShardTopicForUserTopic(topic) === shardTopic &&
|
|
2062
|
+
(this.topics.get(topic)?.has(peerHash) ?? false),
|
|
2063
|
+
);
|
|
2064
|
+
const shouldApply =
|
|
2065
|
+
relevantTopics.length > 0 &&
|
|
2066
|
+
(isShardFastPath ||
|
|
2067
|
+
this.subscriptionStateIsLatest(
|
|
2068
|
+
peerHash,
|
|
2069
|
+
pubsubMessage.session,
|
|
2070
|
+
pubsubMessage.timestamp,
|
|
2071
|
+
relevantTopics,
|
|
2072
|
+
));
|
|
2073
|
+
if (shouldApply) {
|
|
2074
|
+
const changed: string[] = [];
|
|
2075
|
+
let publicKey: PublicSignKey | undefined;
|
|
2076
|
+
for (const topic of relevantTopics) {
|
|
2077
|
+
const peers = this.topics.get(topic);
|
|
2078
|
+
if (!peers) continue;
|
|
2079
|
+
const existing = peers.get(peerHash);
|
|
2080
|
+
if (!existing) continue;
|
|
2081
|
+
publicKey = publicKey ?? existing.publicKey;
|
|
2082
|
+
if (peers.delete(peerHash)) {
|
|
2083
|
+
changed.push(topic);
|
|
2084
|
+
this.peerToTopic.get(peerHash)?.delete(topic);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
if (!this.peerToTopic.get(peerHash)?.size) {
|
|
2088
|
+
this.peerToTopic.delete(peerHash);
|
|
2089
|
+
this.lastSubscriptionMessages.delete(peerHash);
|
|
2090
|
+
}
|
|
2091
|
+
if (changed.length > 0 && publicKey) {
|
|
2092
|
+
this.dispatchEvent(
|
|
2093
|
+
new CustomEvent<UnsubcriptionEvent>("unsubscribe", {
|
|
2094
|
+
detail: new UnsubcriptionEvent(
|
|
2095
|
+
publicKey,
|
|
2096
|
+
changed,
|
|
2097
|
+
"peer-unreachable",
|
|
2098
|
+
),
|
|
2099
|
+
}),
|
|
2100
|
+
);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
1860
2106
|
if (pubsubMessage instanceof GetSubscribers) {
|
|
1861
2107
|
const sender = from;
|
|
1862
2108
|
const senderKey = sender.hashcode();
|