@peerbit/pubsub 5.1.6 → 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 +357 -101
- package/dist/src/fanout-tree.js.map +1 -1
- package/package.json +4 -4
- package/src/fanout-tree.ts +480 -97
package/dist/src/fanout-tree.js
CHANGED
|
@@ -66,6 +66,9 @@ const MSG_TRACKER_FEEDBACK = 33;
|
|
|
66
66
|
const MSG_PROVIDER_ANNOUNCE = 34;
|
|
67
67
|
const MSG_PROVIDER_QUERY = 35;
|
|
68
68
|
const MSG_PROVIDER_REPLY = 36;
|
|
69
|
+
const MSG_PROVIDER_SUBSCRIBE = 37;
|
|
70
|
+
const MSG_PROVIDER_UNSUBSCRIBE = 38;
|
|
71
|
+
const MSG_PROVIDER_NOTIFY = 39;
|
|
69
72
|
const JOIN_REJECT_NOT_ATTACHED = 1;
|
|
70
73
|
const JOIN_REJECT_NO_CAPACITY = 2;
|
|
71
74
|
const JOIN_REJECT_LOW_BID = 3;
|
|
@@ -541,9 +544,9 @@ const encodeProviderQuery = (namespaceKey, reqId, want, seed) => {
|
|
|
541
544
|
writeU32BE(buf, 39, seed >>> 0);
|
|
542
545
|
return buf;
|
|
543
546
|
};
|
|
544
|
-
const
|
|
547
|
+
const encodeProviderEntries = (entries) => {
|
|
545
548
|
const count = Math.max(0, Math.min(255, entries.length));
|
|
546
|
-
let bytes = 1
|
|
549
|
+
let bytes = 1;
|
|
547
550
|
const encoded = [];
|
|
548
551
|
for (let i = 0; i < count; i++) {
|
|
549
552
|
const e = entries[i];
|
|
@@ -557,6 +560,11 @@ const encodeProviderReply = (namespaceKey, reqId, entries) => {
|
|
|
557
560
|
bytes += 2 + a.length;
|
|
558
561
|
encoded.push({ hashBytes, addrs });
|
|
559
562
|
}
|
|
563
|
+
return { bytes, encoded };
|
|
564
|
+
};
|
|
565
|
+
const encodeProviderReply = (namespaceKey, reqId, entries) => {
|
|
566
|
+
const { bytes: entryBytes, encoded } = encodeProviderEntries(entries);
|
|
567
|
+
let bytes = 1 + 32 + 4 + entryBytes;
|
|
560
568
|
const buf = new Uint8Array(bytes);
|
|
561
569
|
buf[0] = MSG_PROVIDER_REPLY;
|
|
562
570
|
buf.set(namespaceKey, 1);
|
|
@@ -577,6 +585,81 @@ const encodeProviderReply = (namespaceKey, reqId, entries) => {
|
|
|
577
585
|
}
|
|
578
586
|
return buf;
|
|
579
587
|
};
|
|
588
|
+
const encodeProviderSubscribe = (namespaceKey, want, ttlMs) => {
|
|
589
|
+
const buf = new Uint8Array(1 + 32 + 2 + 4);
|
|
590
|
+
buf[0] = MSG_PROVIDER_SUBSCRIBE;
|
|
591
|
+
buf.set(namespaceKey, 1);
|
|
592
|
+
writeU16BE(buf, 33, clampU16(want));
|
|
593
|
+
writeU32BE(buf, 35, Math.max(0, Math.floor(ttlMs)) >>> 0);
|
|
594
|
+
return buf;
|
|
595
|
+
};
|
|
596
|
+
const encodeProviderUnsubscribe = (namespaceKey) => {
|
|
597
|
+
const buf = new Uint8Array(1 + 32);
|
|
598
|
+
buf[0] = MSG_PROVIDER_UNSUBSCRIBE;
|
|
599
|
+
buf.set(namespaceKey, 1);
|
|
600
|
+
return buf;
|
|
601
|
+
};
|
|
602
|
+
const encodeProviderNotify = (namespaceKey, entries) => {
|
|
603
|
+
const { bytes: entryBytes, encoded } = encodeProviderEntries(entries);
|
|
604
|
+
let bytes = 1 + 32 + entryBytes;
|
|
605
|
+
const buf = new Uint8Array(bytes);
|
|
606
|
+
buf[0] = MSG_PROVIDER_NOTIFY;
|
|
607
|
+
buf.set(namespaceKey, 1);
|
|
608
|
+
buf[33] = Math.max(0, Math.min(255, encoded.length)) & 0xff;
|
|
609
|
+
let offset = 34;
|
|
610
|
+
for (const e of encoded) {
|
|
611
|
+
buf[offset++] = e.hashBytes.length & 0xff;
|
|
612
|
+
buf.set(e.hashBytes, offset);
|
|
613
|
+
offset += e.hashBytes.length;
|
|
614
|
+
buf[offset++] = Math.max(0, Math.min(255, e.addrs.length)) & 0xff;
|
|
615
|
+
for (const a of e.addrs) {
|
|
616
|
+
writeU16BE(buf, offset, a.length);
|
|
617
|
+
offset += 2;
|
|
618
|
+
buf.set(a, offset);
|
|
619
|
+
offset += a.length;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return buf;
|
|
623
|
+
};
|
|
624
|
+
const decodeProviderEntries = (data, offsetStart, maxCount) => {
|
|
625
|
+
let offset = offsetStart;
|
|
626
|
+
const providers = [];
|
|
627
|
+
const limit = Math.min(maxCount, 255);
|
|
628
|
+
for (let i = 0; i < limit; i++) {
|
|
629
|
+
if (offset + 1 > data.length)
|
|
630
|
+
break;
|
|
631
|
+
const hashLen = data[offset++];
|
|
632
|
+
if (offset + hashLen > data.length)
|
|
633
|
+
break;
|
|
634
|
+
const hash = textDecoder.decode(data.subarray(offset, offset + hashLen));
|
|
635
|
+
offset += hashLen;
|
|
636
|
+
if (offset + 1 > data.length)
|
|
637
|
+
break;
|
|
638
|
+
const addrCount = data[offset++];
|
|
639
|
+
const addrs = [];
|
|
640
|
+
const addrMax = Math.min(addrCount, 16);
|
|
641
|
+
for (let j = 0; j < addrMax; j++) {
|
|
642
|
+
if (offset + 2 > data.length)
|
|
643
|
+
break;
|
|
644
|
+
const len = readU16BE(data, offset);
|
|
645
|
+
offset += 2;
|
|
646
|
+
if (offset + len > data.length)
|
|
647
|
+
break;
|
|
648
|
+
const bytes = data.subarray(offset, offset + len);
|
|
649
|
+
offset += len;
|
|
650
|
+
try {
|
|
651
|
+
addrs.push(multiaddr(bytes));
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
// ignore invalid
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (!hash)
|
|
658
|
+
continue;
|
|
659
|
+
providers.push({ hash, addrs });
|
|
660
|
+
}
|
|
661
|
+
return { providers, offset };
|
|
662
|
+
};
|
|
580
663
|
const createDeferred = () => {
|
|
581
664
|
let resolve;
|
|
582
665
|
let reject;
|
|
@@ -658,6 +741,9 @@ export class FanoutTree extends DirectStream {
|
|
|
658
741
|
trackerNamespaceLru = new Map();
|
|
659
742
|
providerBySuffixKey = new Map();
|
|
660
743
|
providerNamespaceLru = new Map();
|
|
744
|
+
providerNamespaceBySuffixKey = new Map();
|
|
745
|
+
providerWatchersBySuffixKey = new Map();
|
|
746
|
+
providerWatchesBySuffixKey = new Map();
|
|
661
747
|
underlayPeerDisconnectHandler;
|
|
662
748
|
pendingProviderQueryBySuffixKey = new Map();
|
|
663
749
|
providerAnnounceBySuffixKey = new Map();
|
|
@@ -744,6 +830,7 @@ export class FanoutTree extends DirectStream {
|
|
|
744
830
|
break;
|
|
745
831
|
this.providerNamespaceLru.delete(oldest);
|
|
746
832
|
this.providerBySuffixKey.delete(oldest);
|
|
833
|
+
this.providerNamespaceBySuffixKey.delete(oldest);
|
|
747
834
|
this.pendingProviderQueryBySuffixKey.delete(oldest);
|
|
748
835
|
}
|
|
749
836
|
}
|
|
@@ -755,11 +842,122 @@ export class FanoutTree extends DirectStream {
|
|
|
755
842
|
this.pendingProviderQueryBySuffixKey.delete(suffixKey);
|
|
756
843
|
}
|
|
757
844
|
}
|
|
845
|
+
pruneProviderWatchersIfEmpty(suffixKey) {
|
|
846
|
+
const watchers = this.providerWatchersBySuffixKey.get(suffixKey);
|
|
847
|
+
if (watchers && watchers.size === 0) {
|
|
848
|
+
this.providerWatchersBySuffixKey.delete(suffixKey);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
getProviderCandidatesFromCache(id, options) {
|
|
852
|
+
const now = options?.now ?? Date.now();
|
|
853
|
+
const cached = [];
|
|
854
|
+
const byPeer = this.providerBySuffixKey.get(id.suffixKey);
|
|
855
|
+
if (byPeer) {
|
|
856
|
+
for (const [hash, e] of byPeer) {
|
|
857
|
+
if (e.expiresAt <= now) {
|
|
858
|
+
byPeer.delete(hash);
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
if (hash === this.publicKeyHash)
|
|
862
|
+
continue;
|
|
863
|
+
const addrs = [];
|
|
864
|
+
for (const a of e.addrs) {
|
|
865
|
+
try {
|
|
866
|
+
addrs.push(multiaddr(a));
|
|
867
|
+
}
|
|
868
|
+
catch {
|
|
869
|
+
// ignore invalid
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
cached.push({ hash, addrs });
|
|
873
|
+
if (options?.want && cached.length >= options.want)
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
this.pruneProviderNamespaceIfEmpty(id.suffixKey);
|
|
877
|
+
}
|
|
878
|
+
return cached;
|
|
879
|
+
}
|
|
880
|
+
rememberProviderCandidates(id, candidates, expiresAt) {
|
|
881
|
+
if (candidates.length === 0)
|
|
882
|
+
return;
|
|
883
|
+
let cache = this.providerBySuffixKey.get(id.suffixKey);
|
|
884
|
+
if (!cache) {
|
|
885
|
+
cache = new Map();
|
|
886
|
+
this.providerBySuffixKey.set(id.suffixKey, cache);
|
|
887
|
+
}
|
|
888
|
+
for (const c of candidates) {
|
|
889
|
+
cache.delete(c.hash);
|
|
890
|
+
cache.set(c.hash, {
|
|
891
|
+
hash: c.hash,
|
|
892
|
+
addrs: c.addrs.map((a) => a.bytes),
|
|
893
|
+
expiresAt,
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
while (cache.size > PROVIDER_DIRECTORY_MAX_ENTRIES) {
|
|
897
|
+
const oldest = cache.keys().next().value;
|
|
898
|
+
if (!oldest)
|
|
899
|
+
break;
|
|
900
|
+
cache.delete(oldest);
|
|
901
|
+
}
|
|
902
|
+
this.touchProviderNamespace(id.suffixKey);
|
|
903
|
+
}
|
|
904
|
+
emitProviderUpdate(id, providers) {
|
|
905
|
+
if (providers.length === 0)
|
|
906
|
+
return;
|
|
907
|
+
this.dispatchEvent(new CustomEvent("fanout:provider", {
|
|
908
|
+
detail: { namespace: id.namespace, providers },
|
|
909
|
+
}));
|
|
910
|
+
const localWatches = this.providerWatchesBySuffixKey.get(id.suffixKey);
|
|
911
|
+
if (!localWatches || localWatches.size === 0)
|
|
912
|
+
return;
|
|
913
|
+
for (const watch of localWatches) {
|
|
914
|
+
if (watch.closed)
|
|
915
|
+
continue;
|
|
916
|
+
try {
|
|
917
|
+
watch.onProviders(providers.slice(0, watch.want));
|
|
918
|
+
}
|
|
919
|
+
catch {
|
|
920
|
+
// ignore consumer callback failures
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
async publishProviderWatchUpdate(id) {
|
|
925
|
+
const watchers = this.providerWatchersBySuffixKey.get(id.suffixKey);
|
|
926
|
+
if (!watchers || watchers.size === 0)
|
|
927
|
+
return;
|
|
928
|
+
const now = Date.now();
|
|
929
|
+
const cached = this.getProviderCandidatesFromCache(id, { now });
|
|
930
|
+
const byPeer = new Map(watchers);
|
|
931
|
+
for (const [hash, watch] of byPeer) {
|
|
932
|
+
if (watch.expiresAt <= now) {
|
|
933
|
+
watchers.delete(hash);
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
const entries = cached
|
|
937
|
+
.filter((candidate) => candidate.hash !== hash)
|
|
938
|
+
.slice(0, Math.max(1, watch.want))
|
|
939
|
+
.map((candidate) => ({
|
|
940
|
+
hash: candidate.hash,
|
|
941
|
+
addrs: candidate.addrs.map((a) => a.bytes),
|
|
942
|
+
expiresAt: now + 60_000,
|
|
943
|
+
}));
|
|
944
|
+
void this._sendControl(hash, encodeProviderNotify(id.key, entries)).catch(dontThrowIfDeliveryError);
|
|
945
|
+
}
|
|
946
|
+
this.pruneProviderWatchersIfEmpty(id.suffixKey);
|
|
947
|
+
}
|
|
758
948
|
getProviderNamespaceId(namespace) {
|
|
759
949
|
const key = sha256Sync(textEncoder.encode(`provider|${namespace}`));
|
|
760
950
|
const suffixKey = toBase64(key.subarray(0, 24));
|
|
951
|
+
this.providerNamespaceBySuffixKey.set(suffixKey, namespace);
|
|
761
952
|
return { namespace, key, suffixKey };
|
|
762
953
|
}
|
|
954
|
+
getProviderNamespaceIdFromKey(key, suffixKey = toBase64(key.subarray(0, 24))) {
|
|
955
|
+
return {
|
|
956
|
+
namespace: this.providerNamespaceBySuffixKey.get(suffixKey) ?? "",
|
|
957
|
+
key,
|
|
958
|
+
suffixKey,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
763
961
|
provide(namespace, options = {}) {
|
|
764
962
|
if (!this.started) {
|
|
765
963
|
throw new Error("FanoutTree must be started before providing");
|
|
@@ -927,29 +1125,7 @@ export class FanoutTree extends DirectStream {
|
|
|
927
1125
|
shuffleInPlace(unconnected, seed ? (seed ^ 0x9e3779b9) >>> 0 : 0);
|
|
928
1126
|
return connected.concat(unconnected).slice(0, want);
|
|
929
1127
|
};
|
|
930
|
-
const cached =
|
|
931
|
-
const byPeer = this.providerBySuffixKey.get(id.suffixKey);
|
|
932
|
-
if (byPeer) {
|
|
933
|
-
for (const [hash, e] of byPeer) {
|
|
934
|
-
if (e.expiresAt <= now) {
|
|
935
|
-
byPeer.delete(hash);
|
|
936
|
-
continue;
|
|
937
|
-
}
|
|
938
|
-
if (hash === this.publicKeyHash)
|
|
939
|
-
continue;
|
|
940
|
-
const addrs = [];
|
|
941
|
-
for (const a of e.addrs) {
|
|
942
|
-
try {
|
|
943
|
-
addrs.push(multiaddr(a));
|
|
944
|
-
}
|
|
945
|
-
catch {
|
|
946
|
-
// ignore invalid
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
cached.push({ hash, addrs });
|
|
950
|
-
}
|
|
951
|
-
this.pruneProviderNamespaceIfEmpty(id.suffixKey);
|
|
952
|
-
}
|
|
1128
|
+
const cached = this.getProviderCandidatesFromCache(id, { now });
|
|
953
1129
|
// If the cache is warm, avoid tracker queries on hot paths.
|
|
954
1130
|
if (cached.length >= want) {
|
|
955
1131
|
return orderCandidates(cached);
|
|
@@ -1005,29 +1181,13 @@ export class FanoutTree extends DirectStream {
|
|
|
1005
1181
|
});
|
|
1006
1182
|
// Cache (best-effort) to avoid repeated tracker lookups.
|
|
1007
1183
|
if (cacheTtlMs > 0) {
|
|
1008
|
-
|
|
1009
|
-
let cache = this.providerBySuffixKey.get(id.suffixKey);
|
|
1010
|
-
if (!cache) {
|
|
1011
|
-
cache = new Map();
|
|
1012
|
-
this.providerBySuffixKey.set(id.suffixKey, cache);
|
|
1013
|
-
}
|
|
1014
|
-
for (const c of deduped) {
|
|
1015
|
-
cache.delete(c.hash);
|
|
1016
|
-
cache.set(c.hash, {
|
|
1017
|
-
hash: c.hash,
|
|
1018
|
-
addrs: c.addrs.map((a) => a.bytes),
|
|
1019
|
-
expiresAt: exp,
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
while (cache.size > PROVIDER_DIRECTORY_MAX_ENTRIES) {
|
|
1023
|
-
const oldest = cache.keys().next().value;
|
|
1024
|
-
if (!oldest)
|
|
1025
|
-
break;
|
|
1026
|
-
cache.delete(oldest);
|
|
1027
|
-
}
|
|
1028
|
-
this.touchProviderNamespace(id.suffixKey);
|
|
1184
|
+
this.rememberProviderCandidates(id, deduped, Date.now() + cacheTtlMs);
|
|
1029
1185
|
}
|
|
1030
|
-
|
|
1186
|
+
const ordered = orderCandidates(deduped);
|
|
1187
|
+
if (ordered.length > 0) {
|
|
1188
|
+
this.emitProviderUpdate(id, ordered);
|
|
1189
|
+
}
|
|
1190
|
+
return ordered;
|
|
1031
1191
|
}
|
|
1032
1192
|
finally {
|
|
1033
1193
|
signal?.clear?.();
|
|
@@ -1037,6 +1197,99 @@ export class FanoutTree extends DirectStream {
|
|
|
1037
1197
|
const candidates = await this.queryProviderCandidates(namespace, options);
|
|
1038
1198
|
return candidates.map((c) => c.hash);
|
|
1039
1199
|
}
|
|
1200
|
+
watchProviders(namespace, options) {
|
|
1201
|
+
if (!this.started) {
|
|
1202
|
+
throw new Error("FanoutTree must be started before watching providers");
|
|
1203
|
+
}
|
|
1204
|
+
const id = this.getProviderNamespaceId(namespace);
|
|
1205
|
+
const ttlMs = Math.max(1_000, Math.floor(options.ttlMs ?? 10_000));
|
|
1206
|
+
const renewIntervalMs = Math.max(250, Math.min(ttlMs, Math.floor(options.renewIntervalMs ?? Math.max(500, ttlMs / 2))));
|
|
1207
|
+
const watch = {
|
|
1208
|
+
id,
|
|
1209
|
+
want: Math.max(1, Math.floor(options.want ?? 8)),
|
|
1210
|
+
ttlMs,
|
|
1211
|
+
renewIntervalMs,
|
|
1212
|
+
bootstrapOverride: options.bootstrap && options.bootstrap.length > 0
|
|
1213
|
+
? options.bootstrap
|
|
1214
|
+
.map((a) => (typeof a === "string" ? multiaddr(a) : a))
|
|
1215
|
+
.filter((a) => Boolean(a))
|
|
1216
|
+
: undefined,
|
|
1217
|
+
bootstrapDialTimeoutMs: Math.max(0, Math.floor(options.bootstrapDialTimeoutMs ?? 2_000)),
|
|
1218
|
+
bootstrapMaxPeers: Math.max(0, Math.floor(options.bootstrapMaxPeers ?? 0)),
|
|
1219
|
+
onProviders: options.onProviders,
|
|
1220
|
+
signal: options.signal,
|
|
1221
|
+
closed: false,
|
|
1222
|
+
trackerPeers: [],
|
|
1223
|
+
};
|
|
1224
|
+
let localWatches = this.providerWatchesBySuffixKey.get(id.suffixKey);
|
|
1225
|
+
if (!localWatches) {
|
|
1226
|
+
localWatches = new Set();
|
|
1227
|
+
this.providerWatchesBySuffixKey.set(id.suffixKey, localWatches);
|
|
1228
|
+
}
|
|
1229
|
+
localWatches.add(watch);
|
|
1230
|
+
const cached = this.getProviderCandidatesFromCache(id, {
|
|
1231
|
+
want: watch.want,
|
|
1232
|
+
});
|
|
1233
|
+
if (cached.length > 0) {
|
|
1234
|
+
queueMicrotask(() => {
|
|
1235
|
+
if (watch.closed)
|
|
1236
|
+
return;
|
|
1237
|
+
try {
|
|
1238
|
+
watch.onProviders(cached.slice(0, watch.want));
|
|
1239
|
+
}
|
|
1240
|
+
catch {
|
|
1241
|
+
// ignore consumer callback failures
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
const close = () => {
|
|
1246
|
+
if (watch.closed)
|
|
1247
|
+
return;
|
|
1248
|
+
watch.closed = true;
|
|
1249
|
+
localWatches?.delete(watch);
|
|
1250
|
+
if (localWatches && localWatches.size === 0) {
|
|
1251
|
+
this.providerWatchesBySuffixKey.delete(id.suffixKey);
|
|
1252
|
+
}
|
|
1253
|
+
for (const trackerHash of watch.trackerPeers) {
|
|
1254
|
+
void this._sendControl(trackerHash, encodeProviderUnsubscribe(id.key)).catch(dontThrowIfDeliveryError);
|
|
1255
|
+
}
|
|
1256
|
+
watch.trackerPeers = [];
|
|
1257
|
+
};
|
|
1258
|
+
const onAbort = () => close();
|
|
1259
|
+
watch.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1260
|
+
const loopSignal = watch.signal
|
|
1261
|
+
? anySignal([this.closeController.signal, watch.signal])
|
|
1262
|
+
: this.closeController.signal;
|
|
1263
|
+
watch.loop = (async () => {
|
|
1264
|
+
for (;;) {
|
|
1265
|
+
if (watch.closed || loopSignal.aborted)
|
|
1266
|
+
return;
|
|
1267
|
+
try {
|
|
1268
|
+
const bootstraps = watch.bootstrapOverride ?? this.bootstraps;
|
|
1269
|
+
const trackerPeers = bootstraps.length > 0
|
|
1270
|
+
? await this.ensureBootstrapPeers(bootstraps, watch.bootstrapDialTimeoutMs, loopSignal, watch.bootstrapMaxPeers)
|
|
1271
|
+
: [];
|
|
1272
|
+
watch.trackerPeers = trackerPeers;
|
|
1273
|
+
for (const trackerHash of trackerPeers) {
|
|
1274
|
+
void this._sendControl(trackerHash, encodeProviderSubscribe(id.key, watch.want, watch.ttlMs)).catch(dontThrowIfDeliveryError);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
catch (error) {
|
|
1278
|
+
if (error instanceof AbortError ||
|
|
1279
|
+
(error && typeof error === "object" && "name" in error && error.name === "AbortError")) {
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
await delay(watch.renewIntervalMs, { signal: loopSignal }).catch(() => { });
|
|
1284
|
+
}
|
|
1285
|
+
})();
|
|
1286
|
+
void watch.loop.finally(() => {
|
|
1287
|
+
watch.signal?.removeEventListener("abort", onAbort);
|
|
1288
|
+
close();
|
|
1289
|
+
loopSignal?.clear?.();
|
|
1290
|
+
});
|
|
1291
|
+
return { close };
|
|
1292
|
+
}
|
|
1040
1293
|
getChannelId(topic, root) {
|
|
1041
1294
|
const key = sha256Sync(textEncoder.encode(`fanout-tree|${root}|${topic}`));
|
|
1042
1295
|
const suffixKey = toBase64(key.subarray(0, 24));
|
|
@@ -2246,6 +2499,9 @@ export class FanoutTree extends DirectStream {
|
|
|
2246
2499
|
case MSG_PROVIDER_ANNOUNCE:
|
|
2247
2500
|
case MSG_PROVIDER_QUERY:
|
|
2248
2501
|
case MSG_PROVIDER_REPLY:
|
|
2502
|
+
case MSG_PROVIDER_SUBSCRIBE:
|
|
2503
|
+
case MSG_PROVIDER_UNSUBSCRIBE:
|
|
2504
|
+
case MSG_PROVIDER_NOTIFY:
|
|
2249
2505
|
m.controlBytesSentTracker += sentBytes;
|
|
2250
2506
|
break;
|
|
2251
2507
|
default:
|
|
@@ -2308,6 +2564,9 @@ export class FanoutTree extends DirectStream {
|
|
|
2308
2564
|
case MSG_PROVIDER_ANNOUNCE:
|
|
2309
2565
|
case MSG_PROVIDER_QUERY:
|
|
2310
2566
|
case MSG_PROVIDER_REPLY:
|
|
2567
|
+
case MSG_PROVIDER_SUBSCRIBE:
|
|
2568
|
+
case MSG_PROVIDER_UNSUBSCRIBE:
|
|
2569
|
+
case MSG_PROVIDER_NOTIFY:
|
|
2311
2570
|
m.controlBytesReceivedTracker += bytesReceived;
|
|
2312
2571
|
break;
|
|
2313
2572
|
default:
|
|
@@ -3945,7 +4204,10 @@ export class FanoutTree extends DirectStream {
|
|
|
3945
4204
|
kind === MSG_TRACKER_FEEDBACK ||
|
|
3946
4205
|
kind === MSG_PROVIDER_ANNOUNCE ||
|
|
3947
4206
|
kind === MSG_PROVIDER_QUERY ||
|
|
3948
|
-
kind === MSG_PROVIDER_REPLY
|
|
4207
|
+
kind === MSG_PROVIDER_REPLY ||
|
|
4208
|
+
kind === MSG_PROVIDER_SUBSCRIBE ||
|
|
4209
|
+
kind === MSG_PROVIDER_UNSUBSCRIBE ||
|
|
4210
|
+
kind === MSG_PROVIDER_NOTIFY) {
|
|
3949
4211
|
if (data.length < 1 + 32)
|
|
3950
4212
|
return false;
|
|
3951
4213
|
const channelKey = data.subarray(1, 33);
|
|
@@ -4114,6 +4376,7 @@ export class FanoutTree extends DirectStream {
|
|
|
4114
4376
|
if (kind === MSG_PROVIDER_ANNOUNCE) {
|
|
4115
4377
|
if (data.length < 1 + 32 + 4 + 1)
|
|
4116
4378
|
return false;
|
|
4379
|
+
const providerId = this.getProviderNamespaceIdFromKey(channelKey, suffixKey);
|
|
4117
4380
|
const ttlMs = readU32BE(data, 33);
|
|
4118
4381
|
const addrCount = data[37];
|
|
4119
4382
|
let offset = 38;
|
|
@@ -4159,6 +4422,8 @@ export class FanoutTree extends DirectStream {
|
|
|
4159
4422
|
if (ttl === 0) {
|
|
4160
4423
|
byPeer.delete(fromHash);
|
|
4161
4424
|
this.pruneProviderNamespaceIfEmpty(suffixKey);
|
|
4425
|
+
this.emitProviderUpdate(providerId, this.getProviderCandidatesFromCache(providerId, { now }));
|
|
4426
|
+
await this.publishProviderWatchUpdate(providerId);
|
|
4162
4427
|
return true;
|
|
4163
4428
|
}
|
|
4164
4429
|
// LRU by announce freshness, with a hard per-namespace cap.
|
|
@@ -4175,6 +4440,8 @@ export class FanoutTree extends DirectStream {
|
|
|
4175
4440
|
byPeer.delete(oldest);
|
|
4176
4441
|
}
|
|
4177
4442
|
this.pruneProviderNamespaceIfEmpty(suffixKey);
|
|
4443
|
+
this.emitProviderUpdate(providerId, this.getProviderCandidatesFromCache(providerId, { now }));
|
|
4444
|
+
await this.publishProviderWatchUpdate(providerId);
|
|
4178
4445
|
return true;
|
|
4179
4446
|
}
|
|
4180
4447
|
if (kind === MSG_PROVIDER_QUERY) {
|
|
@@ -4226,6 +4493,31 @@ export class FanoutTree extends DirectStream {
|
|
|
4226
4493
|
void this._sendControl(fromHash, encodeProviderReply(channelKey, reqId, picked));
|
|
4227
4494
|
return true;
|
|
4228
4495
|
}
|
|
4496
|
+
if (kind === MSG_PROVIDER_SUBSCRIBE) {
|
|
4497
|
+
if (data.length < 1 + 32 + 2 + 4)
|
|
4498
|
+
return false;
|
|
4499
|
+
const want = Math.max(1, readU16BE(data, 33));
|
|
4500
|
+
const ttlMs = Math.min(120_000, Math.max(1_000, readU32BE(data, 35)));
|
|
4501
|
+
const now = Date.now();
|
|
4502
|
+
let watchers = this.providerWatchersBySuffixKey.get(suffixKey);
|
|
4503
|
+
if (!watchers) {
|
|
4504
|
+
watchers = new Map();
|
|
4505
|
+
this.providerWatchersBySuffixKey.set(suffixKey, watchers);
|
|
4506
|
+
}
|
|
4507
|
+
watchers.set(fromHash, {
|
|
4508
|
+
hash: fromHash,
|
|
4509
|
+
want,
|
|
4510
|
+
expiresAt: now + ttlMs,
|
|
4511
|
+
});
|
|
4512
|
+
await this.publishProviderWatchUpdate(this.getProviderNamespaceIdFromKey(channelKey, suffixKey));
|
|
4513
|
+
return true;
|
|
4514
|
+
}
|
|
4515
|
+
if (kind === MSG_PROVIDER_UNSUBSCRIBE) {
|
|
4516
|
+
const watchers = this.providerWatchersBySuffixKey.get(suffixKey);
|
|
4517
|
+
watchers?.delete(fromHash);
|
|
4518
|
+
this.pruneProviderWatchersIfEmpty(suffixKey);
|
|
4519
|
+
return true;
|
|
4520
|
+
}
|
|
4229
4521
|
if (kind === MSG_PROVIDER_REPLY) {
|
|
4230
4522
|
if (data.length < 1 + 32 + 4 + 1)
|
|
4231
4523
|
return false;
|
|
@@ -4236,65 +4528,29 @@ export class FanoutTree extends DirectStream {
|
|
|
4236
4528
|
return true;
|
|
4237
4529
|
pendingByReq.delete(reqId);
|
|
4238
4530
|
const count = data[37];
|
|
4239
|
-
let offset = 38;
|
|
4240
|
-
const candidates = [];
|
|
4241
|
-
const max = Math.min(count, 255);
|
|
4242
4531
|
const now = Date.now();
|
|
4243
4532
|
this.touchProviderNamespace(suffixKey, now);
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
cache = new Map();
|
|
4247
|
-
this.providerBySuffixKey.set(suffixKey, cache);
|
|
4248
|
-
}
|
|
4249
|
-
for (let i = 0; i < max; i++) {
|
|
4250
|
-
if (offset + 1 > data.length)
|
|
4251
|
-
break;
|
|
4252
|
-
const hashLen = data[offset++];
|
|
4253
|
-
if (offset + hashLen > data.length)
|
|
4254
|
-
break;
|
|
4255
|
-
const hash = textDecoder.decode(data.subarray(offset, offset + hashLen));
|
|
4256
|
-
offset += hashLen;
|
|
4257
|
-
if (offset + 1 > data.length)
|
|
4258
|
-
break;
|
|
4259
|
-
const addrCount = data[offset++];
|
|
4260
|
-
const addrs = [];
|
|
4261
|
-
const addrBytes = [];
|
|
4262
|
-
const addrMax = Math.min(addrCount, 16);
|
|
4263
|
-
for (let j = 0; j < addrMax; j++) {
|
|
4264
|
-
if (offset + 2 > data.length)
|
|
4265
|
-
break;
|
|
4266
|
-
const len = readU16BE(data, offset);
|
|
4267
|
-
offset += 2;
|
|
4268
|
-
if (offset + len > data.length)
|
|
4269
|
-
break;
|
|
4270
|
-
const bytes = data.subarray(offset, offset + len);
|
|
4271
|
-
offset += len;
|
|
4272
|
-
addrBytes.push(bytes);
|
|
4273
|
-
try {
|
|
4274
|
-
addrs.push(multiaddr(bytes));
|
|
4275
|
-
}
|
|
4276
|
-
catch {
|
|
4277
|
-
// ignore invalid multiaddrs
|
|
4278
|
-
}
|
|
4279
|
-
}
|
|
4280
|
-
candidates.push({ hash, addrs });
|
|
4281
|
-
cache.delete(hash);
|
|
4282
|
-
cache.set(hash, {
|
|
4283
|
-
hash,
|
|
4284
|
-
addrs: addrBytes,
|
|
4285
|
-
expiresAt: now + 60_000,
|
|
4286
|
-
});
|
|
4287
|
-
}
|
|
4288
|
-
while (cache.size > PROVIDER_DIRECTORY_MAX_ENTRIES) {
|
|
4289
|
-
const oldest = cache.keys().next().value;
|
|
4290
|
-
if (!oldest)
|
|
4291
|
-
break;
|
|
4292
|
-
cache.delete(oldest);
|
|
4293
|
-
}
|
|
4533
|
+
const { providers: candidates } = decodeProviderEntries(data, 38, count);
|
|
4534
|
+
this.rememberProviderCandidates(this.getProviderNamespaceIdFromKey(channelKey, suffixKey), candidates, now + 60_000);
|
|
4294
4535
|
this.pruneProviderNamespaceIfEmpty(suffixKey);
|
|
4536
|
+
if (candidates.length > 0) {
|
|
4537
|
+
this.emitProviderUpdate(this.getProviderNamespaceIdFromKey(channelKey, suffixKey), candidates);
|
|
4538
|
+
}
|
|
4295
4539
|
pending.resolve(candidates);
|
|
4296
4540
|
return true;
|
|
4297
4541
|
}
|
|
4542
|
+
if (kind === MSG_PROVIDER_NOTIFY) {
|
|
4543
|
+
if (data.length < 1 + 32 + 1)
|
|
4544
|
+
return false;
|
|
4545
|
+
const count = data[33];
|
|
4546
|
+
const now = Date.now();
|
|
4547
|
+
const { providers } = decodeProviderEntries(data, 34, count);
|
|
4548
|
+
this.rememberProviderCandidates(this.getProviderNamespaceIdFromKey(channelKey, suffixKey), providers, now + 60_000);
|
|
4549
|
+
if (providers.length > 0) {
|
|
4550
|
+
this.emitProviderUpdate(this.getProviderNamespaceIdFromKey(channelKey, suffixKey), providers);
|
|
4551
|
+
}
|
|
4552
|
+
return true;
|
|
4553
|
+
}
|
|
4298
4554
|
if (kind === MSG_TRACKER_REPLY) {
|
|
4299
4555
|
if (!ch)
|
|
4300
4556
|
return true;
|