@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.
@@ -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 encodeProviderReply = (namespaceKey, reqId, entries) => {
547
+ const encodeProviderEntries = (entries) => {
545
548
  const count = Math.max(0, Math.min(255, entries.length));
546
- let bytes = 1 + 32 + 4 + 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
- const exp = Date.now() + cacheTtlMs;
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
- return orderCandidates(deduped);
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
- let cache = this.providerBySuffixKey.get(suffixKey);
4245
- if (!cache) {
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;