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.
- package/dist/{2019-D2OG2idw.js → 2019-DQdDE6DG.js} +2 -2
- package/dist/{2019-D2OG2idw.js.map → 2019-DQdDE6DG.js.map} +1 -1
- package/dist/{2019-EION3wKo.cjs → 2019-Ew-DTDlI.cjs} +2 -2
- package/dist/{2019-EION3wKo.cjs.map → 2019-Ew-DTDlI.cjs.map} +1 -1
- package/dist/{browser-BSniCNqO.js → browser-CckFyRI9.js} +2 -2
- package/dist/{browser-BSniCNqO.js.map → browser-CckFyRI9.js.map} +1 -1
- package/dist/browser-D2qtVhH5.cjs +2 -0
- package/dist/{browser-Cq59Ij19.cjs.map → browser-D2qtVhH5.cjs.map} +1 -1
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +67 -50
- package/dist/index-9sqetkAn.cjs +29 -0
- package/dist/index-9sqetkAn.cjs.map +1 -0
- package/dist/index-C0ITDyFo.cjs +13 -0
- package/dist/index-C0ITDyFo.cjs.map +1 -0
- package/dist/{index-Bl6rM1NW.js → index-DrYM1LOY.js} +2 -2
- package/dist/{index-Bl6rM1NW.js.map → index-DrYM1LOY.js.map} +1 -1
- package/dist/{index-D-jZhliX.js → index-TDDyakLc.js} +19827 -720
- package/dist/index-TDDyakLc.js.map +1 -0
- package/dist/{index-Bwg3OzRM.cjs → index-kyf1sjaC.cjs} +3 -3
- package/dist/{index-Bwg3OzRM.cjs.map → index-kyf1sjaC.cjs.map} +1 -1
- package/dist/index-lbSQUoRz.js +11663 -0
- package/dist/index-lbSQUoRz.js.map +1 -0
- package/dist/{indexeddb-storage-5eiUNsHC.js → indexeddb-storage-CXhjqwhA.js} +2 -2
- package/dist/{indexeddb-storage-5eiUNsHC.js.map → indexeddb-storage-CXhjqwhA.js.map} +1 -1
- package/dist/{indexeddb-storage-FNFUVvTJ.cjs → indexeddb-storage-DFESDYIj.cjs} +2 -2
- package/dist/{indexeddb-storage-FNFUVvTJ.cjs.map → indexeddb-storage-DFESDYIj.cjs.map} +1 -1
- package/dist/{memory-storage-DMt36uZO.cjs → memory-storage-D1tc1bjk.cjs} +2 -2
- package/dist/{memory-storage-DMt36uZO.cjs.map → memory-storage-D1tc1bjk.cjs.map} +1 -1
- package/dist/{memory-storage-CI-gfmuG.js → memory-storage-DkewsdcM.js} +2 -2
- package/dist/{memory-storage-CI-gfmuG.js.map → memory-storage-DkewsdcM.js.map} +1 -1
- package/docs/FEDERATION.md +474 -0
- package/package.json +3 -1
- package/src/crypto/nostr-utils.js +7 -0
- package/src/crypto/secp256k1.js +46 -29
- package/src/federation/capabilities.js +162 -0
- package/src/federation/card-storage.js +304 -0
- package/src/federation/handshake.js +204 -7
- package/src/federation/hologram.js +167 -58
- package/src/federation/holon-registry.js +166 -0
- package/src/federation/index.js +68 -0
- package/src/federation/registry.js +110 -2
- package/src/federation/request-card.js +373 -0
- package/src/hierarchical/upcast.js +19 -3
- package/src/index.js +162 -76
- package/src/lib/federation-methods.js +527 -5
- package/src/storage/nostr-async.js +2 -2
- package/src/storage/nostr-client.js +370 -155
- package/src/storage/nostr-wrapper.js +1 -1
- package/dist/_commonjsHelpers-C37NGDzP.cjs +0 -2
- package/dist/_commonjsHelpers-C37NGDzP.cjs.map +0 -1
- package/dist/_commonjsHelpers-CUmg6egw.js +0 -7
- package/dist/_commonjsHelpers-CUmg6egw.js.map +0 -1
- package/dist/browser-Cq59Ij19.cjs +0 -2
- package/dist/index-D-jZhliX.js.map +0 -1
- package/dist/index-Dc6Z8Aob.cjs +0 -18
- package/dist/index-Dc6Z8Aob.cjs.map +0 -1
- package/dist/secp256k1-CEwJNcfV.js +0 -1890
- package/dist/secp256k1-CEwJNcfV.js.map +0 -1
- package/dist/secp256k1-CiEONUnj.cjs +0 -12
- package/dist/secp256k1-CiEONUnj.cjs.map +0 -1
|
@@ -1,46 +1,98 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Nostr Client -
|
|
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 {
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
42
|
+
* Global NDK singleton - reuse connections across NostrClient instances.
|
|
22
43
|
* @private
|
|
23
44
|
*/
|
|
24
|
-
let
|
|
45
|
+
let globalNDK = null;
|
|
25
46
|
|
|
26
47
|
/**
|
|
27
|
-
*
|
|
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={}] -
|
|
32
|
-
* @param {
|
|
33
|
-
* @param {
|
|
34
|
-
* @
|
|
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
|
|
37
|
-
if (!
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
353
|
+
// Initialize NDK and storage asynchronously
|
|
269
354
|
this._initReady = this._initialize();
|
|
270
355
|
}
|
|
271
356
|
|
|
272
357
|
/**
|
|
273
|
-
* Initialize WebSocket polyfill and
|
|
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
|
|
364
|
+
// Ensure WebSocket is available before initializing NDK
|
|
280
365
|
await ensureWebSocket();
|
|
281
366
|
|
|
282
|
-
//
|
|
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
|
|
285
|
-
this.
|
|
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 =>
|
|
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
|
|
291
|
-
this.
|
|
292
|
-
|
|
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.
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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 =
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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 (
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
663
|
-
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
693
|
-
const reasons = results
|
|
886
|
+
const reasons = formattedResults
|
|
694
887
|
.filter(r => r.status === 'rejected')
|
|
695
|
-
.map(f => f.reason
|
|
888
|
+
.map(f => f.reason || 'unknown')
|
|
696
889
|
.join(', ');
|
|
697
|
-
|
|
698
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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.
|
|
1196
|
-
existing.subscription.
|
|
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.
|
|
1217
|
-
|
|
1218
|
-
|
|
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
|
-
|
|
1424
|
+
sub.on('event', (ndkEvent) => {
|
|
1425
|
+
const event = ndkEvent.rawEvent();
|
|
1231
1426
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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.
|
|
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.
|
|
1563
|
-
authorSub.subscription.
|
|
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.
|
|
1571
|
-
sub.
|
|
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
|
-
//
|
|
1580
|
-
|
|
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
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
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
|
|