holosphere 2.0.0-alpha11 → 2.0.0-alpha12

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 (60) hide show
  1. package/dist/{2019-D2OG2idw.js → 2019-DQdDE6DG.js} +2 -2
  2. package/dist/{2019-D2OG2idw.js.map → 2019-DQdDE6DG.js.map} +1 -1
  3. package/dist/{2019-EION3wKo.cjs → 2019-Ew-DTDlI.cjs} +2 -2
  4. package/dist/{2019-EION3wKo.cjs.map → 2019-Ew-DTDlI.cjs.map} +1 -1
  5. package/dist/{browser-BSniCNqO.js → browser-CckFyRI9.js} +2 -2
  6. package/dist/{browser-BSniCNqO.js.map → browser-CckFyRI9.js.map} +1 -1
  7. package/dist/browser-D2qtVhH5.cjs +2 -0
  8. package/dist/{browser-Cq59Ij19.cjs.map → browser-D2qtVhH5.cjs.map} +1 -1
  9. package/dist/cjs/holosphere.cjs +1 -1
  10. package/dist/esm/holosphere.js +67 -50
  11. package/dist/index-9sqetkAn.cjs +29 -0
  12. package/dist/index-9sqetkAn.cjs.map +1 -0
  13. package/dist/index-C0ITDyFo.cjs +13 -0
  14. package/dist/index-C0ITDyFo.cjs.map +1 -0
  15. package/dist/{index-Bl6rM1NW.js → index-DrYM1LOY.js} +2 -2
  16. package/dist/{index-Bl6rM1NW.js.map → index-DrYM1LOY.js.map} +1 -1
  17. package/dist/{index-D-jZhliX.js → index-TDDyakLc.js} +19827 -720
  18. package/dist/index-TDDyakLc.js.map +1 -0
  19. package/dist/{index-Bwg3OzRM.cjs → index-kyf1sjaC.cjs} +3 -3
  20. package/dist/{index-Bwg3OzRM.cjs.map → index-kyf1sjaC.cjs.map} +1 -1
  21. package/dist/index-lbSQUoRz.js +11663 -0
  22. package/dist/index-lbSQUoRz.js.map +1 -0
  23. package/dist/{indexeddb-storage-5eiUNsHC.js → indexeddb-storage-CXhjqwhA.js} +2 -2
  24. package/dist/{indexeddb-storage-5eiUNsHC.js.map → indexeddb-storage-CXhjqwhA.js.map} +1 -1
  25. package/dist/{indexeddb-storage-FNFUVvTJ.cjs → indexeddb-storage-DFESDYIj.cjs} +2 -2
  26. package/dist/{indexeddb-storage-FNFUVvTJ.cjs.map → indexeddb-storage-DFESDYIj.cjs.map} +1 -1
  27. package/dist/{memory-storage-DMt36uZO.cjs → memory-storage-D1tc1bjk.cjs} +2 -2
  28. package/dist/{memory-storage-DMt36uZO.cjs.map → memory-storage-D1tc1bjk.cjs.map} +1 -1
  29. package/dist/{memory-storage-CI-gfmuG.js → memory-storage-DkewsdcM.js} +2 -2
  30. package/dist/{memory-storage-CI-gfmuG.js.map → memory-storage-DkewsdcM.js.map} +1 -1
  31. package/docs/FEDERATION.md +474 -0
  32. package/package.json +3 -1
  33. package/src/crypto/nostr-utils.js +7 -0
  34. package/src/crypto/secp256k1.js +46 -29
  35. package/src/federation/capabilities.js +162 -0
  36. package/src/federation/card-storage.js +304 -0
  37. package/src/federation/handshake.js +204 -7
  38. package/src/federation/hologram.js +167 -58
  39. package/src/federation/holon-registry.js +166 -0
  40. package/src/federation/index.js +68 -0
  41. package/src/federation/registry.js +110 -2
  42. package/src/federation/request-card.js +373 -0
  43. package/src/hierarchical/upcast.js +19 -3
  44. package/src/index.js +162 -76
  45. package/src/lib/federation-methods.js +527 -5
  46. package/src/storage/nostr-async.js +2 -2
  47. package/src/storage/nostr-client.js +370 -155
  48. package/src/storage/nostr-wrapper.js +1 -1
  49. package/dist/_commonjsHelpers-C37NGDzP.cjs +0 -2
  50. package/dist/_commonjsHelpers-C37NGDzP.cjs.map +0 -1
  51. package/dist/_commonjsHelpers-CUmg6egw.js +0 -7
  52. package/dist/_commonjsHelpers-CUmg6egw.js.map +0 -1
  53. package/dist/browser-Cq59Ij19.cjs +0 -2
  54. package/dist/index-D-jZhliX.js.map +0 -1
  55. package/dist/index-Dc6Z8Aob.cjs +0 -18
  56. package/dist/index-Dc6Z8Aob.cjs.map +0 -1
  57. package/dist/secp256k1-CEwJNcfV.js +0 -1890
  58. package/dist/secp256k1-CEwJNcfV.js.map +0 -1
  59. package/dist/secp256k1-CiEONUnj.cjs +0 -12
  60. package/dist/secp256k1-CiEONUnj.cjs.map +0 -1
@@ -1,46 +1,98 @@
1
1
  /**
2
- * @fileoverview Nostr Client - SimplePool wrapper for relay management.
2
+ * @fileoverview Nostr Client - NDK-based wrapper for relay management.
3
3
  *
4
4
  * Provides connection pooling, relay management, event caching, persistent storage,
5
5
  * outbox queue for guaranteed delivery, and background sync services.
6
+ * Uses @nostr-dev-kit/ndk for improved relay handling and NIP support.
6
7
  *
7
8
  * @module storage/nostr-client
8
9
  */
9
10
 
10
- import { SimplePool, finalizeEvent, getPublicKey } from 'nostr-tools';
11
+ import NDK, { NDKEvent, NDKPrivateKeySigner, NDKSubscriptionCacheUsage } from '@nostr-dev-kit/ndk';
12
+ import { getPublicKey as nostrToolsGetPublicKey, finalizeEvent as nostrToolsFinalizeEvent } from 'nostr-tools';
11
13
  import { OutboxQueue } from './outbox-queue.js';
12
14
  import { SyncService } from './sync-service.js';
13
15
 
14
16
  /**
15
- * Global pool singleton - reuse connections across NostrClient instances.
17
+ * Lazy-load NDK cache adapter for browser environments.
18
+ * Falls back to null if not available (Node.js or package not installed).
16
19
  * @private
17
20
  */
18
- let globalPool = null;
21
+ let NDKCacheAdapterDexie = null;
22
+ let ndkCacheAdapterLoaded = false;
23
+
24
+ async function loadNDKCacheAdapter() {
25
+ if (ndkCacheAdapterLoaded) return NDKCacheAdapterDexie;
26
+ ndkCacheAdapterLoaded = true;
27
+
28
+ // Only try to load in browser environment with IndexedDB support
29
+ if (typeof window !== 'undefined' && typeof indexedDB !== 'undefined') {
30
+ try {
31
+ const module = await import('@nostr-dev-kit/ndk-cache-dexie');
32
+ NDKCacheAdapterDexie = module.default || module.NDKCacheAdapterDexie;
33
+ } catch (e) {
34
+ // Package not available, use fallback caching
35
+ console.debug('[nostr] NDK cache adapter not available, using LRU cache');
36
+ }
37
+ }
38
+ return NDKCacheAdapterDexie;
39
+ }
19
40
 
20
41
  /**
21
- * Set of relays used by the global pool.
42
+ * Global NDK singleton - reuse connections across NostrClient instances.
22
43
  * @private
23
44
  */
24
- let globalPoolRelays = new Set();
45
+ let globalNDK = null;
25
46
 
26
47
  /**
27
- * Get or create global SimplePool singleton.
48
+ * Set of relays used by the global NDK.
49
+ * @private
50
+ */
51
+ let globalNDKRelays = new Set();
52
+
53
+ /**
54
+ * NDK cache adapter instance (shared across clients).
55
+ * @private
56
+ */
57
+ let globalNDKCacheAdapter = null;
58
+
59
+ /**
60
+ * Get or create global NDK singleton.
28
61
  * This ensures WebSocket connections are reused across all operations.
29
62
  *
30
63
  * @private
31
- * @param {Object} [config={}] - Pool configuration
32
- * @param {boolean} [config.enableReconnect=true] - Enable auto-reconnect
33
- * @param {boolean} [config.enablePing=true] - Enable ping/pong
34
- * @returns {SimplePool} The global SimplePool instance
64
+ * @param {Object} [config={}] - NDK configuration
65
+ * @param {string[]} [config.relays=[]] - Array of relay URLs
66
+ * @param {NDKPrivateKeySigner} [config.signer] - NDK signer instance
67
+ * @param {Object} [config.cacheAdapter] - Optional cache adapter instance
68
+ * @returns {NDK} The global NDK instance
35
69
  */
36
- function getGlobalPool(config = {}) {
37
- if (!globalPool) {
38
- globalPool = new SimplePool({
39
- enableReconnect: config.enableReconnect !== false,
40
- enablePing: config.enablePing !== false,
41
- });
70
+ function getGlobalNDK(config = {}) {
71
+ if (!globalNDK) {
72
+ const ndkOptions = {
73
+ explicitRelayUrls: config.relays || [],
74
+ signer: config.signer,
75
+ autoConnectUserRelays: false,
76
+ autoFetchUserMutelist: false,
77
+ };
78
+
79
+ // Use cache adapter if provided
80
+ if (config.cacheAdapter) {
81
+ ndkOptions.cacheAdapter = config.cacheAdapter;
82
+ globalNDKCacheAdapter = config.cacheAdapter;
83
+ }
84
+
85
+ globalNDK = new NDK(ndkOptions);
86
+ } else if (config.relays) {
87
+ // Add any new relays to existing NDK
88
+ for (const relay of config.relays) {
89
+ if (!globalNDKRelays.has(relay)) {
90
+ globalNDK.addExplicitRelay(relay);
91
+ globalNDKRelays.add(relay);
92
+ }
93
+ }
42
94
  }
43
- return globalPool;
95
+ return globalNDK;
44
96
  }
45
97
 
46
98
  /**
@@ -211,7 +263,33 @@ function ensureWebSocket() {
211
263
  import { createPersistentStorage } from './persistent-storage.js';
212
264
 
213
265
  /**
214
- * NostrClient - Manages connections to Nostr relays.
266
+ * Convert hex string to Uint8Array.
267
+ * @private
268
+ * @param {string} hex - Hexadecimal string
269
+ * @returns {Uint8Array} Byte array
270
+ */
271
+ function hexToBytes(hex) {
272
+ const bytes = new Uint8Array(hex.length / 2);
273
+ for (let i = 0; i < hex.length; i += 2) {
274
+ bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
275
+ }
276
+ return bytes;
277
+ }
278
+
279
+ /**
280
+ * Convert Uint8Array to hex string.
281
+ * @private
282
+ * @param {Uint8Array} bytes - Byte array
283
+ * @returns {string} Hexadecimal string
284
+ */
285
+ function bytesToHex(bytes) {
286
+ return Array.from(bytes)
287
+ .map((b) => b.toString(16).padStart(2, '0'))
288
+ .join('');
289
+ }
290
+
291
+ /**
292
+ * NostrClient - Manages connections to Nostr relays using NDK.
215
293
  *
216
294
  * Provides event publishing, querying, subscription management, persistent caching,
217
295
  * and guaranteed delivery via outbox queue.
@@ -252,7 +330,14 @@ export class NostrClient {
252
330
  // IMPORTANT: If you want keys to persist across restarts, you MUST provide
253
331
  // a privateKey in config. Otherwise, a new key is generated each time.
254
332
  this.privateKey = config.privateKey || this._generatePrivateKey();
255
- this.publicKey = getPublicKey(this.privateKey);
333
+
334
+ // Create NDK signer
335
+ this.signer = new NDKPrivateKeySigner(this.privateKey);
336
+
337
+ // Set public key synchronously for immediate access
338
+ // This uses nostr-tools which is synchronous, unlike NDK's async signer.user()
339
+ this.publicKey = nostrToolsGetPublicKey(this.privateKey);
340
+
256
341
  this.config = config;
257
342
 
258
343
  this._subscriptions = new Map();
@@ -265,38 +350,59 @@ export class NostrClient {
265
350
  this._persistTimer = null;
266
351
  this._persistBatchMs = config.persistBatchMs || 100; // Batch writes within 100ms window
267
352
 
268
- // Initialize pool and storage asynchronously
353
+ // Initialize NDK and storage asynchronously
269
354
  this._initReady = this._initialize();
270
355
  }
271
356
 
272
357
  /**
273
- * Initialize WebSocket polyfill and pool.
358
+ * Initialize WebSocket polyfill and NDK.
274
359
  *
275
360
  * @private
276
361
  * @returns {Promise<void>}
277
362
  */
278
363
  async _initialize() {
279
- // Ensure WebSocket is available before initializing pool
364
+ // Ensure WebSocket is available before initializing NDK
280
365
  await ensureWebSocket();
281
366
 
282
- // Initialize SimplePool with options (only if relays exist)
367
+ // Try to load NDK cache adapter for browser environments
368
+ const CacheAdapter = await loadNDKCacheAdapter();
369
+ let cacheAdapter = null;
370
+
371
+ if (CacheAdapter && this.config.appName) {
372
+ try {
373
+ // Create NDK cache adapter with app-specific database name
374
+ cacheAdapter = new CacheAdapter({
375
+ dbName: `holosphere_${this.config.appName}`,
376
+ });
377
+ this._useNDKCache = true;
378
+ } catch (e) {
379
+ console.warn('[nostr] Failed to initialize NDK cache adapter:', e.message);
380
+ }
381
+ }
382
+
383
+ // Initialize NDK with options (only if relays exist)
283
384
  if (this.relays.length > 0) {
284
- // Use global pool singleton to reuse WebSocket connections
285
- this.pool = getGlobalPool(this.config);
385
+ // Use global NDK singleton to reuse WebSocket connections
386
+ this.ndk = getGlobalNDK({
387
+ relays: this.relays,
388
+ signer: this.signer,
389
+ cacheAdapter,
390
+ });
286
391
 
287
392
  // Track relays used by this client
288
- this.relays.forEach(r => globalPoolRelays.add(r));
393
+ this.relays.forEach(r => globalNDKRelays.add(r));
394
+
395
+ // Connect to relays
396
+ await this.ndk.connect();
397
+
398
+ // Note: publicKey is already set synchronously in constructor using nostr-tools
289
399
  } else {
290
- // Mock pool for testing - returns mock promise that resolves immediately
291
- this.pool = {
292
- publish: (relays, event) => [Promise.resolve()],
293
- querySync: (relays, filter, options) => Promise.resolve([]),
294
- subscribeMany: (relays, filters, opts) => ({ close: () => {} }),
295
- close: (relays) => {},
296
- };
400
+ // Mock NDK for testing - returns mock promises that resolve immediately
401
+ this.ndk = null;
402
+ // Note: publicKey is already set synchronously in constructor using nostr-tools
297
403
  }
298
404
 
299
- // Initialize persistent storage
405
+ // Initialize persistent storage (used for outbox queue and fallback caching)
300
406
  await this._initPersistentStorage();
301
407
 
302
408
  // Start long-lived subscription for real-time cache updates
@@ -339,28 +445,29 @@ export class NostrClient {
339
445
  authors: [this.publicKey],
340
446
  };
341
447
 
342
- const sub = this.pool.subscribeMany(
343
- this.relays,
344
- [filter],
345
- {
346
- onevent: (event) => {
347
- // Verify author (relay may not respect filter)
348
- if (event.pubkey !== this.publicKey) {
349
- return;
350
- }
448
+ const sub = this.ndk.subscribe(filter, {
449
+ closeOnEose: false,
450
+ cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
451
+ });
351
452
 
352
- // Cache the event - this keeps our local cache in sync
353
- this._cacheEvent(event);
354
- },
355
- oneose: () => {
356
- // End of stored events - initial load complete
357
- if (!subInfo.initialized) {
358
- subInfo.initialized = true;
359
- subInfo.initResolve();
360
- }
361
- },
453
+ sub.on('event', (ndkEvent) => {
454
+ // Verify author (relay may not respect filter)
455
+ if (ndkEvent.pubkey !== this.publicKey) {
456
+ return;
362
457
  }
363
- );
458
+
459
+ // Convert NDKEvent to raw event format and cache it
460
+ const event = ndkEvent.rawEvent();
461
+ this._cacheEvent(event);
462
+ });
463
+
464
+ sub.on('eose', () => {
465
+ // End of stored events - initial load complete
466
+ if (!subInfo.initialized) {
467
+ subInfo.initialized = true;
468
+ subInfo.initResolve();
469
+ }
470
+ });
364
471
 
365
472
  subInfo.subscription = sub;
366
473
 
@@ -600,11 +707,53 @@ export class NostrClient {
600
707
  pendingWrites.set(dTagPath, { event, timer, resolve, reject });
601
708
 
602
709
  // Cache immediately for local-first reads (even before relay publish)
603
- const signedEvent = finalizeEvent(event, this.privateKey);
710
+ const signedEvent = this._createSignedEvent(event);
604
711
  this._cacheEvent(signedEvent);
605
712
  });
606
713
  }
607
714
 
715
+ /**
716
+ * Create a signed event using NDK or nostr-tools fallback.
717
+ * @private
718
+ * @param {Object} event - Unsigned event
719
+ * @returns {Object} Signed event
720
+ */
721
+ _createSignedEvent(event) {
722
+ // If NDK is available and we have a signer, use NDKEvent
723
+ if (this.ndk) {
724
+ const ndkEvent = new NDKEvent(this.ndk);
725
+ ndkEvent.kind = event.kind;
726
+ ndkEvent.content = event.content;
727
+ ndkEvent.tags = event.tags || [];
728
+ ndkEvent.created_at = event.created_at || Math.floor(Date.now() / 1000);
729
+
730
+ // NDK will sign when publishing, for now return raw event format
731
+ return {
732
+ kind: ndkEvent.kind,
733
+ content: ndkEvent.content,
734
+ tags: ndkEvent.tags,
735
+ created_at: ndkEvent.created_at,
736
+ pubkey: this.publicKey,
737
+ id: '', // Will be set during actual publish
738
+ sig: '', // Will be set during actual publish
739
+ };
740
+ }
741
+
742
+ // Mock mode - use nostr-tools to create a properly signed event
743
+ try {
744
+ return nostrToolsFinalizeEvent(event, hexToBytes(this.privateKey));
745
+ } catch (e) {
746
+ // Return mock signed event if signing fails
747
+ return {
748
+ ...event,
749
+ pubkey: this.publicKey,
750
+ id: Math.random().toString(16).slice(2).padStart(64, '0'),
751
+ sig: 'mock-signature',
752
+ created_at: event.created_at || Math.floor(Date.now() / 1000),
753
+ };
754
+ }
755
+ }
756
+
608
757
  /**
609
758
  * Internal publish implementation
610
759
  * @private
@@ -613,10 +762,24 @@ export class NostrClient {
613
762
  const waitForRelays = options.waitForRelays || false;
614
763
 
615
764
  // Check if event is already signed (has id and sig)
616
- // If so, use it as-is; otherwise sign it
617
- const signedEvent = (event.id && event.sig)
618
- ? event
619
- : finalizeEvent(event, this.privateKey);
765
+ let signedEvent;
766
+ if (event.id && event.sig) {
767
+ signedEvent = event;
768
+ } else if (this.ndk) {
769
+ // Use NDK to create and sign the event
770
+ const ndkEvent = new NDKEvent(this.ndk);
771
+ ndkEvent.kind = event.kind;
772
+ ndkEvent.content = event.content;
773
+ ndkEvent.tags = event.tags || [];
774
+ ndkEvent.created_at = event.created_at || Math.floor(Date.now() / 1000);
775
+
776
+ // Sign the event
777
+ await ndkEvent.sign(this.signer);
778
+ signedEvent = ndkEvent.rawEvent();
779
+ } else {
780
+ // Mock mode - create mock signed event
781
+ signedEvent = this._createSignedEvent(event);
782
+ }
620
783
 
621
784
  // 1. Cache the event locally first (this makes reads instant)
622
785
  await this._cacheEvent(signedEvent);
@@ -627,28 +790,37 @@ export class NostrClient {
627
790
  }
628
791
 
629
792
  // 3. Publish to relays
630
- if (waitForRelays) {
631
- // Wait for relay confirmation if explicitly requested
632
- const deliveryResult = await this._attemptDelivery(signedEvent, this.relays);
633
-
634
- return {
635
- event: signedEvent,
636
- results: deliveryResult.results,
637
- queued: !!this.outboxQueue,
638
- };
639
- } else {
640
- // Fire and forget - publish in background, return immediately
641
- this._attemptDelivery(signedEvent, this.relays).catch(() => {
642
- // Silent - event is cached locally and queued for retry
643
- });
793
+ if (this.ndk && this.relays.length > 0) {
794
+ if (waitForRelays) {
795
+ // Wait for relay confirmation if explicitly requested
796
+ const deliveryResult = await this._attemptDelivery(signedEvent, this.relays);
797
+
798
+ return {
799
+ event: signedEvent,
800
+ results: deliveryResult.results,
801
+ queued: !!this.outboxQueue,
802
+ };
803
+ } else {
804
+ // Fire and forget - publish in background, return immediately
805
+ this._attemptDelivery(signedEvent, this.relays).catch(() => {
806
+ // Silent - event is cached locally and queued for retry
807
+ });
644
808
 
645
- // Return immediately (data is in local cache and queued for delivery)
646
- return {
647
- event: signedEvent,
648
- results: [],
649
- queued: !!this.outboxQueue,
650
- };
809
+ // Return immediately (data is in local cache and queued for delivery)
810
+ return {
811
+ event: signedEvent,
812
+ results: [],
813
+ queued: !!this.outboxQueue,
814
+ };
815
+ }
651
816
  }
817
+
818
+ // No relays configured - just return the signed event
819
+ return {
820
+ event: signedEvent,
821
+ results: [],
822
+ queued: !!this.outboxQueue,
823
+ };
652
824
  }
653
825
 
654
826
  /**
@@ -659,28 +831,50 @@ export class NostrClient {
659
831
  * @returns {Promise<Object>} Delivery result with successful/failed relays
660
832
  */
661
833
  async _attemptDelivery(event, relays) {
662
- const results = await Promise.allSettled(
663
- this.pool.publish(relays, event)
664
- );
834
+ if (!this.ndk) {
835
+ return { successful: [], failed: relays, results: [] };
836
+ }
837
+
838
+ const ndkEvent = new NDKEvent(this.ndk, event);
665
839
 
666
840
  const successful = [];
667
841
  const failed = [];
668
842
  const formattedResults = [];
669
843
 
670
- results.forEach((r, i) => {
671
- formattedResults.push({
672
- relay: relays[i],
673
- status: r.status,
674
- value: r.value,
675
- reason: r.reason,
676
- });
677
-
678
- if (r.status === 'fulfilled') {
679
- successful.push(relays[i]);
680
- } else {
681
- failed.push(relays[i]);
844
+ try {
845
+ // Publish using NDK
846
+ const publishedRelays = await ndkEvent.publish();
847
+
848
+ // Track results
849
+ for (const relay of relays) {
850
+ const wasPublished = publishedRelays.has(this.ndk.pool.getRelay(relay));
851
+ if (wasPublished) {
852
+ successful.push(relay);
853
+ formattedResults.push({
854
+ relay,
855
+ status: 'fulfilled',
856
+ value: true,
857
+ });
858
+ } else {
859
+ failed.push(relay);
860
+ formattedResults.push({
861
+ relay,
862
+ status: 'rejected',
863
+ reason: 'Not published to relay',
864
+ });
865
+ }
682
866
  }
683
- });
867
+ } catch (error) {
868
+ // All failed
869
+ for (const relay of relays) {
870
+ failed.push(relay);
871
+ formattedResults.push({
872
+ relay,
873
+ status: 'rejected',
874
+ reason: error.message,
875
+ });
876
+ }
877
+ }
684
878
 
685
879
  // Update outbox queue with delivery status
686
880
  if (this.outboxQueue) {
@@ -689,16 +883,12 @@ export class NostrClient {
689
883
 
690
884
  // Log failures only at debug level (common during network issues)
691
885
  if (failed.length > 0 && successful.length === 0) {
692
- // Only warn if ALL relays failed (likely a real issue)
693
- const reasons = results
886
+ const reasons = formattedResults
694
887
  .filter(r => r.status === 'rejected')
695
- .map(f => f.reason?.message || f.reason || 'unknown')
888
+ .map(f => f.reason || 'unknown')
696
889
  .join(', ');
697
- // Use debug level for timeout issues (very common)
698
- if (reasons.includes('timed out')) {
699
- // Silent - event is queued for retry anyway
700
- } else {
701
- console.warn(`[nostr] All relays failed for ${event.id.slice(0, 8)}: ${reasons}`);
890
+ if (!reasons.includes('timed out')) {
891
+ console.warn(`[nostr] All relays failed for ${event.id?.slice(0, 8)}: ${reasons}`);
702
892
  }
703
893
  }
704
894
 
@@ -732,7 +922,7 @@ export class NostrClient {
732
922
  const forceRelay = options.forceRelay === true;
733
923
 
734
924
  // If no relays, query from cache only
735
- if (this.relays.length === 0) {
925
+ if (this.relays.length === 0 || !this.ndk) {
736
926
  const matchingEvents = this._getMatchingCachedEvents(filter);
737
927
  return matchingEvents;
738
928
  }
@@ -812,7 +1002,17 @@ export class NostrClient {
812
1002
  // Create the query promise
813
1003
  const queryPromise = (async () => {
814
1004
  try {
815
- let events = await this.pool.querySync(this.relays, filter, { timeout });
1005
+ let events = [];
1006
+
1007
+ if (this.ndk) {
1008
+ // Use NDK to fetch events
1009
+ const fetchedEvents = await this.ndk.fetchEvents(filter, {
1010
+ closeOnEose: true,
1011
+ });
1012
+
1013
+ // Convert NDKEvent Set to raw event array
1014
+ events = Array.from(fetchedEvents).map(e => e.rawEvent());
1015
+ }
816
1016
 
817
1017
  // CRITICAL: Filter out events from other authors (relay may not respect filter)
818
1018
  if (filter.authors && filter.authors.length > 0) {
@@ -925,7 +1125,7 @@ export class NostrClient {
925
1125
  * @private
926
1126
  */
927
1127
  async _doBackgroundPathRefresh(path, kind, options) {
928
- if (this.relays.length === 0) return;
1128
+ if (this.relays.length === 0 || !this.ndk) return;
929
1129
 
930
1130
  // Throttle: Skip if we've refreshed this path recently
931
1131
  const lastRefresh = backgroundRefreshThrottle.get(path);
@@ -954,7 +1154,7 @@ export class NostrClient {
954
1154
  };
955
1155
  const cacheKey = JSON.stringify(filter);
956
1156
 
957
- // Use our query deduplication by calling query() instead of pool.querySync() directly
1157
+ // Use our query deduplication by calling query() instead of direct fetch
958
1158
  const timeout = options.timeout || 30000;
959
1159
  const events = await this._queryRelaysAndCache(filter, cacheKey, timeout);
960
1160
 
@@ -988,7 +1188,7 @@ export class NostrClient {
988
1188
  * @private
989
1189
  */
990
1190
  async _doBackgroundPrefixRefresh(prefix, kind, options) {
991
- if (this.relays.length === 0) return;
1191
+ if (this.relays.length === 0 || !this.ndk) return;
992
1192
 
993
1193
  // Throttle: Skip if we've refreshed this prefix recently
994
1194
  const throttleKey = `prefix:${prefix}`;
@@ -1047,14 +1247,17 @@ export class NostrClient {
1047
1247
  const localEvents = this._getMatchingCachedEvents(filter);
1048
1248
 
1049
1249
  // If no relays configured, return local only
1050
- if (this.relays.length === 0) {
1250
+ if (this.relays.length === 0 || !this.ndk) {
1051
1251
  return localEvents;
1052
1252
  }
1053
1253
 
1054
1254
  // Step 2: Query relays
1055
1255
  let relayEvents = [];
1056
1256
  try {
1057
- relayEvents = await this.pool.querySync(this.relays, filter, { timeout });
1257
+ const fetchedEvents = await this.ndk.fetchEvents(filter, {
1258
+ closeOnEose: true,
1259
+ });
1260
+ relayEvents = Array.from(fetchedEvents).map(e => e.rawEvent());
1058
1261
 
1059
1262
  // Filter out events from other authors
1060
1263
  if (filter.authors && filter.authors.length > 0) {
@@ -1142,7 +1345,7 @@ export class NostrClient {
1142
1345
  const filterKey = JSON.stringify(filter);
1143
1346
 
1144
1347
  // If no relays, check cache for matching events and trigger callbacks
1145
- if (this.relays.length === 0) {
1348
+ if (this.relays.length === 0 || !this.ndk) {
1146
1349
  // Trigger with cached events that match the filter
1147
1350
  const matchingEvents = this._getMatchingCachedEvents(filter);
1148
1351
 
@@ -1192,8 +1395,8 @@ export class NostrClient {
1192
1395
 
1193
1396
  // Only close actual subscription when no more callbacks
1194
1397
  if (existing.refCount === 0) {
1195
- if (existing.subscription && existing.subscription.close) {
1196
- existing.subscription.close();
1398
+ if (existing.subscription && existing.subscription.stop) {
1399
+ existing.subscription.stop();
1197
1400
  }
1198
1401
  activeSubscriptions.delete(filterKey);
1199
1402
  }
@@ -1213,39 +1416,41 @@ export class NostrClient {
1213
1416
  // Store before creating subscription to handle race conditions
1214
1417
  activeSubscriptions.set(filterKey, subscriptionInfo);
1215
1418
 
1216
- const sub = this.pool.subscribeMany(
1217
- this.relays,
1218
- [filter],
1219
- {
1220
- onevent: (event) => {
1221
- // CRITICAL: Filter out events from other authors immediately
1222
- // Many relays don't respect the authors filter, so we must do it client-side
1223
- if (filter.authors && filter.authors.length > 0) {
1224
- if (!filter.authors.includes(event.pubkey)) {
1225
- // Silently reject events from other authors
1226
- return;
1227
- }
1228
- }
1419
+ const sub = this.ndk.subscribe(filter, {
1420
+ closeOnEose: false,
1421
+ cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
1422
+ });
1229
1423
 
1230
- this._cacheEvent(event);
1424
+ sub.on('event', (ndkEvent) => {
1425
+ const event = ndkEvent.rawEvent();
1231
1426
 
1232
- // Dispatch to ALL registered callbacks for this subscription
1233
- const subInfo = activeSubscriptions.get(filterKey);
1234
- if (subInfo) {
1235
- for (const cb of subInfo.callbacks) {
1236
- try {
1237
- cb(event);
1238
- } catch (err) {
1239
- console.warn('[nostr] Subscription callback error:', err.message);
1240
- }
1241
- }
1427
+ // CRITICAL: Filter out events from other authors immediately
1428
+ // Many relays don't respect the authors filter, so we must do it client-side
1429
+ if (filter.authors && filter.authors.length > 0) {
1430
+ if (!filter.authors.includes(event.pubkey)) {
1431
+ // Silently reject events from other authors
1432
+ return;
1433
+ }
1434
+ }
1435
+
1436
+ this._cacheEvent(event);
1437
+
1438
+ // Dispatch to ALL registered callbacks for this subscription
1439
+ const subInfo = activeSubscriptions.get(filterKey);
1440
+ if (subInfo) {
1441
+ for (const cb of subInfo.callbacks) {
1442
+ try {
1443
+ cb(event);
1444
+ } catch (err) {
1445
+ console.warn('[nostr] Subscription callback error:', err.message);
1242
1446
  }
1243
- },
1244
- oneose: () => {
1245
- if (options.onEOSE) options.onEOSE();
1246
- },
1447
+ }
1247
1448
  }
1248
- );
1449
+ });
1450
+
1451
+ sub.on('eose', () => {
1452
+ if (options.onEOSE) options.onEOSE();
1453
+ });
1249
1454
 
1250
1455
  // Store the actual subscription object
1251
1456
  subscriptionInfo.subscription = sub;
@@ -1259,7 +1464,7 @@ export class NostrClient {
1259
1464
 
1260
1465
  // Only close actual subscription when no more callbacks
1261
1466
  if (subscriptionInfo.refCount === 0) {
1262
- if (sub.close) sub.close();
1467
+ if (sub.stop) sub.stop();
1263
1468
  activeSubscriptions.delete(filterKey);
1264
1469
  }
1265
1470
  this._subscriptions.delete(subId);
@@ -1379,7 +1584,7 @@ export class NostrClient {
1379
1584
  }
1380
1585
 
1381
1586
  // Trigger any active subscriptions that match this event
1382
- if (this.relays.length === 0) {
1587
+ if (this.relays.length === 0 || !this.ndk) {
1383
1588
  for (const sub of this._subscriptions.values()) {
1384
1589
  if (sub.active && this._eventMatchesFilter(event, sub.filter)) {
1385
1590
  // Trigger callback asynchronously, but check active status again
@@ -1559,16 +1764,16 @@ export class NostrClient {
1559
1764
  // Close long-lived author subscription
1560
1765
  const authorSub = authorSubscriptions.get(this.publicKey);
1561
1766
  if (authorSub && authorSub.subscription) {
1562
- if (authorSub.subscription.close) {
1563
- authorSub.subscription.close();
1767
+ if (authorSub.subscription.stop) {
1768
+ authorSub.subscription.stop();
1564
1769
  }
1565
1770
  authorSubscriptions.delete(this.publicKey);
1566
1771
  }
1567
1772
 
1568
1773
  // Close all subscriptions
1569
1774
  for (const sub of this._subscriptions.values()) {
1570
- if (sub.close) {
1571
- sub.close();
1775
+ if (sub.stop) {
1776
+ sub.stop();
1572
1777
  } else if (sub.active !== undefined) {
1573
1778
  // Mock subscription
1574
1779
  sub.active = false;
@@ -1576,8 +1781,8 @@ export class NostrClient {
1576
1781
  }
1577
1782
  this._subscriptions.clear();
1578
1783
 
1579
- // Close pool
1580
- this.pool.close(this.relays);
1784
+ // Note: We don't close the global NDK as it may be used by other clients
1785
+ // The pool connections will be cleaned up when the process exits
1581
1786
 
1582
1787
  // Clear cache and index
1583
1788
  this._eventCache.clear();
@@ -1589,10 +1794,20 @@ export class NostrClient {
1589
1794
  * @returns {Array} Relay connection statuses
1590
1795
  */
1591
1796
  getRelayStatus() {
1592
- return this.relays.map(relay => ({
1593
- url: relay,
1594
- connected: true, // SimplePool manages this internally
1595
- }));
1797
+ if (!this.ndk) {
1798
+ return this.relays.map(relay => ({
1799
+ url: relay,
1800
+ connected: false,
1801
+ }));
1802
+ }
1803
+
1804
+ return this.relays.map(relay => {
1805
+ const ndkRelay = this.ndk.pool.getRelay(relay);
1806
+ return {
1807
+ url: relay,
1808
+ connected: ndkRelay?.status === 1, // WebSocket.OPEN
1809
+ };
1810
+ });
1596
1811
  }
1597
1812
  }
1598
1813