@hashtree/core 0.1.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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bep52.d.ts +179 -0
  3. package/dist/bep52.d.ts.map +1 -0
  4. package/dist/bep52.js +384 -0
  5. package/dist/bep52.js.map +1 -0
  6. package/dist/builder.d.ts +137 -0
  7. package/dist/builder.d.ts.map +1 -0
  8. package/dist/builder.js +281 -0
  9. package/dist/builder.js.map +1 -0
  10. package/dist/codec.d.ts +37 -0
  11. package/dist/codec.d.ts.map +1 -0
  12. package/dist/codec.js +109 -0
  13. package/dist/codec.js.map +1 -0
  14. package/dist/crypto.d.ts +92 -0
  15. package/dist/crypto.d.ts.map +1 -0
  16. package/dist/crypto.js +212 -0
  17. package/dist/crypto.js.map +1 -0
  18. package/dist/encrypted.d.ts +114 -0
  19. package/dist/encrypted.d.ts.map +1 -0
  20. package/dist/encrypted.js +446 -0
  21. package/dist/encrypted.js.map +1 -0
  22. package/dist/hash.d.ts +14 -0
  23. package/dist/hash.d.ts.map +1 -0
  24. package/dist/hash.js +27 -0
  25. package/dist/hash.js.map +1 -0
  26. package/dist/hashtree.d.ts +237 -0
  27. package/dist/hashtree.d.ts.map +1 -0
  28. package/dist/hashtree.js +557 -0
  29. package/dist/hashtree.js.map +1 -0
  30. package/dist/index.d.ts +27 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +44 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/nhash.d.ts +94 -0
  35. package/dist/nhash.d.ts.map +1 -0
  36. package/dist/nhash.js +293 -0
  37. package/dist/nhash.js.map +1 -0
  38. package/dist/resolver/index.d.ts +5 -0
  39. package/dist/resolver/index.d.ts.map +1 -0
  40. package/dist/resolver/index.js +5 -0
  41. package/dist/resolver/index.js.map +1 -0
  42. package/dist/resolver/nostr.d.ts +82 -0
  43. package/dist/resolver/nostr.d.ts.map +1 -0
  44. package/dist/resolver/nostr.js +868 -0
  45. package/dist/resolver/nostr.js.map +1 -0
  46. package/dist/store/blossom.d.ts +100 -0
  47. package/dist/store/blossom.d.ts.map +1 -0
  48. package/dist/store/blossom.js +355 -0
  49. package/dist/store/blossom.js.map +1 -0
  50. package/dist/store/dexie.d.ts +44 -0
  51. package/dist/store/dexie.d.ts.map +1 -0
  52. package/dist/store/dexie.js +196 -0
  53. package/dist/store/dexie.js.map +1 -0
  54. package/dist/store/fallback.d.ts +40 -0
  55. package/dist/store/fallback.d.ts.map +1 -0
  56. package/dist/store/fallback.js +71 -0
  57. package/dist/store/fallback.js.map +1 -0
  58. package/dist/store/index.d.ts +6 -0
  59. package/dist/store/index.d.ts.map +1 -0
  60. package/dist/store/index.js +6 -0
  61. package/dist/store/index.js.map +1 -0
  62. package/dist/store/memory.d.ts +29 -0
  63. package/dist/store/memory.d.ts.map +1 -0
  64. package/dist/store/memory.js +66 -0
  65. package/dist/store/memory.js.map +1 -0
  66. package/dist/store/opfs.d.ts +56 -0
  67. package/dist/store/opfs.d.ts.map +1 -0
  68. package/dist/store/opfs.js +200 -0
  69. package/dist/store/opfs.js.map +1 -0
  70. package/dist/streaming.d.ts +74 -0
  71. package/dist/streaming.d.ts.map +1 -0
  72. package/dist/streaming.js +199 -0
  73. package/dist/streaming.js.map +1 -0
  74. package/dist/tree/create.d.ts +35 -0
  75. package/dist/tree/create.d.ts.map +1 -0
  76. package/dist/tree/create.js +90 -0
  77. package/dist/tree/create.js.map +1 -0
  78. package/dist/tree/edit.d.ts +28 -0
  79. package/dist/tree/edit.d.ts.map +1 -0
  80. package/dist/tree/edit.js +115 -0
  81. package/dist/tree/edit.js.map +1 -0
  82. package/dist/tree/editEncrypted.d.ts +46 -0
  83. package/dist/tree/editEncrypted.d.ts.map +1 -0
  84. package/dist/tree/editEncrypted.js +225 -0
  85. package/dist/tree/editEncrypted.js.map +1 -0
  86. package/dist/tree/index.d.ts +7 -0
  87. package/dist/tree/index.d.ts.map +1 -0
  88. package/dist/tree/index.js +7 -0
  89. package/dist/tree/index.js.map +1 -0
  90. package/dist/tree/read.d.ts +75 -0
  91. package/dist/tree/read.d.ts.map +1 -0
  92. package/dist/tree/read.js +389 -0
  93. package/dist/tree/read.js.map +1 -0
  94. package/dist/tree/writeAt.d.ts +44 -0
  95. package/dist/tree/writeAt.d.ts.map +1 -0
  96. package/dist/tree/writeAt.js +282 -0
  97. package/dist/tree/writeAt.js.map +1 -0
  98. package/dist/types.d.ts +274 -0
  99. package/dist/types.d.ts.map +1 -0
  100. package/dist/types.js +47 -0
  101. package/dist/types.js.map +1 -0
  102. package/dist/verify.d.ts +12 -0
  103. package/dist/verify.d.ts.map +1 -0
  104. package/dist/verify.js +32 -0
  105. package/dist/verify.js.map +1 -0
  106. package/dist/visibility.d.ts +50 -0
  107. package/dist/visibility.d.ts.map +1 -0
  108. package/dist/visibility.js +111 -0
  109. package/dist/visibility.js.map +1 -0
  110. package/dist/webrtc/index.d.ts +4 -0
  111. package/dist/webrtc/index.d.ts.map +1 -0
  112. package/dist/webrtc/index.js +4 -0
  113. package/dist/webrtc/index.js.map +1 -0
  114. package/dist/webrtc/lruCache.d.ts +20 -0
  115. package/dist/webrtc/lruCache.d.ts.map +1 -0
  116. package/dist/webrtc/lruCache.js +59 -0
  117. package/dist/webrtc/lruCache.js.map +1 -0
  118. package/dist/webrtc/peer.d.ts +122 -0
  119. package/dist/webrtc/peer.d.ts.map +1 -0
  120. package/dist/webrtc/peer.js +583 -0
  121. package/dist/webrtc/peer.js.map +1 -0
  122. package/dist/webrtc/protocol.d.ts +76 -0
  123. package/dist/webrtc/protocol.d.ts.map +1 -0
  124. package/dist/webrtc/protocol.js +167 -0
  125. package/dist/webrtc/protocol.js.map +1 -0
  126. package/dist/webrtc/store.d.ts +190 -0
  127. package/dist/webrtc/store.d.ts.map +1 -0
  128. package/dist/webrtc/store.js +1043 -0
  129. package/dist/webrtc/store.js.map +1 -0
  130. package/dist/webrtc/types.d.ts +196 -0
  131. package/dist/webrtc/types.d.ts.map +1 -0
  132. package/dist/webrtc/types.js +46 -0
  133. package/dist/webrtc/types.js.map +1 -0
  134. package/dist/worker/protocol.d.ts +493 -0
  135. package/dist/worker/protocol.d.ts.map +1 -0
  136. package/dist/worker/protocol.js +15 -0
  137. package/dist/worker/protocol.js.map +1 -0
  138. package/package.json +59 -0
@@ -0,0 +1,1043 @@
1
+ /**
2
+ * WebRTC-based distributed store for hashtree
3
+ *
4
+ * Implements the Store interface, fetching data from P2P network.
5
+ * Uses Nostr relays for WebRTC signaling with perfect negotiation (both peers can initiate).
6
+ *
7
+ * Signaling protocol (all use ephemeral kind 25050):
8
+ * - Hello messages: #l: "hello" tag, broadcast for peer discovery (unencrypted)
9
+ * - Directed signaling (offer, answer, candidate, candidates): #p tag with
10
+ * recipient pubkey, NIP-17 style gift wrap for privacy
11
+ *
12
+ * Pool-based peer management:
13
+ * - 'follows' pool: Users in your social graph (followed or followers)
14
+ * - 'other' pool: Everyone else (randos)
15
+ * Each pool has its own connection limits.
16
+ */
17
+ import { SimplePool } from 'nostr-tools';
18
+ import { toHex } from '../types.js';
19
+ import { PeerId, generateUuid, } from './types.js';
20
+ import { Peer } from './peer.js';
21
+ export const DEFAULT_RELAYS = [
22
+ 'wss://relay.damus.io',
23
+ 'wss://relay.primal.net',
24
+ 'wss://nos.lol',
25
+ 'wss://relay.nostr.band',
26
+ 'wss://temp.iris.to',
27
+ 'wss://relay.snort.social',
28
+ ];
29
+ // All WebRTC signaling uses ephemeral kind 25050
30
+ // Hello messages use #l tag for broadcast discovery
31
+ // Directed messages use #p tag with gift wrap
32
+ const SIGNALING_KIND = 25050;
33
+ const HELLO_TAG = 'hello';
34
+ export class WebRTCStore {
35
+ config;
36
+ pools;
37
+ peerClassifier;
38
+ getFollowedPubkeys;
39
+ isPeerBlocked;
40
+ signer;
41
+ encrypt;
42
+ decrypt;
43
+ giftWrap;
44
+ giftUnwrap;
45
+ myPeerId;
46
+ pool;
47
+ subscriptions = [];
48
+ helloSubscription = null;
49
+ peers = new Map();
50
+ // Track pubkeys we're currently connecting to in 'other' pool (prevents race conditions)
51
+ pendingOtherPubkeys = new Set();
52
+ helloInterval = null;
53
+ cleanupInterval = null;
54
+ eventHandlers = new Set();
55
+ running = false;
56
+ pendingReqs = new Map();
57
+ // Deduplicate concurrent get() calls for the same hash
58
+ pendingGets = new Map();
59
+ // Store-level stats (not per-peer)
60
+ blossomFetches = 0;
61
+ // Track current hello subscription authors for change detection
62
+ currentHelloAuthors = null;
63
+ constructor(config) {
64
+ this.signer = config.signer;
65
+ this.encrypt = config.encrypt;
66
+ this.decrypt = config.decrypt;
67
+ this.giftWrap = config.giftWrap;
68
+ this.giftUnwrap = config.giftUnwrap;
69
+ this.myPeerId = new PeerId(config.pubkey, generateUuid());
70
+ // Default classifier: everyone is 'other' unless classifier provided
71
+ this.peerClassifier = config.peerClassifier ?? (() => 'other');
72
+ // Function to get followed pubkeys for subscription filtering
73
+ this.getFollowedPubkeys = config.getFollowedPubkeys ?? null;
74
+ // Function to check if a peer is blocked
75
+ this.isPeerBlocked = config.isPeerBlocked ?? null;
76
+ // Use pool config if provided, otherwise fall back to legacy config or defaults
77
+ if (config.pools) {
78
+ this.pools = config.pools;
79
+ }
80
+ else {
81
+ // Legacy mode: single pool with old config values
82
+ const maxConn = config.maxConnections ?? 6;
83
+ const satConn = config.satisfiedConnections ?? 3;
84
+ this.pools = {
85
+ follows: { maxConnections: 0, satisfiedConnections: 0 }, // No follows pool in legacy
86
+ other: { maxConnections: maxConn, satisfiedConnections: satConn },
87
+ };
88
+ }
89
+ this.config = {
90
+ helloInterval: config.helloInterval ?? 10000,
91
+ messageTimeout: config.messageTimeout ?? 60000, // 60 seconds for relay propagation
92
+ requestTimeout: config.requestTimeout ?? 500,
93
+ peerQueryDelay: config.peerQueryDelay ?? 500,
94
+ relays: config.relays ?? DEFAULT_RELAYS,
95
+ localStore: config.localStore ?? null,
96
+ fallbackStores: config.fallbackStores ?? [],
97
+ debug: config.debug ?? false,
98
+ };
99
+ this.pool = new SimplePool();
100
+ }
101
+ log(...args) {
102
+ if (this.config.debug) {
103
+ console.log('[WebRTCStore]', ...args);
104
+ }
105
+ }
106
+ /**
107
+ * Get pool counts
108
+ */
109
+ getPoolCounts() {
110
+ const counts = {
111
+ follows: { connected: 0, total: 0 },
112
+ other: { connected: 0, total: 0 },
113
+ };
114
+ for (const { peer, pool } of this.peers.values()) {
115
+ counts[pool].total++;
116
+ if (peer.isConnected) {
117
+ counts[pool].connected++;
118
+ }
119
+ }
120
+ return counts;
121
+ }
122
+ /**
123
+ * Check if we can accept a peer in a given pool
124
+ */
125
+ canAcceptPeer(pool) {
126
+ const counts = this.getPoolCounts();
127
+ return counts[pool].total < this.pools[pool].maxConnections;
128
+ }
129
+ /**
130
+ * Check if a pool is satisfied
131
+ */
132
+ isPoolSatisfied(pool) {
133
+ const counts = this.getPoolCounts();
134
+ return counts[pool].connected >= this.pools[pool].satisfiedConnections;
135
+ }
136
+ /**
137
+ * Check if we already have a connection from a pubkey in the 'other' pool.
138
+ * In the 'other' pool, we only allow 1 instance per pubkey.
139
+ * Also checks pendingOtherPubkeys to prevent race conditions.
140
+ */
141
+ hasOtherPoolPubkey(pubkey) {
142
+ if (this.pendingOtherPubkeys.has(pubkey)) {
143
+ return true;
144
+ }
145
+ for (const { peer, pool } of this.peers.values()) {
146
+ if (pool === 'other' && peer.pubkey === pubkey) {
147
+ return true;
148
+ }
149
+ }
150
+ return false;
151
+ }
152
+ /**
153
+ * Start the WebRTC store - connect to relays and begin peer discovery
154
+ */
155
+ start() {
156
+ if (this.running)
157
+ return;
158
+ this.running = true;
159
+ this.log('Starting with peerId:', this.myPeerId.short());
160
+ this.log('Pool config:', this.pools);
161
+ this.log('Relays:', this.config.relays);
162
+ // Subscribe to signaling messages
163
+ this.startSubscription();
164
+ // Send hello messages when not satisfied
165
+ this.helloInterval = setInterval(() => {
166
+ this.maybeSendHello();
167
+ }, this.config.helloInterval);
168
+ // Send initial hello
169
+ this.maybeSendHello();
170
+ // Cleanup stale connections
171
+ this.cleanupInterval = setInterval(() => {
172
+ this.cleanupConnections();
173
+ }, 5000);
174
+ }
175
+ /**
176
+ * Stop the WebRTC store
177
+ */
178
+ stop() {
179
+ if (!this.running)
180
+ return;
181
+ this.running = false;
182
+ this.log('Stopping');
183
+ if (this.helloInterval) {
184
+ clearInterval(this.helloInterval);
185
+ this.helloInterval = null;
186
+ }
187
+ if (this.cleanupInterval) {
188
+ clearInterval(this.cleanupInterval);
189
+ this.cleanupInterval = null;
190
+ }
191
+ for (const sub of this.subscriptions) {
192
+ sub.close();
193
+ }
194
+ this.subscriptions = [];
195
+ // Close all peer connections
196
+ for (const { peer } of this.peers.values()) {
197
+ peer.close();
198
+ }
199
+ this.peers.clear();
200
+ this.pendingOtherPubkeys.clear();
201
+ }
202
+ /**
203
+ * Set relays and reconnect. Useful for runtime relay configuration changes.
204
+ * Closes existing subscriptions and starts new ones with the updated relays.
205
+ */
206
+ setRelays(relays) {
207
+ this.log('setRelays:', relays);
208
+ this.config.relays = relays;
209
+ // If running, restart subscriptions with new relays
210
+ if (this.running) {
211
+ // Close existing subscriptions
212
+ for (const sub of this.subscriptions) {
213
+ sub.close();
214
+ }
215
+ this.subscriptions = [];
216
+ // Clear existing peers (they were discovered via old relays)
217
+ for (const { peer } of this.peers.values()) {
218
+ peer.close();
219
+ }
220
+ this.peers.clear();
221
+ // Start new subscriptions with updated relays
222
+ this.startSubscription();
223
+ this.emit({ type: 'update' });
224
+ }
225
+ }
226
+ /**
227
+ * Add event listener
228
+ */
229
+ on(handler) {
230
+ this.eventHandlers.add(handler);
231
+ return () => this.eventHandlers.delete(handler);
232
+ }
233
+ emit(event) {
234
+ for (const handler of this.eventHandlers) {
235
+ handler(event);
236
+ }
237
+ }
238
+ startSubscription() {
239
+ const since = Math.floor((Date.now() - this.config.messageTimeout) / 1000);
240
+ const subHandler = {
241
+ onevent: (event) => {
242
+ this.handleSignalingEvent(event);
243
+ },
244
+ oneose: () => { },
245
+ };
246
+ // 1. Subscribe to hello messages based on pool configuration
247
+ this.setupHelloSubscription(since, subHandler);
248
+ // 2. Subscribe to directed signaling (kind 25050 with #p tag) for offers/answers/candidates
249
+ // Always subscribe to directed messages (needed to receive offers/answers)
250
+ this.subscriptions.push(this.pool.subscribe(this.config.relays, {
251
+ kinds: [SIGNALING_KIND],
252
+ '#p': [this.myPeerId.pubkey],
253
+ since,
254
+ }, subHandler));
255
+ }
256
+ /**
257
+ * Setup hello subscription based on pool configuration
258
+ * - If both pools are 0: don't subscribe to hellos
259
+ * - If other pool is disabled but follows is enabled: subscribe to followed pubkeys only
260
+ * - If other pool is enabled: subscribe to all hellos
261
+ */
262
+ setupHelloSubscription(since, subHandler) {
263
+ // Close existing hello subscription if any
264
+ if (this.helloSubscription) {
265
+ this.helloSubscription.close();
266
+ this.helloSubscription = null;
267
+ }
268
+ const followsMax = this.pools.follows.maxConnections;
269
+ const otherMax = this.pools.other.maxConnections;
270
+ // If both pools are disabled, don't subscribe to hellos at all
271
+ if (followsMax === 0 && otherMax === 0) {
272
+ this.log('Both pools disabled, not subscribing to hellos');
273
+ this.currentHelloAuthors = [];
274
+ return;
275
+ }
276
+ // If other pool is disabled but follows pool is enabled, only subscribe to followed users
277
+ if (otherMax === 0 && followsMax > 0) {
278
+ const followedPubkeys = this.getFollowedPubkeys?.() ?? [];
279
+ if (followedPubkeys.length === 0) {
280
+ this.log('Follows pool enabled but no followed users, not subscribing to hellos');
281
+ this.currentHelloAuthors = [];
282
+ return;
283
+ }
284
+ this.log('Other pool disabled, subscribing to hellos from', followedPubkeys.length, 'followed users');
285
+ this.currentHelloAuthors = [...followedPubkeys];
286
+ this.helloSubscription = this.pool.subscribe(this.config.relays, {
287
+ kinds: [SIGNALING_KIND],
288
+ '#l': [HELLO_TAG],
289
+ authors: followedPubkeys,
290
+ since,
291
+ }, subHandler);
292
+ return;
293
+ }
294
+ // Otherwise subscribe to all hellos
295
+ this.log('Subscribing to all hellos');
296
+ this.currentHelloAuthors = null; // null means all authors
297
+ this.helloSubscription = this.pool.subscribe(this.config.relays, {
298
+ kinds: [SIGNALING_KIND],
299
+ '#l': [HELLO_TAG],
300
+ since,
301
+ }, subHandler);
302
+ }
303
+ /**
304
+ * Update hello subscription when follows list changes
305
+ * Call this after social graph updates
306
+ */
307
+ updateHelloSubscription() {
308
+ if (!this.running)
309
+ return;
310
+ const followsMax = this.pools.follows.maxConnections;
311
+ const otherMax = this.pools.other.maxConnections;
312
+ // Only need to update if we're in follows-only mode
313
+ if (otherMax === 0 && followsMax > 0) {
314
+ const followedPubkeys = this.getFollowedPubkeys?.() ?? [];
315
+ const currentAuthors = this.currentHelloAuthors ?? [];
316
+ // Check if follows list changed
317
+ const changed = followedPubkeys.length !== currentAuthors.length ||
318
+ !followedPubkeys.every(pk => currentAuthors.includes(pk));
319
+ if (changed) {
320
+ const since = Math.floor((Date.now() - this.config.messageTimeout) / 1000);
321
+ const subHandler = {
322
+ onevent: (event) => {
323
+ this.handleSignalingEvent(event);
324
+ },
325
+ oneose: () => { },
326
+ };
327
+ this.setupHelloSubscription(since, subHandler);
328
+ }
329
+ }
330
+ }
331
+ async handleSignalingEvent(event) {
332
+ // Filter out old events (created more than messageTimeout ago)
333
+ const eventAge = Date.now() / 1000 - (event.created_at ?? 0);
334
+ if (eventAge > this.config.messageTimeout / 1000) {
335
+ return;
336
+ }
337
+ // Check expiration
338
+ const expirationTag = event.tags.find(t => t[0] === 'expiration');
339
+ if (expirationTag) {
340
+ const expiration = parseInt(expirationTag[1], 10);
341
+ if (expiration < Date.now() / 1000) {
342
+ return;
343
+ }
344
+ }
345
+ // Check if this is a hello message (#l: hello tag)
346
+ const lTag = event.tags.find(t => t[0] === 'l')?.[1];
347
+ if (lTag === HELLO_TAG) {
348
+ const peerIdTag = event.tags.find(t => t[0] === 'peerId')?.[1];
349
+ if (peerIdTag) {
350
+ await this.handleHello(peerIdTag, event.pubkey);
351
+ }
352
+ return;
353
+ }
354
+ // Check if this is a directed message (#p tag pointing to us)
355
+ const pTag = event.tags.find(t => t[0] === 'p')?.[1];
356
+ if (pTag === this.myPeerId.pubkey) {
357
+ // Gift-wrapped signaling message - try to unwrap
358
+ try {
359
+ const inner = await this.giftUnwrap(event);
360
+ if (!inner) {
361
+ return; // Can't decrypt - not for us
362
+ }
363
+ const msg = JSON.parse(inner.content);
364
+ await this.handleSignalingMessage(msg, inner.pubkey);
365
+ }
366
+ catch {
367
+ // Not for us or invalid - ignore silently
368
+ }
369
+ }
370
+ }
371
+ async handleSignalingMessage(msg, senderPubkey) {
372
+ // Directed message - check if it's for us
373
+ if (msg.targetPeerId !== this.myPeerId.toString()) {
374
+ return;
375
+ }
376
+ const peerId = new PeerId(senderPubkey, msg.peerId);
377
+ const peerIdStr = peerId.toString();
378
+ if (msg.type === 'offer') {
379
+ await this.handleOffer(peerId, msg);
380
+ }
381
+ else {
382
+ // answer or candidate
383
+ const peerInfo = this.peers.get(peerIdStr);
384
+ if (peerInfo) {
385
+ await peerInfo.peer.handleSignaling(msg);
386
+ }
387
+ }
388
+ }
389
+ async handleHello(peerUuid, senderPubkey) {
390
+ const peerId = new PeerId(senderPubkey, peerUuid);
391
+ // Skip self (exact same peerId = same session)
392
+ if (peerId.toString() === this.myPeerId.toString()) {
393
+ return;
394
+ }
395
+ // Skip blocked peers
396
+ if (this.isPeerBlocked?.(senderPubkey)) {
397
+ this.log('Ignoring hello from blocked peer:', senderPubkey.slice(0, 8));
398
+ return;
399
+ }
400
+ // Check if we already have this peer
401
+ if (this.peers.has(peerId.toString())) {
402
+ return;
403
+ }
404
+ // Classify the peer
405
+ let pool;
406
+ try {
407
+ pool = this.peerClassifier(senderPubkey);
408
+ }
409
+ catch (e) {
410
+ this.log('Error classifying peer:', e);
411
+ pool = 'other';
412
+ }
413
+ // Check if we can accept this peer in their pool
414
+ if (!this.canAcceptPeer(pool)) {
415
+ return;
416
+ }
417
+ // In 'other' pool, only allow 1 instance per pubkey
418
+ if (pool === 'other' && this.hasOtherPoolPubkey(senderPubkey)) {
419
+ return;
420
+ }
421
+ // Perfect negotiation: both peers initiate
422
+ // Collision is handled by Peer class (polite peer rolls back)
423
+ // Mark as pending before async operation to prevent race conditions
424
+ if (pool === 'other') {
425
+ this.pendingOtherPubkeys.add(senderPubkey);
426
+ }
427
+ try {
428
+ await this.connectToPeer(peerId, pool);
429
+ }
430
+ finally {
431
+ this.pendingOtherPubkeys.delete(senderPubkey);
432
+ }
433
+ }
434
+ async handleOffer(peerId, msg) {
435
+ // Skip self (exact same peerId)
436
+ if (peerId.toString() === this.myPeerId.toString()) {
437
+ return;
438
+ }
439
+ // Skip blocked peers
440
+ if (this.isPeerBlocked?.(peerId.pubkey)) {
441
+ this.log('Ignoring offer from blocked peer:', peerId.pubkey.slice(0, 8));
442
+ return;
443
+ }
444
+ const peerIdStr = peerId.toString();
445
+ // Classify the peer
446
+ const pool = this.peerClassifier(peerId.pubkey);
447
+ // Check if we can accept (unless we already have this peer)
448
+ if (!this.peers.has(peerIdStr) && !this.canAcceptPeer(pool)) {
449
+ return;
450
+ }
451
+ // In 'other' pool, only allow 1 instance per pubkey (unless we already have this exact peer)
452
+ if (!this.peers.has(peerIdStr) && pool === 'other' && this.hasOtherPoolPubkey(peerId.pubkey)) {
453
+ return;
454
+ }
455
+ // Mark as pending before any async gaps to prevent race conditions
456
+ if (pool === 'other' && !this.peers.has(peerIdStr)) {
457
+ this.pendingOtherPubkeys.add(peerId.pubkey);
458
+ }
459
+ // Clean up existing connection if any
460
+ const existing = this.peers.get(peerIdStr);
461
+ if (existing) {
462
+ existing.peer.close();
463
+ this.peers.delete(peerIdStr);
464
+ }
465
+ const peer = new Peer({
466
+ peerId,
467
+ myPeerId: this.myPeerId.uuid,
468
+ direction: 'inbound',
469
+ localStore: this.config.localStore,
470
+ sendSignaling: (m) => this.sendSignaling(m, peerId.pubkey),
471
+ onClose: () => this.handlePeerClose(peerIdStr),
472
+ onConnected: () => {
473
+ this.emit({ type: 'peer-connected', peerId: peerIdStr });
474
+ this.emit({ type: 'update' });
475
+ this.tryPendingReqs(peer);
476
+ },
477
+ onForwardRequest: (hash, exclude, htl) => this.forwardRequest(hash, exclude, htl),
478
+ requestTimeout: this.config.requestTimeout,
479
+ debug: this.config.debug,
480
+ });
481
+ this.peers.set(peerIdStr, { peer, pool });
482
+ // Clear pending now that peer is in the map
483
+ this.pendingOtherPubkeys.delete(peerId.pubkey);
484
+ await peer.handleSignaling(msg);
485
+ }
486
+ async connectToPeer(peerId, pool) {
487
+ const peerIdStr = peerId.toString();
488
+ if (this.peers.has(peerIdStr)) {
489
+ return;
490
+ }
491
+ this.log('Initiating connection to', peerId.short(), 'pool:', pool);
492
+ const peer = new Peer({
493
+ peerId,
494
+ myPeerId: this.myPeerId.uuid,
495
+ direction: 'outbound',
496
+ localStore: this.config.localStore,
497
+ sendSignaling: (m) => this.sendSignaling(m, peerId.pubkey),
498
+ onClose: () => this.handlePeerClose(peerIdStr),
499
+ onConnected: () => {
500
+ this.emit({ type: 'peer-connected', peerId: peerIdStr });
501
+ this.emit({ type: 'update' });
502
+ this.tryPendingReqs(peer);
503
+ },
504
+ onForwardRequest: (hash, exclude, htl) => this.forwardRequest(hash, exclude, htl),
505
+ requestTimeout: this.config.requestTimeout,
506
+ debug: this.config.debug,
507
+ });
508
+ this.peers.set(peerIdStr, { peer, pool });
509
+ await peer.connect();
510
+ }
511
+ handlePeerClose(peerIdStr) {
512
+ this.peers.delete(peerIdStr);
513
+ this.emit({ type: 'peer-disconnected', peerId: peerIdStr });
514
+ this.emit({ type: 'update' });
515
+ }
516
+ /**
517
+ * Forward a request to other peers (excluding the requester)
518
+ * Called by Peer when it receives a request it can't fulfill locally
519
+ * Uses sequential queries with delays between attempts
520
+ * @param htl - Hops To Live (already decremented by calling peer)
521
+ */
522
+ async forwardRequest(hash, excludePeerId, htl) {
523
+ // Try all connected peers except the one who requested
524
+ const otherPeers = Array.from(this.peers.values())
525
+ .filter(({ peer }) => peer.isConnected && peer.peerId !== excludePeerId);
526
+ // Sort: follows first
527
+ otherPeers.sort((a, b) => {
528
+ if (a.pool === 'follows' && b.pool !== 'follows')
529
+ return -1;
530
+ if (a.pool !== 'follows' && b.pool === 'follows')
531
+ return 1;
532
+ return 0;
533
+ });
534
+ // Query peers sequentially with delay between attempts
535
+ for (let i = 0; i < otherPeers.length; i++) {
536
+ const { peer } = otherPeers[i];
537
+ // Start request to this peer with the decremented HTL
538
+ const requestPromise = peer.request(hash, htl);
539
+ // Race between request completing and delay timeout
540
+ const result = await Promise.race([
541
+ requestPromise.then(data => ({ type: 'data', data })),
542
+ this.delay(this.config.peerQueryDelay).then(() => ({ type: 'timeout' })),
543
+ ]);
544
+ if (result.type === 'data' && result.data) {
545
+ // Got data from this peer
546
+ if (this.config.localStore) {
547
+ await this.config.localStore.put(hash, result.data);
548
+ }
549
+ return result.data;
550
+ }
551
+ // If timeout, continue to next peer
552
+ if (result.type === 'timeout') {
553
+ this.log('Forward: peer', peer.peerId.slice(0, 12), 'timeout, trying next');
554
+ }
555
+ }
556
+ return null;
557
+ }
558
+ /**
559
+ * Send data to all peers who have requested this hash
560
+ * Called when we receive data that peers may be waiting for
561
+ */
562
+ sendToInterestedPeers(hash, data) {
563
+ let sendCount = 0;
564
+ for (const { peer } of this.peers.values()) {
565
+ if (peer.isConnected && peer.sendData(hash, data)) {
566
+ sendCount++;
567
+ }
568
+ }
569
+ if (sendCount > 0) {
570
+ this.log('Sent data to', sendCount, 'interested peers for hash:', toHex(hash).slice(0, 16));
571
+ }
572
+ return sendCount;
573
+ }
574
+ async sendSignaling(msg, recipientPubkey) {
575
+ // Fill in our peer ID
576
+ if ('peerId' in msg && msg.peerId === '') {
577
+ msg.peerId = this.myPeerId.uuid;
578
+ }
579
+ if (recipientPubkey) {
580
+ // Directed message (offer, answer, candidate, candidates)
581
+ // Use NIP-17 style gift wrap with kind 25050
582
+ const innerEvent = {
583
+ kind: SIGNALING_KIND,
584
+ content: JSON.stringify(msg),
585
+ tags: [],
586
+ };
587
+ const wrappedEvent = await this.giftWrap(innerEvent, recipientPubkey);
588
+ await this.pool.publish(this.config.relays, wrappedEvent);
589
+ }
590
+ else {
591
+ // Hello message - broadcast for peer discovery (kind 25050 with #l: hello)
592
+ const expiration = Math.floor((Date.now() + 5 * 60 * 1000) / 1000); // 5 minutes
593
+ const tags = [
594
+ ['l', HELLO_TAG],
595
+ ['peerId', msg.peerId],
596
+ ['expiration', expiration.toString()],
597
+ ];
598
+ const eventTemplate = {
599
+ kind: SIGNALING_KIND,
600
+ created_at: Math.floor(Date.now() / 1000),
601
+ tags,
602
+ content: '',
603
+ };
604
+ const event = await this.signer(eventTemplate);
605
+ await this.pool.publish(this.config.relays, event);
606
+ }
607
+ }
608
+ /**
609
+ * Force send a hello message (useful for testing after pool config changes)
610
+ */
611
+ sendHello() {
612
+ if (!this.running)
613
+ return;
614
+ this.maybeSendHello();
615
+ }
616
+ maybeSendHello() {
617
+ if (!this.running)
618
+ return;
619
+ // Check if both pools are satisfied
620
+ const followsSatisfied = this.isPoolSatisfied('follows');
621
+ const otherSatisfied = this.isPoolSatisfied('other');
622
+ if (followsSatisfied && otherSatisfied) {
623
+ return;
624
+ }
625
+ this.sendSignaling({
626
+ type: 'hello',
627
+ peerId: this.myPeerId.uuid,
628
+ });
629
+ }
630
+ cleanupConnections() {
631
+ const now = Date.now();
632
+ const connectionTimeout = 15000; // 15 seconds to establish connection
633
+ for (const [peerIdStr, { peer }] of this.peers) {
634
+ const state = peer.state;
635
+ const isStale = state === 'new' && (now - peer.createdAt) > connectionTimeout;
636
+ if (state === 'failed' || state === 'closed' || state === 'disconnected' || isStale) {
637
+ this.log('Cleaning up', state, 'connection', isStale ? '(stale)' : '');
638
+ peer.close();
639
+ this.peers.delete(peerIdStr);
640
+ this.emit({ type: 'update' });
641
+ }
642
+ }
643
+ }
644
+ /**
645
+ * Get number of connected peers
646
+ */
647
+ getConnectedCount() {
648
+ return Array.from(this.peers.values())
649
+ .filter(({ peer }) => peer.isConnected).length;
650
+ }
651
+ /**
652
+ * Get all peer statuses
653
+ */
654
+ getPeers() {
655
+ return Array.from(this.peers.values()).map(({ peer, pool }) => ({
656
+ peerId: peer.peerId,
657
+ pubkey: peer.pubkey,
658
+ state: peer.state,
659
+ direction: peer.direction,
660
+ connectedAt: peer.connectedAt,
661
+ isSelf: peer.pubkey === this.myPeerId.pubkey,
662
+ pool,
663
+ isConnected: peer.isConnected, // Includes data channel state
664
+ }));
665
+ }
666
+ /**
667
+ * Disconnect all peers with a given pubkey
668
+ * Used when blocking a peer to immediately disconnect them
669
+ */
670
+ disconnectPeerByPubkey(pubkey) {
671
+ for (const [peerIdStr, peerInfo] of this.peers) {
672
+ if (peerInfo.peer.pubkey === pubkey) {
673
+ this.log('Disconnecting blocked peer:', pubkey.slice(0, 8));
674
+ peerInfo.peer.close();
675
+ this.peers.delete(peerIdStr);
676
+ this.emit({ type: 'peer-disconnected', peerId: peerIdStr });
677
+ }
678
+ }
679
+ this.emit({ type: 'update' });
680
+ }
681
+ /**
682
+ * Get my peer ID (uuid part only)
683
+ */
684
+ getMyPeerId() {
685
+ return this.myPeerId.uuid;
686
+ }
687
+ /**
688
+ * Check if store is satisfied (has enough connections in all pools)
689
+ */
690
+ isSatisfied() {
691
+ return this.isPoolSatisfied('follows') && this.isPoolSatisfied('other');
692
+ }
693
+ /**
694
+ * Update peer classifier (e.g., when social graph updates)
695
+ */
696
+ setPeerClassifier(classifier) {
697
+ this.peerClassifier = classifier;
698
+ // Re-classify existing peers (they keep their connections, just update pool assignment)
699
+ for (const [peerIdStr, peerInfo] of this.peers) {
700
+ const newPool = classifier(peerInfo.peer.pubkey);
701
+ if (newPool !== peerInfo.pool) {
702
+ this.log('Reclassified peer', peerIdStr.slice(0, 16), 'from', peerInfo.pool, 'to', newPool);
703
+ peerInfo.pool = newPool;
704
+ }
705
+ }
706
+ this.emit({ type: 'update' });
707
+ }
708
+ /**
709
+ * Update pool configuration (e.g., from settings)
710
+ */
711
+ setPoolConfig(pools) {
712
+ const oldOtherMax = this.pools.other.maxConnections;
713
+ const oldFollowsMax = this.pools.follows.maxConnections;
714
+ this.pools = pools;
715
+ this.log('Pool config updated:', pools);
716
+ // Check if subscription mode needs to change
717
+ const newOtherMax = pools.other.maxConnections;
718
+ const newFollowsMax = pools.follows.maxConnections;
719
+ const subscriptionModeChanged = (oldOtherMax === 0) !== (newOtherMax === 0) ||
720
+ (oldFollowsMax === 0) !== (newFollowsMax === 0);
721
+ if (subscriptionModeChanged && this.running) {
722
+ const since = Math.floor((Date.now() - this.config.messageTimeout) / 1000);
723
+ const subHandler = {
724
+ onevent: (event) => {
725
+ this.handleSignalingEvent(event);
726
+ },
727
+ oneose: () => { },
728
+ };
729
+ this.setupHelloSubscription(since, subHandler);
730
+ }
731
+ // Existing connections remain, but new limits apply for future connections
732
+ this.emit({ type: 'update' });
733
+ }
734
+ /**
735
+ * Get current pool configuration
736
+ */
737
+ getPoolConfig() {
738
+ return { ...this.pools };
739
+ }
740
+ /**
741
+ * Get fallback stores count
742
+ */
743
+ getFallbackStoresCount() {
744
+ return this.config.fallbackStores.length;
745
+ }
746
+ /**
747
+ * Get WebRTC stats (aggregate and per-peer)
748
+ */
749
+ getStats() {
750
+ // Aggregate stats from all peers + store-level stats
751
+ const aggregate = {
752
+ requestsSent: 0,
753
+ requestsReceived: 0,
754
+ responsesSent: 0,
755
+ responsesReceived: 0,
756
+ receiveErrors: 0,
757
+ blossomFetches: this.blossomFetches,
758
+ fragmentsSent: 0,
759
+ fragmentsReceived: 0,
760
+ fragmentTimeouts: 0,
761
+ reassembliesCompleted: 0,
762
+ bytesSent: 0,
763
+ bytesReceived: 0,
764
+ bytesForwarded: 0,
765
+ };
766
+ const perPeer = new Map();
767
+ for (const [peerIdStr, { peer, pool }] of this.peers) {
768
+ const peerStats = peer.getStats();
769
+ aggregate.requestsSent += peerStats.requestsSent;
770
+ aggregate.requestsReceived += peerStats.requestsReceived;
771
+ aggregate.responsesSent += peerStats.responsesSent;
772
+ aggregate.responsesReceived += peerStats.responsesReceived;
773
+ aggregate.receiveErrors += peerStats.receiveErrors;
774
+ aggregate.fragmentsSent += peerStats.fragmentsSent;
775
+ aggregate.fragmentsReceived += peerStats.fragmentsReceived;
776
+ aggregate.fragmentTimeouts += peerStats.fragmentTimeouts;
777
+ aggregate.reassembliesCompleted += peerStats.reassembliesCompleted;
778
+ aggregate.bytesSent += peerStats.bytesSent;
779
+ aggregate.bytesReceived += peerStats.bytesReceived;
780
+ aggregate.bytesForwarded += peerStats.bytesForwarded;
781
+ perPeer.set(peerIdStr, {
782
+ pubkey: peer.pubkey,
783
+ pool,
784
+ stats: peerStats,
785
+ });
786
+ }
787
+ return { aggregate, perPeer };
788
+ }
789
+ // Store interface implementation
790
+ async put(hash, data) {
791
+ // Write to local store if available
792
+ const success = this.config.localStore
793
+ ? await this.config.localStore.put(hash, data)
794
+ : false;
795
+ // Send to any peers who have requested this hash
796
+ this.sendToInterestedPeers(hash, data);
797
+ // Fire-and-forget writes to fallback stores (e.g. blossom servers)
798
+ // Don't await - let them complete in background
799
+ for (const store of this.config.fallbackStores) {
800
+ store.put(hash, data).catch(() => {
801
+ // Silently ignore failures - fallback stores are best-effort
802
+ });
803
+ }
804
+ return success;
805
+ }
806
+ async get(hash) {
807
+ // Guard against undefined hash
808
+ if (!hash)
809
+ return null;
810
+ // Try local store first
811
+ if (this.config.localStore) {
812
+ const local = await this.config.localStore.get(hash);
813
+ if (local)
814
+ return local;
815
+ }
816
+ // Deduplicate: if there's already a pending request for this hash, wait for it
817
+ const hashHex = toHex(hash);
818
+ const pendingGet = this.pendingGets.get(hashHex);
819
+ if (pendingGet) {
820
+ return pendingGet;
821
+ }
822
+ // Create the actual fetch promise
823
+ const fetchPromise = this.fetchFromPeers(hash);
824
+ // Store it for deduplication
825
+ this.pendingGets.set(hashHex, fetchPromise);
826
+ // Clean up when done
827
+ try {
828
+ const result = await fetchPromise;
829
+ return result;
830
+ }
831
+ finally {
832
+ this.pendingGets.delete(hashHex);
833
+ }
834
+ }
835
+ /**
836
+ * Internal method to fetch data from peers (separated for deduplication)
837
+ */
838
+ async fetchFromPeers(hash) {
839
+ // Get currently connected peers (prioritize follows pool)
840
+ const triedPeers = new Set();
841
+ const allPeers = Array.from(this.peers.values())
842
+ .filter(({ peer }) => peer.isConnected);
843
+ // Sort: follows first, then others
844
+ allPeers.sort((a, b) => {
845
+ if (a.pool === 'follows' && b.pool !== 'follows')
846
+ return -1;
847
+ if (a.pool !== 'follows' && b.pool === 'follows')
848
+ return 1;
849
+ return 0;
850
+ });
851
+ // Query peers sequentially with delay between attempts
852
+ for (let i = 0; i < allPeers.length; i++) {
853
+ const { peer } = allPeers[i];
854
+ triedPeers.add(peer.peerId);
855
+ // Start request to this peer
856
+ const requestPromise = peer.request(hash);
857
+ // Race between request completing and delay timeout
858
+ // If request completes within delay, we're done
859
+ // If delay passes first, start next peer while still waiting
860
+ const result = await Promise.race([
861
+ requestPromise.then(data => ({ type: 'data', data })),
862
+ this.delay(this.config.peerQueryDelay).then(() => ({ type: 'timeout' })),
863
+ ]);
864
+ if (result.type === 'data' && result.data) {
865
+ // Got data from this peer
866
+ if (this.config.localStore) {
867
+ await this.config.localStore.put(hash, result.data);
868
+ }
869
+ return result.data;
870
+ }
871
+ // If timeout, continue to next peer but also await the original request
872
+ // in case it eventually returns data
873
+ if (result.type === 'timeout') {
874
+ // Fire-and-forget: if this peer eventually responds, we'll miss it
875
+ // but that's fine - we're trying the next peer
876
+ this.log('Peer', peer.peerId.slice(0, 12), 'timeout after', this.config.peerQueryDelay, 'ms, trying next');
877
+ }
878
+ }
879
+ // All WebRTC peers failed - try fallback stores in order
880
+ if (this.config.fallbackStores.length > 0) {
881
+ for (const store of this.config.fallbackStores) {
882
+ try {
883
+ const data = await store.get(hash);
884
+ if (data) {
885
+ this.blossomFetches++;
886
+ if (this.config.localStore) {
887
+ await this.config.localStore.put(hash, data);
888
+ }
889
+ return data;
890
+ }
891
+ }
892
+ catch (e) {
893
+ this.log('Fallback store error:', e);
894
+ }
895
+ }
896
+ }
897
+ // If running and either:
898
+ // 1. Not satisfied (still seeking more peers), OR
899
+ // 2. We haven't tried any peers yet (no peers connected, but might connect soon)
900
+ // Then add to pending reqs and wait for new peers
901
+ if (this.running && (!this.isSatisfied() || triedPeers.size === 0)) {
902
+ return this.waitForHash(hash, triedPeers);
903
+ }
904
+ return null;
905
+ }
906
+ /**
907
+ * Helper to create a delay promise
908
+ */
909
+ delay(ms) {
910
+ return new Promise(resolve => setTimeout(resolve, ms));
911
+ }
912
+ /**
913
+ * Add hash to pending requests list and wait for it to be resolved by peers
914
+ * Also immediately tries any connected peers that weren't tried yet
915
+ */
916
+ waitForHash(hash, triedPeers) {
917
+ return new Promise((resolve) => {
918
+ // Use longer timeout for pending requests - we need to wait for peers to connect
919
+ const reqTimeout = Math.max(this.config.requestTimeout * 6, 30000);
920
+ const timeout = setTimeout(() => {
921
+ this.removePendingReq(hash, req);
922
+ resolve(null);
923
+ }, reqTimeout);
924
+ const req = { resolve, timeout, triedPeers };
925
+ const existing = this.pendingReqs.get(hash);
926
+ if (existing) {
927
+ existing.push(req);
928
+ }
929
+ else {
930
+ this.pendingReqs.set(hash, [req]);
931
+ }
932
+ this.log('Added to pending reqs:', hash.slice(0, 16), 'tried', triedPeers.size, 'peers');
933
+ // Immediately try any connected peers that weren't tried yet
934
+ // This handles the race condition where peers connect while we're setting up the request
935
+ this.tryConnectedPeersForHash(hash);
936
+ });
937
+ }
938
+ /**
939
+ * Try all currently connected peers for a specific hash in the pending requests
940
+ */
941
+ async tryConnectedPeersForHash(hash) {
942
+ const reqs = this.pendingReqs.get(hash);
943
+ if (!reqs || reqs.length === 0)
944
+ return;
945
+ // Get all connected peers
946
+ const connectedPeers = Array.from(this.peers.values())
947
+ .filter(({ peer }) => peer.isConnected)
948
+ .map(({ peer }) => peer);
949
+ for (const peer of connectedPeers) {
950
+ const peerIdStr = peer.peerId;
951
+ // Find requests that haven't tried this peer yet
952
+ const untried = reqs.filter(r => !r.triedPeers.has(peerIdStr));
953
+ if (untried.length === 0)
954
+ continue;
955
+ // Mark as tried
956
+ for (const r of untried) {
957
+ r.triedPeers.add(peerIdStr);
958
+ }
959
+ this.log('Trying pending req from connected peer:', hash.slice(0, 16));
960
+ const data = await peer.request(hash);
961
+ if (data) {
962
+ // Store locally
963
+ if (this.config.localStore) {
964
+ await this.config.localStore.put(hash, data);
965
+ }
966
+ // Resolve all waiting requests
967
+ const currentReqs = this.pendingReqs.get(hash);
968
+ if (currentReqs) {
969
+ for (const r of currentReqs) {
970
+ clearTimeout(r.timeout);
971
+ r.resolve(data);
972
+ }
973
+ this.pendingReqs.delete(hash);
974
+ }
975
+ this.log('Resolved pending req:', hash.slice(0, 16));
976
+ return;
977
+ }
978
+ }
979
+ }
980
+ /**
981
+ * Remove a pending request from the list
982
+ */
983
+ removePendingReq(hash, req) {
984
+ const reqs = this.pendingReqs.get(hash);
985
+ if (!reqs)
986
+ return;
987
+ const idx = reqs.indexOf(req);
988
+ if (idx !== -1) {
989
+ reqs.splice(idx, 1);
990
+ if (reqs.length === 0) {
991
+ this.pendingReqs.delete(hash);
992
+ }
993
+ }
994
+ }
995
+ /**
996
+ * Try pending requests with a newly connected peer
997
+ */
998
+ async tryPendingReqs(peer) {
999
+ const peerIdStr = peer.peerId;
1000
+ for (const [hash, reqs] of this.pendingReqs.entries()) {
1001
+ // Find requests that haven't tried this peer yet
1002
+ const untried = reqs.filter(r => !r.triedPeers.has(peerIdStr));
1003
+ if (untried.length === 0)
1004
+ continue;
1005
+ // Mark as tried
1006
+ for (const r of untried) {
1007
+ r.triedPeers.add(peerIdStr);
1008
+ }
1009
+ const data = await peer.request(hash);
1010
+ if (data) {
1011
+ // Store locally
1012
+ if (this.config.localStore) {
1013
+ await this.config.localStore.put(hash, data);
1014
+ }
1015
+ // Resolve all waiting requests
1016
+ for (const r of reqs) {
1017
+ clearTimeout(r.timeout);
1018
+ r.resolve(data);
1019
+ }
1020
+ this.pendingReqs.delete(hash);
1021
+ this.log('Resolved pending req:', hash.slice(0, 16));
1022
+ }
1023
+ }
1024
+ }
1025
+ async has(hash) {
1026
+ // Check local store
1027
+ if (this.config.localStore) {
1028
+ const hasLocal = await this.config.localStore.has(hash);
1029
+ if (hasLocal)
1030
+ return true;
1031
+ }
1032
+ // Could query peers, but for now just check locally
1033
+ return false;
1034
+ }
1035
+ async delete(hash) {
1036
+ // Only delete from local store
1037
+ if (this.config.localStore) {
1038
+ return this.config.localStore.delete(hash);
1039
+ }
1040
+ return false;
1041
+ }
1042
+ }
1043
+ //# sourceMappingURL=store.js.map