holosphere 2.0.0-alpha11 → 2.0.0-alpha13
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-CLMqIAfQ.js} +1722 -1668
- package/dist/{2019-D2OG2idw.js.map → 2019-CLMqIAfQ.js.map} +1 -1
- package/dist/2019-Cp3uYhyY.cjs +8 -0
- package/dist/{2019-EION3wKo.cjs.map → 2019-Cp3uYhyY.cjs.map} +1 -1
- package/dist/browser-D6cNVl0v.cjs +2 -0
- package/dist/{browser-Cq59Ij19.cjs.map → browser-D6cNVl0v.cjs.map} +1 -1
- package/dist/{browser-BSniCNqO.js → browser-nUQt1cnB.js} +2 -2
- package/dist/{browser-BSniCNqO.js.map → browser-nUQt1cnB.js.map} +1 -1
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +67 -50
- package/dist/{index-D-jZhliX.js → index-BN_uoxQK.js} +20324 -735
- package/dist/index-BN_uoxQK.js.map +1 -0
- package/dist/{index-Bl6rM1NW.js → index-CoAjtqsD.js} +2 -2
- package/dist/{index-Bl6rM1NW.js.map → index-CoAjtqsD.js.map} +1 -1
- package/dist/{index-Bwg3OzRM.cjs → index-Cp3tI53z.cjs} +3 -3
- package/dist/{index-Bwg3OzRM.cjs.map → index-Cp3tI53z.cjs.map} +1 -1
- package/dist/index-DJjGSwXG.cjs +13 -0
- package/dist/index-DJjGSwXG.cjs.map +1 -0
- package/dist/index-V8EHMYEY.cjs +29 -0
- package/dist/index-V8EHMYEY.cjs.map +1 -0
- package/dist/index-Z5TstN1e.js +11663 -0
- package/dist/index-Z5TstN1e.js.map +1 -0
- package/dist/indexeddb-storage-CZK5A7XH.cjs +2 -0
- package/dist/indexeddb-storage-CZK5A7XH.cjs.map +1 -0
- package/dist/{indexeddb-storage-5eiUNsHC.js → indexeddb-storage-bpA01pAU.js} +39 -2
- package/dist/indexeddb-storage-bpA01pAU.js.map +1 -0
- package/dist/{memory-storage-DMt36uZO.cjs → memory-storage-B1k8Jszd.cjs} +2 -2
- package/dist/{memory-storage-DMt36uZO.cjs.map → memory-storage-B1k8Jszd.cjs.map} +1 -1
- package/dist/{memory-storage-CI-gfmuG.js → memory-storage-BqhmytP_.js} +2 -2
- package/dist/{memory-storage-CI-gfmuG.js.map → memory-storage-BqhmytP_.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 +104 -38
- package/src/federation/capabilities.js +162 -0
- package/src/federation/card-storage.js +376 -0
- package/src/federation/handshake.js +561 -9
- package/src/federation/hologram.js +194 -57
- package/src/federation/holon-registry.js +187 -0
- package/src/federation/index.js +68 -0
- package/src/federation/registry.js +164 -6
- package/src/federation/request-card.js +373 -0
- package/src/hierarchical/upcast.js +19 -3
- package/src/index.js +209 -75
- package/src/lib/federation-methods.js +527 -5
- package/src/storage/indexeddb-storage.js +41 -0
- package/src/storage/nostr-async.js +14 -5
- package/src/storage/nostr-client.js +471 -155
- package/src/storage/nostr-wrapper.js +6 -3
- package/dist/2019-EION3wKo.cjs +0 -8
- 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/indexeddb-storage-5eiUNsHC.js.map +0 -1
- package/dist/indexeddb-storage-FNFUVvTJ.cjs +0 -2
- package/dist/indexeddb-storage-FNFUVvTJ.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,72 @@ 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
|
+
// Use distinct name from IndexedDBStorage to avoid conflicts
|
|
373
|
+
const dbName = `holosphere_ndk_${this.config.appName}`;
|
|
374
|
+
|
|
375
|
+
// Pre-emptively check for and clear incompatible NDK cache database
|
|
376
|
+
// This prevents async Dexie UpgradeError crashes during background cache operations
|
|
377
|
+
await this._clearIncompatibleNDKCache(dbName);
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
// Create NDK cache adapter with app-specific database name
|
|
381
|
+
cacheAdapter = new CacheAdapter({ dbName });
|
|
382
|
+
this._useNDKCache = true;
|
|
383
|
+
} catch (e) {
|
|
384
|
+
console.warn('[nostr] Failed to initialize NDK cache adapter:', e.message);
|
|
385
|
+
// Try once more after clearing the database
|
|
386
|
+
await this._deleteDexieDatabase(dbName);
|
|
387
|
+
try {
|
|
388
|
+
cacheAdapter = new CacheAdapter({ dbName });
|
|
389
|
+
this._useNDKCache = true;
|
|
390
|
+
} catch (retryError) {
|
|
391
|
+
console.warn('[nostr] Failed to initialize NDK cache adapter after reset:', retryError.message);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Initialize NDK with options (only if relays exist)
|
|
283
397
|
if (this.relays.length > 0) {
|
|
284
|
-
// Use global
|
|
285
|
-
this.
|
|
398
|
+
// Use global NDK singleton to reuse WebSocket connections
|
|
399
|
+
this.ndk = getGlobalNDK({
|
|
400
|
+
relays: this.relays,
|
|
401
|
+
signer: this.signer,
|
|
402
|
+
cacheAdapter,
|
|
403
|
+
});
|
|
286
404
|
|
|
287
405
|
// Track relays used by this client
|
|
288
|
-
this.relays.forEach(r =>
|
|
406
|
+
this.relays.forEach(r => globalNDKRelays.add(r));
|
|
407
|
+
|
|
408
|
+
// Connect to relays
|
|
409
|
+
await this.ndk.connect();
|
|
410
|
+
|
|
411
|
+
// Note: publicKey is already set synchronously in constructor using nostr-tools
|
|
289
412
|
} else {
|
|
290
|
-
// Mock
|
|
291
|
-
this.
|
|
292
|
-
|
|
293
|
-
querySync: (relays, filter, options) => Promise.resolve([]),
|
|
294
|
-
subscribeMany: (relays, filters, opts) => ({ close: () => {} }),
|
|
295
|
-
close: (relays) => {},
|
|
296
|
-
};
|
|
413
|
+
// Mock NDK for testing - returns mock promises that resolve immediately
|
|
414
|
+
this.ndk = null;
|
|
415
|
+
// Note: publicKey is already set synchronously in constructor using nostr-tools
|
|
297
416
|
}
|
|
298
417
|
|
|
299
|
-
// Initialize persistent storage
|
|
418
|
+
// Initialize persistent storage (used for outbox queue and fallback caching)
|
|
300
419
|
await this._initPersistentStorage();
|
|
301
420
|
|
|
302
421
|
// Start long-lived subscription for real-time cache updates
|
|
@@ -339,28 +458,29 @@ export class NostrClient {
|
|
|
339
458
|
authors: [this.publicKey],
|
|
340
459
|
};
|
|
341
460
|
|
|
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
|
-
}
|
|
461
|
+
const sub = this.ndk.subscribe(filter, {
|
|
462
|
+
closeOnEose: false,
|
|
463
|
+
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
|
|
464
|
+
});
|
|
351
465
|
|
|
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
|
-
},
|
|
466
|
+
sub.on('event', (ndkEvent) => {
|
|
467
|
+
// Verify author (relay may not respect filter)
|
|
468
|
+
if (ndkEvent.pubkey !== this.publicKey) {
|
|
469
|
+
return;
|
|
362
470
|
}
|
|
363
|
-
|
|
471
|
+
|
|
472
|
+
// Convert NDKEvent to raw event format and cache it
|
|
473
|
+
const event = ndkEvent.rawEvent();
|
|
474
|
+
this._cacheEvent(event);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
sub.on('eose', () => {
|
|
478
|
+
// End of stored events - initial load complete
|
|
479
|
+
if (!subInfo.initialized) {
|
|
480
|
+
subInfo.initialized = true;
|
|
481
|
+
subInfo.initResolve();
|
|
482
|
+
}
|
|
483
|
+
});
|
|
364
484
|
|
|
365
485
|
subInfo.subscription = sub;
|
|
366
486
|
|
|
@@ -422,6 +542,83 @@ export class NostrClient {
|
|
|
422
542
|
}
|
|
423
543
|
}
|
|
424
544
|
|
|
545
|
+
/**
|
|
546
|
+
* Check if NDK cache database exists with potentially incompatible schema.
|
|
547
|
+
* NDK cache adapter uses Dexie which can't handle primary key changes.
|
|
548
|
+
* We detect this by checking if the database exists and clearing it if needed.
|
|
549
|
+
* @param {string} dbName - The database name to check
|
|
550
|
+
* @returns {Promise<void>}
|
|
551
|
+
* @private
|
|
552
|
+
*/
|
|
553
|
+
async _clearIncompatibleNDKCache(dbName) {
|
|
554
|
+
if (typeof indexedDB === 'undefined') return;
|
|
555
|
+
|
|
556
|
+
// Check if database exists and get its version
|
|
557
|
+
const databases = await indexedDB.databases?.() || [];
|
|
558
|
+
const existingDb = databases.find(db => db.name === dbName);
|
|
559
|
+
|
|
560
|
+
if (existingDb) {
|
|
561
|
+
// NDK cache adapter expects specific schema versions
|
|
562
|
+
// If the database exists but was created with an incompatible version,
|
|
563
|
+
// we need to delete it to prevent UpgradeError crashes
|
|
564
|
+
// The NDK cache adapter will recreate it with the correct schema
|
|
565
|
+
try {
|
|
566
|
+
await new Promise((resolve, reject) => {
|
|
567
|
+
const request = indexedDB.open(dbName);
|
|
568
|
+
request.onsuccess = () => {
|
|
569
|
+
const db = request.result;
|
|
570
|
+
const storeNames = Array.from(db.objectStoreNames);
|
|
571
|
+
db.close();
|
|
572
|
+
|
|
573
|
+
// Check for known incompatible schemas (missing required stores or wrong structure)
|
|
574
|
+
// NDK cache adapter expects: events, eventTags, users, nip05, profiles, etc.
|
|
575
|
+
const requiredStores = ['events', 'eventTags'];
|
|
576
|
+
const hasRequiredStores = requiredStores.every(name => storeNames.includes(name));
|
|
577
|
+
|
|
578
|
+
if (!hasRequiredStores && storeNames.length > 0) {
|
|
579
|
+
// Database exists but doesn't have the right structure
|
|
580
|
+
console.warn(`[nostr] NDK cache database ${dbName} has incompatible schema, clearing...`);
|
|
581
|
+
this._deleteDexieDatabase(dbName).then(resolve).catch(resolve);
|
|
582
|
+
} else {
|
|
583
|
+
resolve();
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
request.onerror = () => resolve(); // Ignore errors, continue without cache
|
|
587
|
+
request.onblocked = () => resolve();
|
|
588
|
+
});
|
|
589
|
+
} catch {
|
|
590
|
+
// Ignore errors during compatibility check
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Delete a Dexie database by name.
|
|
597
|
+
* Used to recover from schema upgrade errors.
|
|
598
|
+
* @param {string} dbName - The database name to delete
|
|
599
|
+
* @returns {Promise<void>}
|
|
600
|
+
* @private
|
|
601
|
+
*/
|
|
602
|
+
async _deleteDexieDatabase(dbName) {
|
|
603
|
+
if (typeof indexedDB === 'undefined') return;
|
|
604
|
+
|
|
605
|
+
return new Promise((resolve) => {
|
|
606
|
+
const request = indexedDB.deleteDatabase(dbName);
|
|
607
|
+
request.onsuccess = () => {
|
|
608
|
+
console.log(`[nostr] Deleted incompatible database: ${dbName}`);
|
|
609
|
+
resolve();
|
|
610
|
+
};
|
|
611
|
+
request.onerror = () => {
|
|
612
|
+
console.warn(`[nostr] Failed to delete database: ${dbName}`);
|
|
613
|
+
resolve(); // Resolve anyway to continue initialization
|
|
614
|
+
};
|
|
615
|
+
request.onblocked = () => {
|
|
616
|
+
console.warn(`[nostr] Database deletion blocked: ${dbName}`);
|
|
617
|
+
resolve(); // Resolve anyway to continue initialization
|
|
618
|
+
};
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
425
622
|
/**
|
|
426
623
|
* Load events from persistent storage into cache
|
|
427
624
|
* @private
|
|
@@ -538,6 +735,7 @@ export class NostrClient {
|
|
|
538
735
|
* @param {Object} [options={}] - Publish options
|
|
539
736
|
* @param {boolean} [options.waitForRelays=false] - Wait for relay confirmation
|
|
540
737
|
* @param {boolean} [options.debounce=true] - Debounce rapid writes to same d-tag
|
|
738
|
+
* @param {string} [options.signingKey] - Private key to sign with (hex format). If not provided, uses client's default key.
|
|
541
739
|
* @returns {Promise<Object>} Signed event with relay publish results
|
|
542
740
|
* @example
|
|
543
741
|
* const result = await client.publish({
|
|
@@ -600,23 +798,89 @@ export class NostrClient {
|
|
|
600
798
|
pendingWrites.set(dTagPath, { event, timer, resolve, reject });
|
|
601
799
|
|
|
602
800
|
// Cache immediately for local-first reads (even before relay publish)
|
|
603
|
-
const signedEvent =
|
|
801
|
+
const signedEvent = this._createSignedEvent(event, options.signingKey);
|
|
604
802
|
this._cacheEvent(signedEvent);
|
|
605
803
|
});
|
|
606
804
|
}
|
|
607
805
|
|
|
806
|
+
/**
|
|
807
|
+
* Create a signed event using NDK or nostr-tools fallback.
|
|
808
|
+
* @private
|
|
809
|
+
* @param {Object} event - Unsigned event
|
|
810
|
+
* @param {string} [signingKey] - Optional private key to sign with (hex format)
|
|
811
|
+
* @returns {Object} Signed event
|
|
812
|
+
*/
|
|
813
|
+
_createSignedEvent(event, signingKey = null) {
|
|
814
|
+
const keyToUse = signingKey || this.privateKey;
|
|
815
|
+
const pubkeyToUse = signingKey ? nostrToolsGetPublicKey(hexToBytes(signingKey)) : this.publicKey;
|
|
816
|
+
|
|
817
|
+
// If NDK is available and we have a signer, use NDKEvent
|
|
818
|
+
if (this.ndk) {
|
|
819
|
+
const ndkEvent = new NDKEvent(this.ndk);
|
|
820
|
+
ndkEvent.kind = event.kind;
|
|
821
|
+
ndkEvent.content = event.content;
|
|
822
|
+
ndkEvent.tags = event.tags || [];
|
|
823
|
+
ndkEvent.created_at = event.created_at || Math.floor(Date.now() / 1000);
|
|
824
|
+
|
|
825
|
+
// NDK will sign when publishing, for now return raw event format
|
|
826
|
+
return {
|
|
827
|
+
kind: ndkEvent.kind,
|
|
828
|
+
content: ndkEvent.content,
|
|
829
|
+
tags: ndkEvent.tags,
|
|
830
|
+
created_at: ndkEvent.created_at,
|
|
831
|
+
pubkey: pubkeyToUse,
|
|
832
|
+
id: '', // Will be set during actual publish
|
|
833
|
+
sig: '', // Will be set during actual publish
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Mock mode - use nostr-tools to create a properly signed event
|
|
838
|
+
try {
|
|
839
|
+
return nostrToolsFinalizeEvent(event, hexToBytes(keyToUse));
|
|
840
|
+
} catch (e) {
|
|
841
|
+
// Return mock signed event if signing fails
|
|
842
|
+
return {
|
|
843
|
+
...event,
|
|
844
|
+
pubkey: pubkeyToUse,
|
|
845
|
+
id: Math.random().toString(16).slice(2).padStart(64, '0'),
|
|
846
|
+
sig: 'mock-signature',
|
|
847
|
+
created_at: event.created_at || Math.floor(Date.now() / 1000),
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
608
852
|
/**
|
|
609
853
|
* Internal publish implementation
|
|
610
854
|
* @private
|
|
611
855
|
*/
|
|
612
856
|
async _doPublish(event, options = {}) {
|
|
613
857
|
const waitForRelays = options.waitForRelays || false;
|
|
858
|
+
const signingKey = options.signingKey || null;
|
|
614
859
|
|
|
615
860
|
// Check if event is already signed (has id and sig)
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
861
|
+
let signedEvent;
|
|
862
|
+
if (event.id && event.sig) {
|
|
863
|
+
signedEvent = event;
|
|
864
|
+
} else if (this.ndk) {
|
|
865
|
+
// Use NDK to create and sign the event
|
|
866
|
+
const ndkEvent = new NDKEvent(this.ndk);
|
|
867
|
+
ndkEvent.kind = event.kind;
|
|
868
|
+
ndkEvent.content = event.content;
|
|
869
|
+
ndkEvent.tags = event.tags || [];
|
|
870
|
+
ndkEvent.created_at = event.created_at || Math.floor(Date.now() / 1000);
|
|
871
|
+
|
|
872
|
+
// Sign the event - use different signer if signingKey provided
|
|
873
|
+
if (signingKey) {
|
|
874
|
+
const tempSigner = new NDKPrivateKeySigner(signingKey);
|
|
875
|
+
await ndkEvent.sign(tempSigner);
|
|
876
|
+
} else {
|
|
877
|
+
await ndkEvent.sign(this.signer);
|
|
878
|
+
}
|
|
879
|
+
signedEvent = ndkEvent.rawEvent();
|
|
880
|
+
} else {
|
|
881
|
+
// Mock mode - create mock signed event
|
|
882
|
+
signedEvent = this._createSignedEvent(event, signingKey);
|
|
883
|
+
}
|
|
620
884
|
|
|
621
885
|
// 1. Cache the event locally first (this makes reads instant)
|
|
622
886
|
await this._cacheEvent(signedEvent);
|
|
@@ -627,28 +891,37 @@ export class NostrClient {
|
|
|
627
891
|
}
|
|
628
892
|
|
|
629
893
|
// 3. Publish to relays
|
|
630
|
-
if (
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
894
|
+
if (this.ndk && this.relays.length > 0) {
|
|
895
|
+
if (waitForRelays) {
|
|
896
|
+
// Wait for relay confirmation if explicitly requested
|
|
897
|
+
const deliveryResult = await this._attemptDelivery(signedEvent, this.relays);
|
|
898
|
+
|
|
899
|
+
return {
|
|
900
|
+
event: signedEvent,
|
|
901
|
+
results: deliveryResult.results,
|
|
902
|
+
queued: !!this.outboxQueue,
|
|
903
|
+
};
|
|
904
|
+
} else {
|
|
905
|
+
// Fire and forget - publish in background, return immediately
|
|
906
|
+
this._attemptDelivery(signedEvent, this.relays).catch(() => {
|
|
907
|
+
// Silent - event is cached locally and queued for retry
|
|
908
|
+
});
|
|
644
909
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
910
|
+
// Return immediately (data is in local cache and queued for delivery)
|
|
911
|
+
return {
|
|
912
|
+
event: signedEvent,
|
|
913
|
+
results: [],
|
|
914
|
+
queued: !!this.outboxQueue,
|
|
915
|
+
};
|
|
916
|
+
}
|
|
651
917
|
}
|
|
918
|
+
|
|
919
|
+
// No relays configured - just return the signed event
|
|
920
|
+
return {
|
|
921
|
+
event: signedEvent,
|
|
922
|
+
results: [],
|
|
923
|
+
queued: !!this.outboxQueue,
|
|
924
|
+
};
|
|
652
925
|
}
|
|
653
926
|
|
|
654
927
|
/**
|
|
@@ -659,28 +932,50 @@ export class NostrClient {
|
|
|
659
932
|
* @returns {Promise<Object>} Delivery result with successful/failed relays
|
|
660
933
|
*/
|
|
661
934
|
async _attemptDelivery(event, relays) {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
935
|
+
if (!this.ndk) {
|
|
936
|
+
return { successful: [], failed: relays, results: [] };
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const ndkEvent = new NDKEvent(this.ndk, event);
|
|
665
940
|
|
|
666
941
|
const successful = [];
|
|
667
942
|
const failed = [];
|
|
668
943
|
const formattedResults = [];
|
|
669
944
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
945
|
+
try {
|
|
946
|
+
// Publish using NDK
|
|
947
|
+
const publishedRelays = await ndkEvent.publish();
|
|
948
|
+
|
|
949
|
+
// Track results
|
|
950
|
+
for (const relay of relays) {
|
|
951
|
+
const wasPublished = publishedRelays.has(this.ndk.pool.getRelay(relay));
|
|
952
|
+
if (wasPublished) {
|
|
953
|
+
successful.push(relay);
|
|
954
|
+
formattedResults.push({
|
|
955
|
+
relay,
|
|
956
|
+
status: 'fulfilled',
|
|
957
|
+
value: true,
|
|
958
|
+
});
|
|
959
|
+
} else {
|
|
960
|
+
failed.push(relay);
|
|
961
|
+
formattedResults.push({
|
|
962
|
+
relay,
|
|
963
|
+
status: 'rejected',
|
|
964
|
+
reason: 'Not published to relay',
|
|
965
|
+
});
|
|
966
|
+
}
|
|
682
967
|
}
|
|
683
|
-
})
|
|
968
|
+
} catch (error) {
|
|
969
|
+
// All failed
|
|
970
|
+
for (const relay of relays) {
|
|
971
|
+
failed.push(relay);
|
|
972
|
+
formattedResults.push({
|
|
973
|
+
relay,
|
|
974
|
+
status: 'rejected',
|
|
975
|
+
reason: error.message,
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
}
|
|
684
979
|
|
|
685
980
|
// Update outbox queue with delivery status
|
|
686
981
|
if (this.outboxQueue) {
|
|
@@ -689,16 +984,12 @@ export class NostrClient {
|
|
|
689
984
|
|
|
690
985
|
// Log failures only at debug level (common during network issues)
|
|
691
986
|
if (failed.length > 0 && successful.length === 0) {
|
|
692
|
-
|
|
693
|
-
const reasons = results
|
|
987
|
+
const reasons = formattedResults
|
|
694
988
|
.filter(r => r.status === 'rejected')
|
|
695
|
-
.map(f => f.reason
|
|
989
|
+
.map(f => f.reason || 'unknown')
|
|
696
990
|
.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}`);
|
|
991
|
+
if (!reasons.includes('timed out')) {
|
|
992
|
+
console.warn(`[nostr] All relays failed for ${event.id?.slice(0, 8)}: ${reasons}`);
|
|
702
993
|
}
|
|
703
994
|
}
|
|
704
995
|
|
|
@@ -732,7 +1023,7 @@ export class NostrClient {
|
|
|
732
1023
|
const forceRelay = options.forceRelay === true;
|
|
733
1024
|
|
|
734
1025
|
// If no relays, query from cache only
|
|
735
|
-
if (this.relays.length === 0) {
|
|
1026
|
+
if (this.relays.length === 0 || !this.ndk) {
|
|
736
1027
|
const matchingEvents = this._getMatchingCachedEvents(filter);
|
|
737
1028
|
return matchingEvents;
|
|
738
1029
|
}
|
|
@@ -812,7 +1103,17 @@ export class NostrClient {
|
|
|
812
1103
|
// Create the query promise
|
|
813
1104
|
const queryPromise = (async () => {
|
|
814
1105
|
try {
|
|
815
|
-
let events =
|
|
1106
|
+
let events = [];
|
|
1107
|
+
|
|
1108
|
+
if (this.ndk) {
|
|
1109
|
+
// Use NDK to fetch events
|
|
1110
|
+
const fetchedEvents = await this.ndk.fetchEvents(filter, {
|
|
1111
|
+
closeOnEose: true,
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
// Convert NDKEvent Set to raw event array
|
|
1115
|
+
events = Array.from(fetchedEvents).map(e => e.rawEvent());
|
|
1116
|
+
}
|
|
816
1117
|
|
|
817
1118
|
// CRITICAL: Filter out events from other authors (relay may not respect filter)
|
|
818
1119
|
if (filter.authors && filter.authors.length > 0) {
|
|
@@ -925,7 +1226,7 @@ export class NostrClient {
|
|
|
925
1226
|
* @private
|
|
926
1227
|
*/
|
|
927
1228
|
async _doBackgroundPathRefresh(path, kind, options) {
|
|
928
|
-
if (this.relays.length === 0) return;
|
|
1229
|
+
if (this.relays.length === 0 || !this.ndk) return;
|
|
929
1230
|
|
|
930
1231
|
// Throttle: Skip if we've refreshed this path recently
|
|
931
1232
|
const lastRefresh = backgroundRefreshThrottle.get(path);
|
|
@@ -954,7 +1255,7 @@ export class NostrClient {
|
|
|
954
1255
|
};
|
|
955
1256
|
const cacheKey = JSON.stringify(filter);
|
|
956
1257
|
|
|
957
|
-
// Use our query deduplication by calling query() instead of
|
|
1258
|
+
// Use our query deduplication by calling query() instead of direct fetch
|
|
958
1259
|
const timeout = options.timeout || 30000;
|
|
959
1260
|
const events = await this._queryRelaysAndCache(filter, cacheKey, timeout);
|
|
960
1261
|
|
|
@@ -988,7 +1289,7 @@ export class NostrClient {
|
|
|
988
1289
|
* @private
|
|
989
1290
|
*/
|
|
990
1291
|
async _doBackgroundPrefixRefresh(prefix, kind, options) {
|
|
991
|
-
if (this.relays.length === 0) return;
|
|
1292
|
+
if (this.relays.length === 0 || !this.ndk) return;
|
|
992
1293
|
|
|
993
1294
|
// Throttle: Skip if we've refreshed this prefix recently
|
|
994
1295
|
const throttleKey = `prefix:${prefix}`;
|
|
@@ -1047,14 +1348,17 @@ export class NostrClient {
|
|
|
1047
1348
|
const localEvents = this._getMatchingCachedEvents(filter);
|
|
1048
1349
|
|
|
1049
1350
|
// If no relays configured, return local only
|
|
1050
|
-
if (this.relays.length === 0) {
|
|
1351
|
+
if (this.relays.length === 0 || !this.ndk) {
|
|
1051
1352
|
return localEvents;
|
|
1052
1353
|
}
|
|
1053
1354
|
|
|
1054
1355
|
// Step 2: Query relays
|
|
1055
1356
|
let relayEvents = [];
|
|
1056
1357
|
try {
|
|
1057
|
-
|
|
1358
|
+
const fetchedEvents = await this.ndk.fetchEvents(filter, {
|
|
1359
|
+
closeOnEose: true,
|
|
1360
|
+
});
|
|
1361
|
+
relayEvents = Array.from(fetchedEvents).map(e => e.rawEvent());
|
|
1058
1362
|
|
|
1059
1363
|
// Filter out events from other authors
|
|
1060
1364
|
if (filter.authors && filter.authors.length > 0) {
|
|
@@ -1142,7 +1446,7 @@ export class NostrClient {
|
|
|
1142
1446
|
const filterKey = JSON.stringify(filter);
|
|
1143
1447
|
|
|
1144
1448
|
// If no relays, check cache for matching events and trigger callbacks
|
|
1145
|
-
if (this.relays.length === 0) {
|
|
1449
|
+
if (this.relays.length === 0 || !this.ndk) {
|
|
1146
1450
|
// Trigger with cached events that match the filter
|
|
1147
1451
|
const matchingEvents = this._getMatchingCachedEvents(filter);
|
|
1148
1452
|
|
|
@@ -1192,8 +1496,8 @@ export class NostrClient {
|
|
|
1192
1496
|
|
|
1193
1497
|
// Only close actual subscription when no more callbacks
|
|
1194
1498
|
if (existing.refCount === 0) {
|
|
1195
|
-
if (existing.subscription && existing.subscription.
|
|
1196
|
-
existing.subscription.
|
|
1499
|
+
if (existing.subscription && existing.subscription.stop) {
|
|
1500
|
+
existing.subscription.stop();
|
|
1197
1501
|
}
|
|
1198
1502
|
activeSubscriptions.delete(filterKey);
|
|
1199
1503
|
}
|
|
@@ -1213,39 +1517,41 @@ export class NostrClient {
|
|
|
1213
1517
|
// Store before creating subscription to handle race conditions
|
|
1214
1518
|
activeSubscriptions.set(filterKey, subscriptionInfo);
|
|
1215
1519
|
|
|
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
|
-
}
|
|
1520
|
+
const sub = this.ndk.subscribe(filter, {
|
|
1521
|
+
closeOnEose: false,
|
|
1522
|
+
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
|
|
1523
|
+
});
|
|
1229
1524
|
|
|
1230
|
-
|
|
1525
|
+
sub.on('event', (ndkEvent) => {
|
|
1526
|
+
const event = ndkEvent.rawEvent();
|
|
1231
1527
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1528
|
+
// CRITICAL: Filter out events from other authors immediately
|
|
1529
|
+
// Many relays don't respect the authors filter, so we must do it client-side
|
|
1530
|
+
if (filter.authors && filter.authors.length > 0) {
|
|
1531
|
+
if (!filter.authors.includes(event.pubkey)) {
|
|
1532
|
+
// Silently reject events from other authors
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
this._cacheEvent(event);
|
|
1538
|
+
|
|
1539
|
+
// Dispatch to ALL registered callbacks for this subscription
|
|
1540
|
+
const subInfo = activeSubscriptions.get(filterKey);
|
|
1541
|
+
if (subInfo) {
|
|
1542
|
+
for (const cb of subInfo.callbacks) {
|
|
1543
|
+
try {
|
|
1544
|
+
cb(event);
|
|
1545
|
+
} catch (err) {
|
|
1546
|
+
console.warn('[nostr] Subscription callback error:', err.message);
|
|
1242
1547
|
}
|
|
1243
|
-
}
|
|
1244
|
-
oneose: () => {
|
|
1245
|
-
if (options.onEOSE) options.onEOSE();
|
|
1246
|
-
},
|
|
1548
|
+
}
|
|
1247
1549
|
}
|
|
1248
|
-
);
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
sub.on('eose', () => {
|
|
1553
|
+
if (options.onEOSE) options.onEOSE();
|
|
1554
|
+
});
|
|
1249
1555
|
|
|
1250
1556
|
// Store the actual subscription object
|
|
1251
1557
|
subscriptionInfo.subscription = sub;
|
|
@@ -1259,7 +1565,7 @@ export class NostrClient {
|
|
|
1259
1565
|
|
|
1260
1566
|
// Only close actual subscription when no more callbacks
|
|
1261
1567
|
if (subscriptionInfo.refCount === 0) {
|
|
1262
|
-
if (sub.
|
|
1568
|
+
if (sub.stop) sub.stop();
|
|
1263
1569
|
activeSubscriptions.delete(filterKey);
|
|
1264
1570
|
}
|
|
1265
1571
|
this._subscriptions.delete(subId);
|
|
@@ -1379,7 +1685,7 @@ export class NostrClient {
|
|
|
1379
1685
|
}
|
|
1380
1686
|
|
|
1381
1687
|
// Trigger any active subscriptions that match this event
|
|
1382
|
-
if (this.relays.length === 0) {
|
|
1688
|
+
if (this.relays.length === 0 || !this.ndk) {
|
|
1383
1689
|
for (const sub of this._subscriptions.values()) {
|
|
1384
1690
|
if (sub.active && this._eventMatchesFilter(event, sub.filter)) {
|
|
1385
1691
|
// Trigger callback asynchronously, but check active status again
|
|
@@ -1559,16 +1865,16 @@ export class NostrClient {
|
|
|
1559
1865
|
// Close long-lived author subscription
|
|
1560
1866
|
const authorSub = authorSubscriptions.get(this.publicKey);
|
|
1561
1867
|
if (authorSub && authorSub.subscription) {
|
|
1562
|
-
if (authorSub.subscription.
|
|
1563
|
-
authorSub.subscription.
|
|
1868
|
+
if (authorSub.subscription.stop) {
|
|
1869
|
+
authorSub.subscription.stop();
|
|
1564
1870
|
}
|
|
1565
1871
|
authorSubscriptions.delete(this.publicKey);
|
|
1566
1872
|
}
|
|
1567
1873
|
|
|
1568
1874
|
// Close all subscriptions
|
|
1569
1875
|
for (const sub of this._subscriptions.values()) {
|
|
1570
|
-
if (sub.
|
|
1571
|
-
sub.
|
|
1876
|
+
if (sub.stop) {
|
|
1877
|
+
sub.stop();
|
|
1572
1878
|
} else if (sub.active !== undefined) {
|
|
1573
1879
|
// Mock subscription
|
|
1574
1880
|
sub.active = false;
|
|
@@ -1576,8 +1882,8 @@ export class NostrClient {
|
|
|
1576
1882
|
}
|
|
1577
1883
|
this._subscriptions.clear();
|
|
1578
1884
|
|
|
1579
|
-
//
|
|
1580
|
-
|
|
1885
|
+
// Note: We don't close the global NDK as it may be used by other clients
|
|
1886
|
+
// The pool connections will be cleaned up when the process exits
|
|
1581
1887
|
|
|
1582
1888
|
// Clear cache and index
|
|
1583
1889
|
this._eventCache.clear();
|
|
@@ -1589,10 +1895,20 @@ export class NostrClient {
|
|
|
1589
1895
|
* @returns {Array} Relay connection statuses
|
|
1590
1896
|
*/
|
|
1591
1897
|
getRelayStatus() {
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1898
|
+
if (!this.ndk) {
|
|
1899
|
+
return this.relays.map(relay => ({
|
|
1900
|
+
url: relay,
|
|
1901
|
+
connected: false,
|
|
1902
|
+
}));
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
return this.relays.map(relay => {
|
|
1906
|
+
const ndkRelay = this.ndk.pool.getRelay(relay);
|
|
1907
|
+
return {
|
|
1908
|
+
url: relay,
|
|
1909
|
+
connected: ndkRelay?.status === 1, // WebSocket.OPEN
|
|
1910
|
+
};
|
|
1911
|
+
});
|
|
1596
1912
|
}
|
|
1597
1913
|
}
|
|
1598
1914
|
|