@peerbit/pubsub 5.1.5 → 5.2.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 +24 -0
- package/dist/src/fanout-tree.d.ts.map +1 -1
- package/dist/src/fanout-tree.js +366 -101
- package/dist/src/fanout-tree.js.map +1 -1
- package/package.json +3 -3
- package/src/fanout-tree.ts +492 -97
package/src/fanout-tree.ts
CHANGED
|
@@ -335,6 +335,17 @@ export type FanoutProviderQueryOptions = {
|
|
|
335
335
|
bootstrapMaxPeers?: number;
|
|
336
336
|
};
|
|
337
337
|
|
|
338
|
+
export type FanoutProviderWatchOptions = {
|
|
339
|
+
want?: number;
|
|
340
|
+
signal?: AbortSignal;
|
|
341
|
+
onProviders: (providers: FanoutProviderCandidate[]) => void;
|
|
342
|
+
bootstrap?: Array<string | Multiaddr>;
|
|
343
|
+
bootstrapDialTimeoutMs?: number;
|
|
344
|
+
bootstrapMaxPeers?: number;
|
|
345
|
+
ttlMs?: number;
|
|
346
|
+
renewIntervalMs?: number;
|
|
347
|
+
};
|
|
348
|
+
|
|
338
349
|
export type FanoutProviderHandle = {
|
|
339
350
|
close: () => void;
|
|
340
351
|
};
|
|
@@ -447,6 +458,10 @@ export interface FanoutTreeEvents extends StreamEvents {
|
|
|
447
458
|
"fanout:unicast": CustomEvent<FanoutTreeUnicastEvent>;
|
|
448
459
|
"fanout:joined": CustomEvent<{ topic: string; root: string; parent: string }>;
|
|
449
460
|
"fanout:kicked": CustomEvent<{ topic: string; root: string; from: string }>;
|
|
461
|
+
"fanout:provider": CustomEvent<{
|
|
462
|
+
namespace: string;
|
|
463
|
+
providers: FanoutProviderCandidate[];
|
|
464
|
+
}>;
|
|
450
465
|
"fanout:peer-unreachable": CustomEvent<{
|
|
451
466
|
topic: string;
|
|
452
467
|
root: string;
|
|
@@ -520,6 +535,9 @@ const MSG_TRACKER_FEEDBACK = 33;
|
|
|
520
535
|
const MSG_PROVIDER_ANNOUNCE = 34;
|
|
521
536
|
const MSG_PROVIDER_QUERY = 35;
|
|
522
537
|
const MSG_PROVIDER_REPLY = 36;
|
|
538
|
+
const MSG_PROVIDER_SUBSCRIBE = 37;
|
|
539
|
+
const MSG_PROVIDER_UNSUBSCRIBE = 38;
|
|
540
|
+
const MSG_PROVIDER_NOTIFY = 39;
|
|
523
541
|
|
|
524
542
|
const JOIN_REJECT_NOT_ATTACHED = 1;
|
|
525
543
|
const JOIN_REJECT_NO_CAPACITY = 2;
|
|
@@ -1081,6 +1099,12 @@ type ProviderEntry = {
|
|
|
1081
1099
|
expiresAt: number;
|
|
1082
1100
|
};
|
|
1083
1101
|
|
|
1102
|
+
type ProviderWatchRegistration = {
|
|
1103
|
+
hash: string;
|
|
1104
|
+
want: number;
|
|
1105
|
+
expiresAt: number;
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1084
1108
|
type ProviderAnnounceState = {
|
|
1085
1109
|
id: ProviderNamespaceId;
|
|
1086
1110
|
ttlMs: number;
|
|
@@ -1092,6 +1116,21 @@ type ProviderAnnounceState = {
|
|
|
1092
1116
|
loop?: Promise<void>;
|
|
1093
1117
|
};
|
|
1094
1118
|
|
|
1119
|
+
type ProviderWatchState = {
|
|
1120
|
+
id: ProviderNamespaceId;
|
|
1121
|
+
want: number;
|
|
1122
|
+
ttlMs: number;
|
|
1123
|
+
renewIntervalMs: number;
|
|
1124
|
+
bootstrapOverride?: Multiaddr[];
|
|
1125
|
+
bootstrapDialTimeoutMs: number;
|
|
1126
|
+
bootstrapMaxPeers: number;
|
|
1127
|
+
onProviders: (providers: FanoutProviderCandidate[]) => void;
|
|
1128
|
+
signal?: AbortSignal;
|
|
1129
|
+
closed: boolean;
|
|
1130
|
+
trackerPeers: string[];
|
|
1131
|
+
loop?: Promise<void>;
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1095
1134
|
const encodeProviderAnnounce = (
|
|
1096
1135
|
namespaceKey: Uint8Array,
|
|
1097
1136
|
ttlMs: number,
|
|
@@ -1137,13 +1176,9 @@ const encodeProviderQuery = (
|
|
|
1137
1176
|
return buf;
|
|
1138
1177
|
};
|
|
1139
1178
|
|
|
1140
|
-
const
|
|
1141
|
-
namespaceKey: Uint8Array,
|
|
1142
|
-
reqId: number,
|
|
1143
|
-
entries: ProviderEntry[],
|
|
1144
|
-
) => {
|
|
1179
|
+
const encodeProviderEntries = (entries: ProviderEntry[]) => {
|
|
1145
1180
|
const count = Math.max(0, Math.min(255, entries.length));
|
|
1146
|
-
let bytes = 1
|
|
1181
|
+
let bytes = 1;
|
|
1147
1182
|
const encoded: Array<{ hashBytes: Uint8Array; addrs: Uint8Array[] }> = [];
|
|
1148
1183
|
for (let i = 0; i < count; i++) {
|
|
1149
1184
|
const e = entries[i]!;
|
|
@@ -1155,6 +1190,16 @@ const encodeProviderReply = (
|
|
|
1155
1190
|
for (const a of addrs) bytes += 2 + a.length;
|
|
1156
1191
|
encoded.push({ hashBytes, addrs });
|
|
1157
1192
|
}
|
|
1193
|
+
return { bytes, encoded };
|
|
1194
|
+
};
|
|
1195
|
+
|
|
1196
|
+
const encodeProviderReply = (
|
|
1197
|
+
namespaceKey: Uint8Array,
|
|
1198
|
+
reqId: number,
|
|
1199
|
+
entries: ProviderEntry[],
|
|
1200
|
+
) => {
|
|
1201
|
+
const { bytes: entryBytes, encoded } = encodeProviderEntries(entries);
|
|
1202
|
+
let bytes = 1 + 32 + 4 + entryBytes;
|
|
1158
1203
|
|
|
1159
1204
|
const buf = new Uint8Array(bytes);
|
|
1160
1205
|
buf[0] = MSG_PROVIDER_REPLY;
|
|
@@ -1177,6 +1222,90 @@ const encodeProviderReply = (
|
|
|
1177
1222
|
return buf;
|
|
1178
1223
|
};
|
|
1179
1224
|
|
|
1225
|
+
const encodeProviderSubscribe = (
|
|
1226
|
+
namespaceKey: Uint8Array,
|
|
1227
|
+
want: number,
|
|
1228
|
+
ttlMs: number,
|
|
1229
|
+
) => {
|
|
1230
|
+
const buf = new Uint8Array(1 + 32 + 2 + 4);
|
|
1231
|
+
buf[0] = MSG_PROVIDER_SUBSCRIBE;
|
|
1232
|
+
buf.set(namespaceKey, 1);
|
|
1233
|
+
writeU16BE(buf, 33, clampU16(want));
|
|
1234
|
+
writeU32BE(buf, 35, Math.max(0, Math.floor(ttlMs)) >>> 0);
|
|
1235
|
+
return buf;
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
const encodeProviderUnsubscribe = (namespaceKey: Uint8Array) => {
|
|
1239
|
+
const buf = new Uint8Array(1 + 32);
|
|
1240
|
+
buf[0] = MSG_PROVIDER_UNSUBSCRIBE;
|
|
1241
|
+
buf.set(namespaceKey, 1);
|
|
1242
|
+
return buf;
|
|
1243
|
+
};
|
|
1244
|
+
|
|
1245
|
+
const encodeProviderNotify = (
|
|
1246
|
+
namespaceKey: Uint8Array,
|
|
1247
|
+
entries: ProviderEntry[],
|
|
1248
|
+
) => {
|
|
1249
|
+
const { bytes: entryBytes, encoded } = encodeProviderEntries(entries);
|
|
1250
|
+
let bytes = 1 + 32 + entryBytes;
|
|
1251
|
+
|
|
1252
|
+
const buf = new Uint8Array(bytes);
|
|
1253
|
+
buf[0] = MSG_PROVIDER_NOTIFY;
|
|
1254
|
+
buf.set(namespaceKey, 1);
|
|
1255
|
+
buf[33] = Math.max(0, Math.min(255, encoded.length)) & 0xff;
|
|
1256
|
+
let offset = 34;
|
|
1257
|
+
for (const e of encoded) {
|
|
1258
|
+
buf[offset++] = e.hashBytes.length & 0xff;
|
|
1259
|
+
buf.set(e.hashBytes, offset);
|
|
1260
|
+
offset += e.hashBytes.length;
|
|
1261
|
+
buf[offset++] = Math.max(0, Math.min(255, e.addrs.length)) & 0xff;
|
|
1262
|
+
for (const a of e.addrs) {
|
|
1263
|
+
writeU16BE(buf, offset, a.length);
|
|
1264
|
+
offset += 2;
|
|
1265
|
+
buf.set(a, offset);
|
|
1266
|
+
offset += a.length;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return buf;
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
const decodeProviderEntries = (
|
|
1273
|
+
data: Uint8Array,
|
|
1274
|
+
offsetStart: number,
|
|
1275
|
+
maxCount: number,
|
|
1276
|
+
) => {
|
|
1277
|
+
let offset = offsetStart;
|
|
1278
|
+
const providers: FanoutProviderCandidate[] = [];
|
|
1279
|
+
const limit = Math.min(maxCount, 255);
|
|
1280
|
+
for (let i = 0; i < limit; i++) {
|
|
1281
|
+
if (offset + 1 > data.length) break;
|
|
1282
|
+
const hashLen = data[offset++]!;
|
|
1283
|
+
if (offset + hashLen > data.length) break;
|
|
1284
|
+
const hash = textDecoder.decode(data.subarray(offset, offset + hashLen));
|
|
1285
|
+
offset += hashLen;
|
|
1286
|
+
if (offset + 1 > data.length) break;
|
|
1287
|
+
const addrCount = data[offset++]!;
|
|
1288
|
+
const addrs: Multiaddr[] = [];
|
|
1289
|
+
const addrMax = Math.min(addrCount, 16);
|
|
1290
|
+
for (let j = 0; j < addrMax; j++) {
|
|
1291
|
+
if (offset + 2 > data.length) break;
|
|
1292
|
+
const len = readU16BE(data, offset);
|
|
1293
|
+
offset += 2;
|
|
1294
|
+
if (offset + len > data.length) break;
|
|
1295
|
+
const bytes = data.subarray(offset, offset + len);
|
|
1296
|
+
offset += len;
|
|
1297
|
+
try {
|
|
1298
|
+
addrs.push(multiaddr(bytes));
|
|
1299
|
+
} catch {
|
|
1300
|
+
// ignore invalid
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
if (!hash) continue;
|
|
1304
|
+
providers.push({ hash, addrs });
|
|
1305
|
+
}
|
|
1306
|
+
return { providers, offset };
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1180
1309
|
type ChildInfo = { bidPerByte: number };
|
|
1181
1310
|
|
|
1182
1311
|
type JoinAttemptResult = {
|
|
@@ -1433,6 +1562,12 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
1433
1562
|
private trackerNamespaceLru = new Map<string, number>();
|
|
1434
1563
|
private providerBySuffixKey = new Map<string, Map<string, ProviderEntry>>();
|
|
1435
1564
|
private providerNamespaceLru = new Map<string, number>();
|
|
1565
|
+
private providerNamespaceBySuffixKey = new Map<string, string>();
|
|
1566
|
+
private providerWatchersBySuffixKey = new Map<
|
|
1567
|
+
string,
|
|
1568
|
+
Map<string, ProviderWatchRegistration>
|
|
1569
|
+
>();
|
|
1570
|
+
private providerWatchesBySuffixKey = new Map<string, Set<ProviderWatchState>>();
|
|
1436
1571
|
private underlayPeerDisconnectHandler?: (ev: any) => void;
|
|
1437
1572
|
private pendingProviderQueryBySuffixKey = new Map<
|
|
1438
1573
|
string,
|
|
@@ -1533,6 +1668,7 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
1533
1668
|
if (!oldest) break;
|
|
1534
1669
|
this.providerNamespaceLru.delete(oldest);
|
|
1535
1670
|
this.providerBySuffixKey.delete(oldest);
|
|
1671
|
+
this.providerNamespaceBySuffixKey.delete(oldest);
|
|
1536
1672
|
this.pendingProviderQueryBySuffixKey.delete(oldest);
|
|
1537
1673
|
}
|
|
1538
1674
|
}
|
|
@@ -1546,12 +1682,136 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
1546
1682
|
}
|
|
1547
1683
|
}
|
|
1548
1684
|
|
|
1685
|
+
private pruneProviderWatchersIfEmpty(suffixKey: string) {
|
|
1686
|
+
const watchers = this.providerWatchersBySuffixKey.get(suffixKey);
|
|
1687
|
+
if (watchers && watchers.size === 0) {
|
|
1688
|
+
this.providerWatchersBySuffixKey.delete(suffixKey);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
private getProviderCandidatesFromCache(
|
|
1693
|
+
id: ProviderNamespaceId,
|
|
1694
|
+
options?: { now?: number; want?: number },
|
|
1695
|
+
): FanoutProviderCandidate[] {
|
|
1696
|
+
const now = options?.now ?? Date.now();
|
|
1697
|
+
const cached: FanoutProviderCandidate[] = [];
|
|
1698
|
+
const byPeer = this.providerBySuffixKey.get(id.suffixKey);
|
|
1699
|
+
if (byPeer) {
|
|
1700
|
+
for (const [hash, e] of byPeer) {
|
|
1701
|
+
if (e.expiresAt <= now) {
|
|
1702
|
+
byPeer.delete(hash);
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
1705
|
+
if (hash === this.publicKeyHash) continue;
|
|
1706
|
+
const addrs: Multiaddr[] = [];
|
|
1707
|
+
for (const a of e.addrs) {
|
|
1708
|
+
try {
|
|
1709
|
+
addrs.push(multiaddr(a));
|
|
1710
|
+
} catch {
|
|
1711
|
+
// ignore invalid
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
cached.push({ hash, addrs });
|
|
1715
|
+
if (options?.want && cached.length >= options.want) break;
|
|
1716
|
+
}
|
|
1717
|
+
this.pruneProviderNamespaceIfEmpty(id.suffixKey);
|
|
1718
|
+
}
|
|
1719
|
+
return cached;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
private rememberProviderCandidates(
|
|
1723
|
+
id: ProviderNamespaceId,
|
|
1724
|
+
candidates: FanoutProviderCandidate[],
|
|
1725
|
+
expiresAt: number,
|
|
1726
|
+
) {
|
|
1727
|
+
if (candidates.length === 0) return;
|
|
1728
|
+
let cache = this.providerBySuffixKey.get(id.suffixKey);
|
|
1729
|
+
if (!cache) {
|
|
1730
|
+
cache = new Map<string, ProviderEntry>();
|
|
1731
|
+
this.providerBySuffixKey.set(id.suffixKey, cache);
|
|
1732
|
+
}
|
|
1733
|
+
for (const c of candidates) {
|
|
1734
|
+
cache.delete(c.hash);
|
|
1735
|
+
cache.set(c.hash, {
|
|
1736
|
+
hash: c.hash,
|
|
1737
|
+
addrs: c.addrs.map((a) => a.bytes),
|
|
1738
|
+
expiresAt,
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
while (cache.size > PROVIDER_DIRECTORY_MAX_ENTRIES) {
|
|
1742
|
+
const oldest = cache.keys().next().value as string | undefined;
|
|
1743
|
+
if (!oldest) break;
|
|
1744
|
+
cache.delete(oldest);
|
|
1745
|
+
}
|
|
1746
|
+
this.touchProviderNamespace(id.suffixKey);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
private emitProviderUpdate(
|
|
1750
|
+
id: ProviderNamespaceId,
|
|
1751
|
+
providers: FanoutProviderCandidate[],
|
|
1752
|
+
) {
|
|
1753
|
+
if (providers.length === 0) return;
|
|
1754
|
+
this.dispatchEvent(
|
|
1755
|
+
new CustomEvent("fanout:provider", {
|
|
1756
|
+
detail: { namespace: id.namespace, providers },
|
|
1757
|
+
}),
|
|
1758
|
+
);
|
|
1759
|
+
const localWatches = this.providerWatchesBySuffixKey.get(id.suffixKey);
|
|
1760
|
+
if (!localWatches || localWatches.size === 0) return;
|
|
1761
|
+
for (const watch of localWatches) {
|
|
1762
|
+
if (watch.closed) continue;
|
|
1763
|
+
try {
|
|
1764
|
+
watch.onProviders(providers.slice(0, watch.want));
|
|
1765
|
+
} catch {
|
|
1766
|
+
// ignore consumer callback failures
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
private async publishProviderWatchUpdate(id: ProviderNamespaceId) {
|
|
1772
|
+
const watchers = this.providerWatchersBySuffixKey.get(id.suffixKey);
|
|
1773
|
+
if (!watchers || watchers.size === 0) return;
|
|
1774
|
+
const now = Date.now();
|
|
1775
|
+
const cached = this.getProviderCandidatesFromCache(id, { now });
|
|
1776
|
+
const byPeer = new Map(watchers);
|
|
1777
|
+
for (const [hash, watch] of byPeer) {
|
|
1778
|
+
if (watch.expiresAt <= now) {
|
|
1779
|
+
watchers.delete(hash);
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1782
|
+
const entries = cached
|
|
1783
|
+
.filter((candidate) => candidate.hash !== hash)
|
|
1784
|
+
.slice(0, Math.max(1, watch.want))
|
|
1785
|
+
.map((candidate) => ({
|
|
1786
|
+
hash: candidate.hash,
|
|
1787
|
+
addrs: candidate.addrs.map((a) => a.bytes),
|
|
1788
|
+
expiresAt: now + 60_000,
|
|
1789
|
+
}));
|
|
1790
|
+
void this._sendControl(hash, encodeProviderNotify(id.key, entries)).catch(
|
|
1791
|
+
dontThrowIfDeliveryError,
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
this.pruneProviderWatchersIfEmpty(id.suffixKey);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1549
1797
|
private getProviderNamespaceId(namespace: string): ProviderNamespaceId {
|
|
1550
1798
|
const key = sha256Sync(textEncoder.encode(`provider|${namespace}`));
|
|
1551
1799
|
const suffixKey = toBase64(key.subarray(0, 24));
|
|
1800
|
+
this.providerNamespaceBySuffixKey.set(suffixKey, namespace);
|
|
1552
1801
|
return { namespace, key, suffixKey };
|
|
1553
1802
|
}
|
|
1554
1803
|
|
|
1804
|
+
private getProviderNamespaceIdFromKey(
|
|
1805
|
+
key: Uint8Array,
|
|
1806
|
+
suffixKey = toBase64(key.subarray(0, 24)),
|
|
1807
|
+
): ProviderNamespaceId {
|
|
1808
|
+
return {
|
|
1809
|
+
namespace: this.providerNamespaceBySuffixKey.get(suffixKey) ?? "",
|
|
1810
|
+
key,
|
|
1811
|
+
suffixKey,
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1555
1815
|
public provide(
|
|
1556
1816
|
namespace: string,
|
|
1557
1817
|
options: FanoutProviderAnnounceOptions = {},
|
|
@@ -1770,27 +2030,7 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
1770
2030
|
return connected.concat(unconnected).slice(0, want);
|
|
1771
2031
|
};
|
|
1772
2032
|
|
|
1773
|
-
const cached
|
|
1774
|
-
const byPeer = this.providerBySuffixKey.get(id.suffixKey);
|
|
1775
|
-
if (byPeer) {
|
|
1776
|
-
for (const [hash, e] of byPeer) {
|
|
1777
|
-
if (e.expiresAt <= now) {
|
|
1778
|
-
byPeer.delete(hash);
|
|
1779
|
-
continue;
|
|
1780
|
-
}
|
|
1781
|
-
if (hash === this.publicKeyHash) continue;
|
|
1782
|
-
const addrs: Multiaddr[] = [];
|
|
1783
|
-
for (const a of e.addrs) {
|
|
1784
|
-
try {
|
|
1785
|
-
addrs.push(multiaddr(a));
|
|
1786
|
-
} catch {
|
|
1787
|
-
// ignore invalid
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
cached.push({ hash, addrs });
|
|
1791
|
-
}
|
|
1792
|
-
this.pruneProviderNamespaceIfEmpty(id.suffixKey);
|
|
1793
|
-
}
|
|
2033
|
+
const cached = this.getProviderCandidatesFromCache(id, { now });
|
|
1794
2034
|
|
|
1795
2035
|
// If the cache is warm, avoid tracker queries on hot paths.
|
|
1796
2036
|
if (cached.length >= want) {
|
|
@@ -1876,29 +2116,14 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
1876
2116
|
|
|
1877
2117
|
// Cache (best-effort) to avoid repeated tracker lookups.
|
|
1878
2118
|
if (cacheTtlMs > 0) {
|
|
1879
|
-
|
|
1880
|
-
let cache = this.providerBySuffixKey.get(id.suffixKey);
|
|
1881
|
-
if (!cache) {
|
|
1882
|
-
cache = new Map<string, ProviderEntry>();
|
|
1883
|
-
this.providerBySuffixKey.set(id.suffixKey, cache);
|
|
1884
|
-
}
|
|
1885
|
-
for (const c of deduped) {
|
|
1886
|
-
cache.delete(c.hash);
|
|
1887
|
-
cache.set(c.hash, {
|
|
1888
|
-
hash: c.hash,
|
|
1889
|
-
addrs: c.addrs.map((a) => a.bytes),
|
|
1890
|
-
expiresAt: exp,
|
|
1891
|
-
});
|
|
1892
|
-
}
|
|
1893
|
-
while (cache.size > PROVIDER_DIRECTORY_MAX_ENTRIES) {
|
|
1894
|
-
const oldest = cache.keys().next().value as string | undefined;
|
|
1895
|
-
if (!oldest) break;
|
|
1896
|
-
cache.delete(oldest);
|
|
1897
|
-
}
|
|
1898
|
-
this.touchProviderNamespace(id.suffixKey);
|
|
2119
|
+
this.rememberProviderCandidates(id, deduped, Date.now() + cacheTtlMs);
|
|
1899
2120
|
}
|
|
1900
2121
|
|
|
1901
|
-
|
|
2122
|
+
const ordered = orderCandidates(deduped);
|
|
2123
|
+
if (ordered.length > 0) {
|
|
2124
|
+
this.emitProviderUpdate(id, ordered);
|
|
2125
|
+
}
|
|
2126
|
+
return ordered;
|
|
1902
2127
|
} finally {
|
|
1903
2128
|
(signal as any)?.clear?.();
|
|
1904
2129
|
}
|
|
@@ -1912,6 +2137,134 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
1912
2137
|
return candidates.map((c) => c.hash);
|
|
1913
2138
|
}
|
|
1914
2139
|
|
|
2140
|
+
public watchProviders(
|
|
2141
|
+
namespace: string,
|
|
2142
|
+
options: FanoutProviderWatchOptions,
|
|
2143
|
+
): FanoutProviderHandle {
|
|
2144
|
+
if (!this.started) {
|
|
2145
|
+
throw new Error("FanoutTree must be started before watching providers");
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
const id = this.getProviderNamespaceId(namespace);
|
|
2149
|
+
const ttlMs = Math.max(1_000, Math.floor(options.ttlMs ?? 10_000));
|
|
2150
|
+
const renewIntervalMs = Math.max(
|
|
2151
|
+
250,
|
|
2152
|
+
Math.min(
|
|
2153
|
+
ttlMs,
|
|
2154
|
+
Math.floor(options.renewIntervalMs ?? Math.max(500, ttlMs / 2)),
|
|
2155
|
+
),
|
|
2156
|
+
);
|
|
2157
|
+
const watch: ProviderWatchState = {
|
|
2158
|
+
id,
|
|
2159
|
+
want: Math.max(1, Math.floor(options.want ?? 8)),
|
|
2160
|
+
ttlMs,
|
|
2161
|
+
renewIntervalMs,
|
|
2162
|
+
bootstrapOverride:
|
|
2163
|
+
options.bootstrap && options.bootstrap.length > 0
|
|
2164
|
+
? options.bootstrap
|
|
2165
|
+
.map((a) => (typeof a === "string" ? multiaddr(a) : a))
|
|
2166
|
+
.filter((a) => Boolean(a))
|
|
2167
|
+
: undefined,
|
|
2168
|
+
bootstrapDialTimeoutMs: Math.max(
|
|
2169
|
+
0,
|
|
2170
|
+
Math.floor(options.bootstrapDialTimeoutMs ?? 2_000),
|
|
2171
|
+
),
|
|
2172
|
+
bootstrapMaxPeers: Math.max(
|
|
2173
|
+
0,
|
|
2174
|
+
Math.floor(options.bootstrapMaxPeers ?? 0),
|
|
2175
|
+
),
|
|
2176
|
+
onProviders: options.onProviders,
|
|
2177
|
+
signal: options.signal,
|
|
2178
|
+
closed: false,
|
|
2179
|
+
trackerPeers: [],
|
|
2180
|
+
};
|
|
2181
|
+
|
|
2182
|
+
let localWatches = this.providerWatchesBySuffixKey.get(id.suffixKey);
|
|
2183
|
+
if (!localWatches) {
|
|
2184
|
+
localWatches = new Set<ProviderWatchState>();
|
|
2185
|
+
this.providerWatchesBySuffixKey.set(id.suffixKey, localWatches);
|
|
2186
|
+
}
|
|
2187
|
+
localWatches.add(watch);
|
|
2188
|
+
|
|
2189
|
+
const cached = this.getProviderCandidatesFromCache(id, {
|
|
2190
|
+
want: watch.want,
|
|
2191
|
+
});
|
|
2192
|
+
if (cached.length > 0) {
|
|
2193
|
+
queueMicrotask(() => {
|
|
2194
|
+
if (watch.closed) return;
|
|
2195
|
+
try {
|
|
2196
|
+
watch.onProviders(cached.slice(0, watch.want));
|
|
2197
|
+
} catch {
|
|
2198
|
+
// ignore consumer callback failures
|
|
2199
|
+
}
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
const close = () => {
|
|
2204
|
+
if (watch.closed) return;
|
|
2205
|
+
watch.closed = true;
|
|
2206
|
+
localWatches?.delete(watch);
|
|
2207
|
+
if (localWatches && localWatches.size === 0) {
|
|
2208
|
+
this.providerWatchesBySuffixKey.delete(id.suffixKey);
|
|
2209
|
+
}
|
|
2210
|
+
for (const trackerHash of watch.trackerPeers) {
|
|
2211
|
+
void this._sendControl(
|
|
2212
|
+
trackerHash,
|
|
2213
|
+
encodeProviderUnsubscribe(id.key),
|
|
2214
|
+
).catch(dontThrowIfDeliveryError);
|
|
2215
|
+
}
|
|
2216
|
+
watch.trackerPeers = [];
|
|
2217
|
+
};
|
|
2218
|
+
|
|
2219
|
+
const onAbort = () => close();
|
|
2220
|
+
watch.signal?.addEventListener("abort", onAbort, { once: true });
|
|
2221
|
+
|
|
2222
|
+
const loopSignal = watch.signal
|
|
2223
|
+
? anySignal([this.closeController.signal, watch.signal])
|
|
2224
|
+
: this.closeController.signal;
|
|
2225
|
+
|
|
2226
|
+
watch.loop = (async () => {
|
|
2227
|
+
for (;;) {
|
|
2228
|
+
if (watch.closed || loopSignal.aborted) return;
|
|
2229
|
+
try {
|
|
2230
|
+
const bootstraps = watch.bootstrapOverride ?? this.bootstraps;
|
|
2231
|
+
const trackerPeers =
|
|
2232
|
+
bootstraps.length > 0
|
|
2233
|
+
? await this.ensureBootstrapPeers(
|
|
2234
|
+
bootstraps,
|
|
2235
|
+
watch.bootstrapDialTimeoutMs,
|
|
2236
|
+
loopSignal,
|
|
2237
|
+
watch.bootstrapMaxPeers,
|
|
2238
|
+
)
|
|
2239
|
+
: [];
|
|
2240
|
+
watch.trackerPeers = trackerPeers;
|
|
2241
|
+
for (const trackerHash of trackerPeers) {
|
|
2242
|
+
void this._sendControl(
|
|
2243
|
+
trackerHash,
|
|
2244
|
+
encodeProviderSubscribe(id.key, watch.want, watch.ttlMs),
|
|
2245
|
+
).catch(dontThrowIfDeliveryError);
|
|
2246
|
+
}
|
|
2247
|
+
} catch (error) {
|
|
2248
|
+
if (
|
|
2249
|
+
error instanceof AbortError ||
|
|
2250
|
+
(error && typeof error === "object" && "name" in error && error.name === "AbortError")
|
|
2251
|
+
) {
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
await delay(watch.renewIntervalMs, { signal: loopSignal }).catch(() => {});
|
|
2256
|
+
}
|
|
2257
|
+
})();
|
|
2258
|
+
|
|
2259
|
+
void watch.loop.finally(() => {
|
|
2260
|
+
watch.signal?.removeEventListener("abort", onAbort);
|
|
2261
|
+
close();
|
|
2262
|
+
(loopSignal as any)?.clear?.();
|
|
2263
|
+
});
|
|
2264
|
+
|
|
2265
|
+
return { close };
|
|
2266
|
+
}
|
|
2267
|
+
|
|
1915
2268
|
public getChannelId(topic: string, root: string): FanoutTreeChannelId {
|
|
1916
2269
|
const key = sha256Sync(textEncoder.encode(`fanout-tree|${root}|${topic}`));
|
|
1917
2270
|
const suffixKey = toBase64(key.subarray(0, 24));
|
|
@@ -3147,10 +3500,22 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
3147
3500
|
root: string,
|
|
3148
3501
|
payload: Uint8Array,
|
|
3149
3502
|
): Promise<boolean> {
|
|
3503
|
+
const id = this.getChannelId(topic, root);
|
|
3504
|
+
const ch = this.channelsBySuffixKey.get(id.suffixKey);
|
|
3505
|
+
if (!ch || ch.closed) {
|
|
3506
|
+
return false;
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3150
3509
|
try {
|
|
3151
3510
|
await this.publishToChannel(topic, root, payload);
|
|
3152
3511
|
return true;
|
|
3153
3512
|
} catch (error) {
|
|
3513
|
+
if (
|
|
3514
|
+
error instanceof Error &&
|
|
3515
|
+
error.message.startsWith(`Channel not open: ${topic} (${root})`)
|
|
3516
|
+
) {
|
|
3517
|
+
return false;
|
|
3518
|
+
}
|
|
3154
3519
|
dontThrowIfDeliveryError(error);
|
|
3155
3520
|
return false;
|
|
3156
3521
|
}
|
|
@@ -3313,6 +3678,9 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
3313
3678
|
case MSG_PROVIDER_ANNOUNCE:
|
|
3314
3679
|
case MSG_PROVIDER_QUERY:
|
|
3315
3680
|
case MSG_PROVIDER_REPLY:
|
|
3681
|
+
case MSG_PROVIDER_SUBSCRIBE:
|
|
3682
|
+
case MSG_PROVIDER_UNSUBSCRIBE:
|
|
3683
|
+
case MSG_PROVIDER_NOTIFY:
|
|
3316
3684
|
m.controlBytesSentTracker += sentBytes;
|
|
3317
3685
|
break;
|
|
3318
3686
|
default:
|
|
@@ -3376,6 +3744,9 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
3376
3744
|
case MSG_PROVIDER_ANNOUNCE:
|
|
3377
3745
|
case MSG_PROVIDER_QUERY:
|
|
3378
3746
|
case MSG_PROVIDER_REPLY:
|
|
3747
|
+
case MSG_PROVIDER_SUBSCRIBE:
|
|
3748
|
+
case MSG_PROVIDER_UNSUBSCRIBE:
|
|
3749
|
+
case MSG_PROVIDER_NOTIFY:
|
|
3379
3750
|
m.controlBytesReceivedTracker += bytesReceived;
|
|
3380
3751
|
break;
|
|
3381
3752
|
default:
|
|
@@ -5216,7 +5587,10 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
5216
5587
|
kind === MSG_TRACKER_FEEDBACK ||
|
|
5217
5588
|
kind === MSG_PROVIDER_ANNOUNCE ||
|
|
5218
5589
|
kind === MSG_PROVIDER_QUERY ||
|
|
5219
|
-
kind === MSG_PROVIDER_REPLY
|
|
5590
|
+
kind === MSG_PROVIDER_REPLY ||
|
|
5591
|
+
kind === MSG_PROVIDER_SUBSCRIBE ||
|
|
5592
|
+
kind === MSG_PROVIDER_UNSUBSCRIBE ||
|
|
5593
|
+
kind === MSG_PROVIDER_NOTIFY
|
|
5220
5594
|
) {
|
|
5221
5595
|
if (data.length < 1 + 32) return false;
|
|
5222
5596
|
const channelKey = data.subarray(1, 33);
|
|
@@ -5386,6 +5760,7 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
5386
5760
|
|
|
5387
5761
|
if (kind === MSG_PROVIDER_ANNOUNCE) {
|
|
5388
5762
|
if (data.length < 1 + 32 + 4 + 1) return false;
|
|
5763
|
+
const providerId = this.getProviderNamespaceIdFromKey(channelKey, suffixKey);
|
|
5389
5764
|
const ttlMs = readU32BE(data, 33);
|
|
5390
5765
|
const addrCount = data[37]!;
|
|
5391
5766
|
let offset = 38;
|
|
@@ -5430,6 +5805,11 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
5430
5805
|
if (ttl === 0) {
|
|
5431
5806
|
byPeer.delete(fromHash);
|
|
5432
5807
|
this.pruneProviderNamespaceIfEmpty(suffixKey);
|
|
5808
|
+
this.emitProviderUpdate(
|
|
5809
|
+
providerId,
|
|
5810
|
+
this.getProviderCandidatesFromCache(providerId, { now }),
|
|
5811
|
+
);
|
|
5812
|
+
await this.publishProviderWatchUpdate(providerId);
|
|
5433
5813
|
return true;
|
|
5434
5814
|
}
|
|
5435
5815
|
|
|
@@ -5446,6 +5826,11 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
5446
5826
|
byPeer.delete(oldest);
|
|
5447
5827
|
}
|
|
5448
5828
|
this.pruneProviderNamespaceIfEmpty(suffixKey);
|
|
5829
|
+
this.emitProviderUpdate(
|
|
5830
|
+
providerId,
|
|
5831
|
+
this.getProviderCandidatesFromCache(providerId, { now }),
|
|
5832
|
+
);
|
|
5833
|
+
await this.publishProviderWatchUpdate(providerId);
|
|
5449
5834
|
return true;
|
|
5450
5835
|
}
|
|
5451
5836
|
|
|
@@ -5499,6 +5884,34 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
5499
5884
|
return true;
|
|
5500
5885
|
}
|
|
5501
5886
|
|
|
5887
|
+
if (kind === MSG_PROVIDER_SUBSCRIBE) {
|
|
5888
|
+
if (data.length < 1 + 32 + 2 + 4) return false;
|
|
5889
|
+
const want = Math.max(1, readU16BE(data, 33));
|
|
5890
|
+
const ttlMs = Math.min(120_000, Math.max(1_000, readU32BE(data, 35)));
|
|
5891
|
+
const now = Date.now();
|
|
5892
|
+
let watchers = this.providerWatchersBySuffixKey.get(suffixKey);
|
|
5893
|
+
if (!watchers) {
|
|
5894
|
+
watchers = new Map<string, ProviderWatchRegistration>();
|
|
5895
|
+
this.providerWatchersBySuffixKey.set(suffixKey, watchers);
|
|
5896
|
+
}
|
|
5897
|
+
watchers.set(fromHash, {
|
|
5898
|
+
hash: fromHash,
|
|
5899
|
+
want,
|
|
5900
|
+
expiresAt: now + ttlMs,
|
|
5901
|
+
});
|
|
5902
|
+
await this.publishProviderWatchUpdate(
|
|
5903
|
+
this.getProviderNamespaceIdFromKey(channelKey, suffixKey),
|
|
5904
|
+
);
|
|
5905
|
+
return true;
|
|
5906
|
+
}
|
|
5907
|
+
|
|
5908
|
+
if (kind === MSG_PROVIDER_UNSUBSCRIBE) {
|
|
5909
|
+
const watchers = this.providerWatchersBySuffixKey.get(suffixKey);
|
|
5910
|
+
watchers?.delete(fromHash);
|
|
5911
|
+
this.pruneProviderWatchersIfEmpty(suffixKey);
|
|
5912
|
+
return true;
|
|
5913
|
+
}
|
|
5914
|
+
|
|
5502
5915
|
if (kind === MSG_PROVIDER_REPLY) {
|
|
5503
5916
|
if (data.length < 1 + 32 + 4 + 1) return false;
|
|
5504
5917
|
const reqId = readU32BE(data, 33);
|
|
@@ -5509,59 +5922,41 @@ export class FanoutTree extends DirectStream<FanoutTreeEvents> {
|
|
|
5509
5922
|
pendingByReq!.delete(reqId);
|
|
5510
5923
|
|
|
5511
5924
|
const count = data[37]!;
|
|
5512
|
-
let offset = 38;
|
|
5513
|
-
const candidates: FanoutProviderCandidate[] = [];
|
|
5514
|
-
const max = Math.min(count, 255);
|
|
5515
|
-
|
|
5516
5925
|
const now = Date.now();
|
|
5517
5926
|
this.touchProviderNamespace(suffixKey, now);
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5927
|
+
const { providers: candidates } = decodeProviderEntries(data, 38, count);
|
|
5928
|
+
this.rememberProviderCandidates(
|
|
5929
|
+
this.getProviderNamespaceIdFromKey(channelKey, suffixKey),
|
|
5930
|
+
candidates,
|
|
5931
|
+
now + 60_000,
|
|
5932
|
+
);
|
|
5933
|
+
this.pruneProviderNamespaceIfEmpty(suffixKey);
|
|
5934
|
+
if (candidates.length > 0) {
|
|
5935
|
+
this.emitProviderUpdate(
|
|
5936
|
+
this.getProviderNamespaceIdFromKey(channelKey, suffixKey),
|
|
5937
|
+
candidates,
|
|
5938
|
+
);
|
|
5522
5939
|
}
|
|
5940
|
+
pending.resolve(candidates);
|
|
5941
|
+
return true;
|
|
5942
|
+
}
|
|
5523
5943
|
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
if (offset + len > data.length) break;
|
|
5540
|
-
const bytes = data.subarray(offset, offset + len);
|
|
5541
|
-
offset += len;
|
|
5542
|
-
addrBytes.push(bytes);
|
|
5543
|
-
try {
|
|
5544
|
-
addrs.push(multiaddr(bytes));
|
|
5545
|
-
} catch {
|
|
5546
|
-
// ignore invalid multiaddrs
|
|
5547
|
-
}
|
|
5548
|
-
}
|
|
5549
|
-
candidates.push({ hash, addrs });
|
|
5550
|
-
cache.delete(hash);
|
|
5551
|
-
cache.set(hash, {
|
|
5552
|
-
hash,
|
|
5553
|
-
addrs: addrBytes,
|
|
5554
|
-
expiresAt: now + 60_000,
|
|
5555
|
-
});
|
|
5556
|
-
}
|
|
5557
|
-
while (cache.size > PROVIDER_DIRECTORY_MAX_ENTRIES) {
|
|
5558
|
-
const oldest = cache.keys().next().value as string | undefined;
|
|
5559
|
-
if (!oldest) break;
|
|
5560
|
-
cache.delete(oldest);
|
|
5944
|
+
if (kind === MSG_PROVIDER_NOTIFY) {
|
|
5945
|
+
if (data.length < 1 + 32 + 1) return false;
|
|
5946
|
+
const count = data[33]!;
|
|
5947
|
+
const now = Date.now();
|
|
5948
|
+
const { providers } = decodeProviderEntries(data, 34, count);
|
|
5949
|
+
this.rememberProviderCandidates(
|
|
5950
|
+
this.getProviderNamespaceIdFromKey(channelKey, suffixKey),
|
|
5951
|
+
providers,
|
|
5952
|
+
now + 60_000,
|
|
5953
|
+
);
|
|
5954
|
+
if (providers.length > 0) {
|
|
5955
|
+
this.emitProviderUpdate(
|
|
5956
|
+
this.getProviderNamespaceIdFromKey(channelKey, suffixKey),
|
|
5957
|
+
providers,
|
|
5958
|
+
);
|
|
5561
5959
|
}
|
|
5562
|
-
this.pruneProviderNamespaceIfEmpty(suffixKey);
|
|
5563
|
-
|
|
5564
|
-
pending.resolve(candidates);
|
|
5565
5960
|
return true;
|
|
5566
5961
|
}
|
|
5567
5962
|
|