@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.
@@ -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));
@@ -2063,11 +2316,20 @@ export class FanoutTree extends DirectStream {
2063
2316
  await this._sendControl(ch.parent, encodePublishProxy(ch.id.key, payload));
2064
2317
  }
2065
2318
  async publishToChannelMaybe(topic, root, payload) {
2319
+ const id = this.getChannelId(topic, root);
2320
+ const ch = this.channelsBySuffixKey.get(id.suffixKey);
2321
+ if (!ch || ch.closed) {
2322
+ return false;
2323
+ }
2066
2324
  try {
2067
2325
  await this.publishToChannel(topic, root, payload);
2068
2326
  return true;
2069
2327
  }
2070
2328
  catch (error) {
2329
+ if (error instanceof Error &&
2330
+ error.message.startsWith(`Channel not open: ${topic} (${root})`)) {
2331
+ return false;
2332
+ }
2071
2333
  dontThrowIfDeliveryError(error);
2072
2334
  return false;
2073
2335
  }
@@ -2237,6 +2499,9 @@ export class FanoutTree extends DirectStream {
2237
2499
  case MSG_PROVIDER_ANNOUNCE:
2238
2500
  case MSG_PROVIDER_QUERY:
2239
2501
  case MSG_PROVIDER_REPLY:
2502
+ case MSG_PROVIDER_SUBSCRIBE:
2503
+ case MSG_PROVIDER_UNSUBSCRIBE:
2504
+ case MSG_PROVIDER_NOTIFY:
2240
2505
  m.controlBytesSentTracker += sentBytes;
2241
2506
  break;
2242
2507
  default:
@@ -2299,6 +2564,9 @@ export class FanoutTree extends DirectStream {
2299
2564
  case MSG_PROVIDER_ANNOUNCE:
2300
2565
  case MSG_PROVIDER_QUERY:
2301
2566
  case MSG_PROVIDER_REPLY:
2567
+ case MSG_PROVIDER_SUBSCRIBE:
2568
+ case MSG_PROVIDER_UNSUBSCRIBE:
2569
+ case MSG_PROVIDER_NOTIFY:
2302
2570
  m.controlBytesReceivedTracker += bytesReceived;
2303
2571
  break;
2304
2572
  default:
@@ -3936,7 +4204,10 @@ export class FanoutTree extends DirectStream {
3936
4204
  kind === MSG_TRACKER_FEEDBACK ||
3937
4205
  kind === MSG_PROVIDER_ANNOUNCE ||
3938
4206
  kind === MSG_PROVIDER_QUERY ||
3939
- kind === MSG_PROVIDER_REPLY) {
4207
+ kind === MSG_PROVIDER_REPLY ||
4208
+ kind === MSG_PROVIDER_SUBSCRIBE ||
4209
+ kind === MSG_PROVIDER_UNSUBSCRIBE ||
4210
+ kind === MSG_PROVIDER_NOTIFY) {
3940
4211
  if (data.length < 1 + 32)
3941
4212
  return false;
3942
4213
  const channelKey = data.subarray(1, 33);
@@ -4105,6 +4376,7 @@ export class FanoutTree extends DirectStream {
4105
4376
  if (kind === MSG_PROVIDER_ANNOUNCE) {
4106
4377
  if (data.length < 1 + 32 + 4 + 1)
4107
4378
  return false;
4379
+ const providerId = this.getProviderNamespaceIdFromKey(channelKey, suffixKey);
4108
4380
  const ttlMs = readU32BE(data, 33);
4109
4381
  const addrCount = data[37];
4110
4382
  let offset = 38;
@@ -4150,6 +4422,8 @@ export class FanoutTree extends DirectStream {
4150
4422
  if (ttl === 0) {
4151
4423
  byPeer.delete(fromHash);
4152
4424
  this.pruneProviderNamespaceIfEmpty(suffixKey);
4425
+ this.emitProviderUpdate(providerId, this.getProviderCandidatesFromCache(providerId, { now }));
4426
+ await this.publishProviderWatchUpdate(providerId);
4153
4427
  return true;
4154
4428
  }
4155
4429
  // LRU by announce freshness, with a hard per-namespace cap.
@@ -4166,6 +4440,8 @@ export class FanoutTree extends DirectStream {
4166
4440
  byPeer.delete(oldest);
4167
4441
  }
4168
4442
  this.pruneProviderNamespaceIfEmpty(suffixKey);
4443
+ this.emitProviderUpdate(providerId, this.getProviderCandidatesFromCache(providerId, { now }));
4444
+ await this.publishProviderWatchUpdate(providerId);
4169
4445
  return true;
4170
4446
  }
4171
4447
  if (kind === MSG_PROVIDER_QUERY) {
@@ -4217,6 +4493,31 @@ export class FanoutTree extends DirectStream {
4217
4493
  void this._sendControl(fromHash, encodeProviderReply(channelKey, reqId, picked));
4218
4494
  return true;
4219
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
+ }
4220
4521
  if (kind === MSG_PROVIDER_REPLY) {
4221
4522
  if (data.length < 1 + 32 + 4 + 1)
4222
4523
  return false;
@@ -4227,65 +4528,29 @@ export class FanoutTree extends DirectStream {
4227
4528
  return true;
4228
4529
  pendingByReq.delete(reqId);
4229
4530
  const count = data[37];
4230
- let offset = 38;
4231
- const candidates = [];
4232
- const max = Math.min(count, 255);
4233
4531
  const now = Date.now();
4234
4532
  this.touchProviderNamespace(suffixKey, now);
4235
- let cache = this.providerBySuffixKey.get(suffixKey);
4236
- if (!cache) {
4237
- cache = new Map();
4238
- this.providerBySuffixKey.set(suffixKey, cache);
4239
- }
4240
- for (let i = 0; i < max; i++) {
4241
- if (offset + 1 > data.length)
4242
- break;
4243
- const hashLen = data[offset++];
4244
- if (offset + hashLen > data.length)
4245
- break;
4246
- const hash = textDecoder.decode(data.subarray(offset, offset + hashLen));
4247
- offset += hashLen;
4248
- if (offset + 1 > data.length)
4249
- break;
4250
- const addrCount = data[offset++];
4251
- const addrs = [];
4252
- const addrBytes = [];
4253
- const addrMax = Math.min(addrCount, 16);
4254
- for (let j = 0; j < addrMax; j++) {
4255
- if (offset + 2 > data.length)
4256
- break;
4257
- const len = readU16BE(data, offset);
4258
- offset += 2;
4259
- if (offset + len > data.length)
4260
- break;
4261
- const bytes = data.subarray(offset, offset + len);
4262
- offset += len;
4263
- addrBytes.push(bytes);
4264
- try {
4265
- addrs.push(multiaddr(bytes));
4266
- }
4267
- catch {
4268
- // ignore invalid multiaddrs
4269
- }
4270
- }
4271
- candidates.push({ hash, addrs });
4272
- cache.delete(hash);
4273
- cache.set(hash, {
4274
- hash,
4275
- addrs: addrBytes,
4276
- expiresAt: now + 60_000,
4277
- });
4278
- }
4279
- while (cache.size > PROVIDER_DIRECTORY_MAX_ENTRIES) {
4280
- const oldest = cache.keys().next().value;
4281
- if (!oldest)
4282
- break;
4283
- cache.delete(oldest);
4284
- }
4533
+ const { providers: candidates } = decodeProviderEntries(data, 38, count);
4534
+ this.rememberProviderCandidates(this.getProviderNamespaceIdFromKey(channelKey, suffixKey), candidates, now + 60_000);
4285
4535
  this.pruneProviderNamespaceIfEmpty(suffixKey);
4536
+ if (candidates.length > 0) {
4537
+ this.emitProviderUpdate(this.getProviderNamespaceIdFromKey(channelKey, suffixKey), candidates);
4538
+ }
4286
4539
  pending.resolve(candidates);
4287
4540
  return true;
4288
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
+ }
4289
4554
  if (kind === MSG_TRACKER_REPLY) {
4290
4555
  if (!ch)
4291
4556
  return true;