holosphere 2.0.0-alpha2 → 2.0.0-alpha5
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 +6680 -0
- package/dist/2019-D2OG2idw.js.map +1 -0
- package/dist/2019-EION3wKo.cjs +8 -0
- package/dist/2019-EION3wKo.cjs.map +1 -0
- package/dist/_commonjsHelpers-C37NGDzP.cjs +2 -0
- package/dist/_commonjsHelpers-C37NGDzP.cjs.map +1 -0
- package/dist/_commonjsHelpers-CUmg6egw.js +7 -0
- package/dist/_commonjsHelpers-CUmg6egw.js.map +1 -0
- package/dist/browser-BSniCNqO.js +3058 -0
- package/dist/browser-BSniCNqO.js.map +1 -0
- package/dist/browser-Cq59Ij19.cjs +2 -0
- package/dist/browser-Cq59Ij19.cjs.map +1 -0
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +50 -53
- package/dist/index-BG8FStkt.cjs +12 -0
- package/dist/index-BG8FStkt.cjs.map +1 -0
- package/dist/index-Bbey4GkP.js +37869 -0
- package/dist/index-Bbey4GkP.js.map +1 -0
- package/dist/index-Cp3xctq8.js +15104 -0
- package/dist/index-Cp3xctq8.js.map +1 -0
- package/dist/index-hfVGRwSr.cjs +5 -0
- package/dist/index-hfVGRwSr.cjs.map +1 -0
- package/dist/indexeddb-storage-BD70pN7q.cjs +2 -0
- package/dist/indexeddb-storage-BD70pN7q.cjs.map +1 -0
- package/dist/{indexeddb-storage-CMW4qRQS.js → indexeddb-storage-Bjg84U5R.js} +49 -13
- package/dist/indexeddb-storage-Bjg84U5R.js.map +1 -0
- package/dist/{memory-storage-DQzcAZlf.js → memory-storage-CD0XFayE.js} +6 -2
- package/dist/memory-storage-CD0XFayE.js.map +1 -0
- package/dist/{memory-storage-DmePEP2q.cjs → memory-storage-DmMyJtOo.cjs} +2 -2
- package/dist/memory-storage-DmMyJtOo.cjs.map +1 -0
- package/dist/{secp256k1-vOXp40Fx.js → secp256k1-69sS9O-P.js} +2 -393
- package/dist/secp256k1-69sS9O-P.js.map +1 -0
- package/dist/secp256k1-TcN6vWGh.cjs +12 -0
- package/dist/secp256k1-TcN6vWGh.cjs.map +1 -0
- package/docs/CONTRACTS.md +797 -0
- package/examples/demo.html +47 -0
- package/package.json +10 -5
- package/src/contracts/abis/Appreciative.json +1280 -0
- package/src/contracts/abis/AppreciativeFactory.json +101 -0
- package/src/contracts/abis/Bundle.json +1435 -0
- package/src/contracts/abis/BundleFactory.json +106 -0
- package/src/contracts/abis/Holon.json +881 -0
- package/src/contracts/abis/Holons.json +330 -0
- package/src/contracts/abis/Managed.json +1262 -0
- package/src/contracts/abis/ManagedFactory.json +149 -0
- package/src/contracts/abis/Membrane.json +261 -0
- package/src/contracts/abis/Splitter.json +1624 -0
- package/src/contracts/abis/SplitterFactory.json +220 -0
- package/src/contracts/abis/TestToken.json +321 -0
- package/src/contracts/abis/Zoned.json +1461 -0
- package/src/contracts/abis/ZonedFactory.json +154 -0
- package/src/contracts/chain-manager.js +375 -0
- package/src/contracts/deployer.js +443 -0
- package/src/contracts/event-listener.js +507 -0
- package/src/contracts/holon-contracts.js +344 -0
- package/src/contracts/index.js +83 -0
- package/src/contracts/networks.js +224 -0
- package/src/contracts/operations.js +670 -0
- package/src/contracts/queries.js +589 -0
- package/src/core/holosphere.js +453 -1
- package/src/crypto/nostr-utils.js +263 -0
- package/src/federation/handshake.js +455 -0
- package/src/federation/hologram.js +1 -1
- package/src/hierarchical/upcast.js +6 -5
- package/src/index.js +463 -1939
- package/src/lib/ai-methods.js +308 -0
- package/src/lib/contract-methods.js +293 -0
- package/src/lib/errors.js +23 -0
- package/src/lib/federation-methods.js +238 -0
- package/src/lib/index.js +26 -0
- package/src/spatial/h3-operations.js +2 -2
- package/src/storage/backends/gundb-backend.js +377 -46
- package/src/storage/global-tables.js +28 -1
- package/src/storage/gun-auth.js +303 -0
- package/src/storage/gun-federation.js +776 -0
- package/src/storage/gun-references.js +198 -0
- package/src/storage/gun-schema.js +291 -0
- package/src/storage/gun-wrapper.js +347 -31
- package/src/storage/indexeddb-storage.js +49 -11
- package/src/storage/memory-storage.js +5 -0
- package/src/storage/nostr-async.js +194 -37
- package/src/storage/nostr-client.js +580 -51
- package/src/storage/persistent-storage.js +6 -1
- package/src/storage/unified-storage.js +119 -0
- package/src/subscriptions/manager.js +1 -1
- package/types/index.d.ts +133 -0
- package/dist/index-CDfIuXew.js +0 -15974
- package/dist/index-CDfIuXew.js.map +0 -1
- package/dist/index-ifOgtDvd.cjs +0 -3
- package/dist/index-ifOgtDvd.cjs.map +0 -1
- package/dist/indexeddb-storage-CMW4qRQS.js.map +0 -1
- package/dist/indexeddb-storage-DLZOgetM.cjs +0 -2
- package/dist/indexeddb-storage-DLZOgetM.cjs.map +0 -1
- package/dist/memory-storage-DQzcAZlf.js.map +0 -1
- package/dist/memory-storage-DmePEP2q.cjs.map +0 -1
- package/dist/secp256k1-CP0ZkpAx.cjs +0 -13
- package/dist/secp256k1-CP0ZkpAx.cjs.map +0 -1
- package/dist/secp256k1-vOXp40Fx.js.map +0 -1
|
@@ -7,6 +7,116 @@ import { SimplePool, finalizeEvent, getPublicKey } from 'nostr-tools';
|
|
|
7
7
|
import { OutboxQueue } from './outbox-queue.js';
|
|
8
8
|
import { SyncService } from './sync-service.js';
|
|
9
9
|
|
|
10
|
+
// Global pool singleton - reuse connections across NostrClient instances
|
|
11
|
+
let globalPool = null;
|
|
12
|
+
let globalPoolRelays = new Set();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get or create global SimplePool singleton
|
|
16
|
+
* This ensures WebSocket connections are reused across all operations
|
|
17
|
+
*/
|
|
18
|
+
function getGlobalPool(config = {}) {
|
|
19
|
+
if (!globalPool) {
|
|
20
|
+
globalPool = new SimplePool({
|
|
21
|
+
enableReconnect: config.enableReconnect !== false,
|
|
22
|
+
enablePing: config.enablePing !== false,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return globalPool;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Global pending queries map for deduplication
|
|
29
|
+
// Key: JSON-stringified filter, Value: { promise, timestamp }
|
|
30
|
+
const pendingQueries = new Map();
|
|
31
|
+
const PENDING_QUERY_TIMEOUT = 5000; // 5 seconds
|
|
32
|
+
|
|
33
|
+
// Global active subscriptions map for subscription deduplication
|
|
34
|
+
// Key: JSON-stringified filter, Value: { subscription, callbacks: Set, refCount }
|
|
35
|
+
const activeSubscriptions = new Map();
|
|
36
|
+
|
|
37
|
+
// Throttle background refreshes to avoid flooding the relay
|
|
38
|
+
// Key: path, Value: timestamp of last refresh
|
|
39
|
+
const backgroundRefreshThrottle = new Map();
|
|
40
|
+
const BACKGROUND_REFRESH_INTERVAL = 30000; // Only refresh same path every 30 seconds
|
|
41
|
+
|
|
42
|
+
// Write debouncing for rapid updates to the same path
|
|
43
|
+
// Key: d-tag path, Value: { event, timer, resolve, reject }
|
|
44
|
+
const pendingWrites = new Map();
|
|
45
|
+
const WRITE_DEBOUNCE_MS = 500; // Debounce writes within 500ms window
|
|
46
|
+
|
|
47
|
+
// Long-lived subscription manager - keeps ONE subscription per author for real-time updates
|
|
48
|
+
// Key: pubkey, Value: { subscription, lastEventTime, initialized }
|
|
49
|
+
const authorSubscriptions = new Map();
|
|
50
|
+
const AUTHOR_SUB_INIT_TIMEOUT = 5000; // Wait up to 5s for initial data load
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Simple LRU Cache implementation
|
|
54
|
+
* Automatically evicts least recently used entries when max size is reached
|
|
55
|
+
*/
|
|
56
|
+
class LRUCache {
|
|
57
|
+
constructor(maxSize = 500) {
|
|
58
|
+
this.maxSize = maxSize;
|
|
59
|
+
this.cache = new Map();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get(key) {
|
|
63
|
+
if (!this.cache.has(key)) return undefined;
|
|
64
|
+
|
|
65
|
+
// Move to end (most recently used)
|
|
66
|
+
const value = this.cache.get(key);
|
|
67
|
+
this.cache.delete(key);
|
|
68
|
+
this.cache.set(key, value);
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
set(key, value) {
|
|
73
|
+
// If key exists, delete it first to update position
|
|
74
|
+
if (this.cache.has(key)) {
|
|
75
|
+
this.cache.delete(key);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.cache.set(key, value);
|
|
79
|
+
|
|
80
|
+
// Evict oldest entries if over capacity
|
|
81
|
+
while (this.cache.size > this.maxSize) {
|
|
82
|
+
const oldestKey = this.cache.keys().next().value;
|
|
83
|
+
this.cache.delete(oldestKey);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
has(key) {
|
|
88
|
+
return this.cache.has(key);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
delete(key) {
|
|
92
|
+
return this.cache.delete(key);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
clear() {
|
|
96
|
+
this.cache.clear();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
get size() {
|
|
100
|
+
return this.cache.size;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
keys() {
|
|
104
|
+
return this.cache.keys();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
values() {
|
|
108
|
+
return this.cache.values();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
entries() {
|
|
112
|
+
return this.cache.entries();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
forEach(callback) {
|
|
116
|
+
this.cache.forEach(callback);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
10
120
|
// Lazy-load WebSocket polyfill for Node.js environment
|
|
11
121
|
let webSocketPolyfillPromise = null;
|
|
12
122
|
function ensureWebSocket() {
|
|
@@ -57,9 +167,15 @@ export class NostrClient {
|
|
|
57
167
|
this.config = config;
|
|
58
168
|
|
|
59
169
|
this._subscriptions = new Map();
|
|
60
|
-
this._eventCache = new
|
|
170
|
+
this._eventCache = new LRUCache(config.cacheSize || 500); // LRU cache for recent events
|
|
171
|
+
this._cacheIndex = new Map(); // Reverse index: kind -> Set of cache keys affected by that kind
|
|
61
172
|
this.persistentStorage = null;
|
|
62
173
|
|
|
174
|
+
// Batched persistent writes for better I/O performance
|
|
175
|
+
this._persistQueue = new Map(); // path -> event
|
|
176
|
+
this._persistTimer = null;
|
|
177
|
+
this._persistBatchMs = config.persistBatchMs || 100; // Batch writes within 100ms window
|
|
178
|
+
|
|
63
179
|
// Initialize pool and storage asynchronously
|
|
64
180
|
this._initReady = this._initialize();
|
|
65
181
|
}
|
|
@@ -74,10 +190,11 @@ export class NostrClient {
|
|
|
74
190
|
|
|
75
191
|
// Initialize SimplePool with options (only if relays exist)
|
|
76
192
|
if (this.relays.length > 0) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
193
|
+
// Use global pool singleton to reuse WebSocket connections
|
|
194
|
+
this.pool = getGlobalPool(this.config);
|
|
195
|
+
|
|
196
|
+
// Track relays used by this client
|
|
197
|
+
this.relays.forEach(r => globalPoolRelays.add(r));
|
|
81
198
|
} else {
|
|
82
199
|
// Mock pool for testing - returns mock promise that resolves immediately
|
|
83
200
|
this.pool = {
|
|
@@ -90,6 +207,90 @@ export class NostrClient {
|
|
|
90
207
|
|
|
91
208
|
// Initialize persistent storage
|
|
92
209
|
await this._initPersistentStorage();
|
|
210
|
+
|
|
211
|
+
// Start long-lived subscription for real-time cache updates
|
|
212
|
+
if (this.relays.length > 0) {
|
|
213
|
+
this._initLongLivedSubscription();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Initialize a long-lived subscription to keep cache fresh
|
|
219
|
+
* This replaces polling with a single persistent subscription
|
|
220
|
+
* @private
|
|
221
|
+
*/
|
|
222
|
+
_initLongLivedSubscription() {
|
|
223
|
+
const subKey = this.publicKey;
|
|
224
|
+
|
|
225
|
+
// Check if subscription already exists for this author
|
|
226
|
+
if (authorSubscriptions.has(subKey)) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const subInfo = {
|
|
231
|
+
subscription: null,
|
|
232
|
+
initialized: false,
|
|
233
|
+
initPromise: null,
|
|
234
|
+
initResolve: null,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Create promise for initial load completion
|
|
238
|
+
subInfo.initPromise = new Promise(resolve => {
|
|
239
|
+
subInfo.initResolve = resolve;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
authorSubscriptions.set(subKey, subInfo);
|
|
243
|
+
|
|
244
|
+
// Subscribe to ALL events for this author (kind 30000)
|
|
245
|
+
// This single subscription replaces all the polling queries
|
|
246
|
+
const filter = {
|
|
247
|
+
kinds: [30000],
|
|
248
|
+
authors: [this.publicKey],
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const sub = this.pool.subscribeMany(
|
|
252
|
+
this.relays,
|
|
253
|
+
[filter],
|
|
254
|
+
{
|
|
255
|
+
onevent: (event) => {
|
|
256
|
+
// Verify author (relay may not respect filter)
|
|
257
|
+
if (event.pubkey !== this.publicKey) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Cache the event - this keeps our local cache in sync
|
|
262
|
+
this._cacheEvent(event);
|
|
263
|
+
},
|
|
264
|
+
oneose: () => {
|
|
265
|
+
// End of stored events - initial load complete
|
|
266
|
+
if (!subInfo.initialized) {
|
|
267
|
+
subInfo.initialized = true;
|
|
268
|
+
subInfo.initResolve();
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
subInfo.subscription = sub;
|
|
275
|
+
|
|
276
|
+
// Set timeout for initial load in case EOSE never arrives
|
|
277
|
+
setTimeout(() => {
|
|
278
|
+
if (!subInfo.initialized) {
|
|
279
|
+
subInfo.initialized = true;
|
|
280
|
+
subInfo.initResolve();
|
|
281
|
+
}
|
|
282
|
+
}, AUTHOR_SUB_INIT_TIMEOUT);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Wait for long-lived subscription to complete initial load
|
|
287
|
+
* @private
|
|
288
|
+
*/
|
|
289
|
+
async _waitForSubscriptionInit() {
|
|
290
|
+
const subInfo = authorSubscriptions.get(this.publicKey);
|
|
291
|
+
if (subInfo && subInfo.initPromise) {
|
|
292
|
+
await subInfo.initPromise;
|
|
293
|
+
}
|
|
93
294
|
}
|
|
94
295
|
|
|
95
296
|
/**
|
|
@@ -222,9 +423,11 @@ export class NostrClient {
|
|
|
222
423
|
|
|
223
424
|
/**
|
|
224
425
|
* Publish event to relays
|
|
426
|
+
* Supports debouncing for replaceable events (kind 30000-39999) to avoid rapid updates
|
|
225
427
|
* @param {Object} event - Unsigned event object
|
|
226
428
|
* @param {Object} options - Publish options
|
|
227
429
|
* @param {boolean} options.waitForRelays - Wait for relay confirmation (default: false for speed)
|
|
430
|
+
* @param {boolean} options.debounce - Debounce rapid writes to same d-tag (default: true for replaceable events)
|
|
228
431
|
* @returns {Promise<Object>} Signed event with relay publish results
|
|
229
432
|
*/
|
|
230
433
|
async publish(event, options = {}) {
|
|
@@ -233,6 +436,66 @@ export class NostrClient {
|
|
|
233
436
|
|
|
234
437
|
const waitForRelays = options.waitForRelays || false;
|
|
235
438
|
|
|
439
|
+
// For replaceable events, check if we should debounce
|
|
440
|
+
const isReplaceable = event.kind >= 30000 && event.kind < 40000;
|
|
441
|
+
const shouldDebounce = isReplaceable && options.debounce !== false && !waitForRelays;
|
|
442
|
+
|
|
443
|
+
if (shouldDebounce) {
|
|
444
|
+
const dTag = event.tags?.find(t => t[0] === 'd');
|
|
445
|
+
if (dTag && dTag[1]) {
|
|
446
|
+
return this._debouncedPublish(event, dTag[1], options);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return this._doPublish(event, options);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Debounced publish - coalesces rapid writes to the same d-tag
|
|
455
|
+
* @private
|
|
456
|
+
*/
|
|
457
|
+
_debouncedPublish(event, dTagPath, options) {
|
|
458
|
+
return new Promise((resolve, reject) => {
|
|
459
|
+
const existing = pendingWrites.get(dTagPath);
|
|
460
|
+
|
|
461
|
+
if (existing) {
|
|
462
|
+
// Cancel previous pending write and use the new one
|
|
463
|
+
clearTimeout(existing.timer);
|
|
464
|
+
// Resolve the previous promise with the new event (it will be superseded)
|
|
465
|
+
existing.resolve({
|
|
466
|
+
event: null,
|
|
467
|
+
results: [],
|
|
468
|
+
debounced: true,
|
|
469
|
+
supersededBy: event,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Set up debounced write
|
|
474
|
+
const timer = setTimeout(async () => {
|
|
475
|
+
pendingWrites.delete(dTagPath);
|
|
476
|
+
try {
|
|
477
|
+
const result = await this._doPublish(event, options);
|
|
478
|
+
resolve(result);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
reject(err);
|
|
481
|
+
}
|
|
482
|
+
}, WRITE_DEBOUNCE_MS);
|
|
483
|
+
|
|
484
|
+
pendingWrites.set(dTagPath, { event, timer, resolve, reject });
|
|
485
|
+
|
|
486
|
+
// Cache immediately for local-first reads (even before relay publish)
|
|
487
|
+
const signedEvent = finalizeEvent(event, this.privateKey);
|
|
488
|
+
this._cacheEvent(signedEvent);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Internal publish implementation
|
|
494
|
+
* @private
|
|
495
|
+
*/
|
|
496
|
+
async _doPublish(event, options = {}) {
|
|
497
|
+
const waitForRelays = options.waitForRelays || false;
|
|
498
|
+
|
|
236
499
|
// Sign the event
|
|
237
500
|
const signedEvent = finalizeEvent(event, this.privateKey);
|
|
238
501
|
|
|
@@ -256,8 +519,8 @@ export class NostrClient {
|
|
|
256
519
|
};
|
|
257
520
|
} else {
|
|
258
521
|
// Fire and forget - publish in background, return immediately
|
|
259
|
-
this._attemptDelivery(signedEvent, this.relays).catch(
|
|
260
|
-
|
|
522
|
+
this._attemptDelivery(signedEvent, this.relays).catch(() => {
|
|
523
|
+
// Silent - event is cached locally and queued for retry
|
|
261
524
|
});
|
|
262
525
|
|
|
263
526
|
// Return immediately (data is in local cache and queued for delivery)
|
|
@@ -305,13 +568,19 @@ export class NostrClient {
|
|
|
305
568
|
await this.outboxQueue.markSent(event.id, successful);
|
|
306
569
|
}
|
|
307
570
|
|
|
308
|
-
// Log failures
|
|
309
|
-
if (failed.length > 0) {
|
|
571
|
+
// Log failures only at debug level (common during network issues)
|
|
572
|
+
if (failed.length > 0 && successful.length === 0) {
|
|
573
|
+
// Only warn if ALL relays failed (likely a real issue)
|
|
310
574
|
const reasons = results
|
|
311
575
|
.filter(r => r.status === 'rejected')
|
|
312
576
|
.map(f => f.reason?.message || f.reason || 'unknown')
|
|
313
577
|
.join(', ');
|
|
314
|
-
|
|
578
|
+
// Use debug level for timeout issues (very common)
|
|
579
|
+
if (reasons.includes('timed out')) {
|
|
580
|
+
// Silent - event is queued for retry anyway
|
|
581
|
+
} else {
|
|
582
|
+
console.warn(`[nostr] All relays failed for ${event.id.slice(0, 8)}: ${reasons}`);
|
|
583
|
+
}
|
|
315
584
|
}
|
|
316
585
|
|
|
317
586
|
return { successful, failed, results: formattedResults };
|
|
@@ -319,10 +588,12 @@ export class NostrClient {
|
|
|
319
588
|
|
|
320
589
|
/**
|
|
321
590
|
* Query events from relays
|
|
591
|
+
* Uses long-lived subscription for cache updates - avoids polling
|
|
322
592
|
* @param {Object} filter - Nostr filter object
|
|
323
593
|
* @param {Object} options - Query options
|
|
324
594
|
* @param {number} options.timeout - Query timeout in ms (default: 30000, set to 0 for no timeout)
|
|
325
595
|
* @param {boolean} options.localFirst - Return local cache immediately, refresh in background (default: true)
|
|
596
|
+
* @param {boolean} options.forceRelay - Force relay query even if subscription cache is available (default: false)
|
|
326
597
|
* @returns {Promise<Array>} Array of events
|
|
327
598
|
*/
|
|
328
599
|
async query(filter, options = {}) {
|
|
@@ -331,6 +602,7 @@ export class NostrClient {
|
|
|
331
602
|
|
|
332
603
|
const timeout = options.timeout !== undefined ? options.timeout : 30000;
|
|
333
604
|
const localFirst = options.localFirst !== false; // Default to true for speed
|
|
605
|
+
const forceRelay = options.forceRelay === true;
|
|
334
606
|
|
|
335
607
|
// If no relays, query from cache only
|
|
336
608
|
if (this.relays.length === 0) {
|
|
@@ -338,6 +610,35 @@ export class NostrClient {
|
|
|
338
610
|
return matchingEvents;
|
|
339
611
|
}
|
|
340
612
|
|
|
613
|
+
// Check if this query can be served from the long-lived subscription cache
|
|
614
|
+
// The subscription keeps ALL events for this author in cache, updated in real-time
|
|
615
|
+
const subInfo = authorSubscriptions.get(this.publicKey);
|
|
616
|
+
const isOwnDataQuery = filter.authors &&
|
|
617
|
+
filter.authors.length === 1 &&
|
|
618
|
+
filter.authors[0] === this.publicKey;
|
|
619
|
+
|
|
620
|
+
// If we have an initialized subscription for our own data, use cache
|
|
621
|
+
if (!forceRelay && isOwnDataQuery && subInfo && subInfo.initialized) {
|
|
622
|
+
// Return matching events from cache - no relay query needed!
|
|
623
|
+
const matchingEvents = this._getMatchingCachedEvents(filter);
|
|
624
|
+
return matchingEvents;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// For first query before subscription initializes, wait briefly
|
|
628
|
+
if (isOwnDataQuery && subInfo && !subInfo.initialized) {
|
|
629
|
+
// Wait for subscription to initialize (up to timeout)
|
|
630
|
+
await Promise.race([
|
|
631
|
+
subInfo.initPromise,
|
|
632
|
+
new Promise(resolve => setTimeout(resolve, Math.min(timeout, AUTHOR_SUB_INIT_TIMEOUT)))
|
|
633
|
+
]);
|
|
634
|
+
|
|
635
|
+
// Now try cache again
|
|
636
|
+
if (subInfo.initialized) {
|
|
637
|
+
const matchingEvents = this._getMatchingCachedEvents(filter);
|
|
638
|
+
return matchingEvents;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
341
642
|
// Check d-tag cache first for single-item queries (most common case)
|
|
342
643
|
// This ensures recently written data is returned immediately
|
|
343
644
|
if (filter['#d'] && filter['#d'].length === 1 && filter.kinds && filter.kinds.length === 1) {
|
|
@@ -370,29 +671,101 @@ export class NostrClient {
|
|
|
370
671
|
|
|
371
672
|
/**
|
|
372
673
|
* Query relays and update cache
|
|
674
|
+
* Uses global pending queries map to deduplicate identical concurrent queries
|
|
373
675
|
* @private
|
|
374
676
|
*/
|
|
375
677
|
async _queryRelaysAndCache(filter, cacheKey, timeout) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
678
|
+
// Check if there's already a pending query for this exact filter
|
|
679
|
+
const pending = pendingQueries.get(cacheKey);
|
|
680
|
+
if (pending && Date.now() - pending.timestamp < PENDING_QUERY_TIMEOUT) {
|
|
681
|
+
// Reuse the pending query promise instead of creating a new one
|
|
682
|
+
return pending.promise;
|
|
381
683
|
}
|
|
382
684
|
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
685
|
+
// Create the query promise
|
|
686
|
+
const queryPromise = (async () => {
|
|
687
|
+
try {
|
|
688
|
+
let events = await this.pool.querySync(this.relays, filter, { timeout });
|
|
689
|
+
|
|
690
|
+
// CRITICAL: Filter out events from other authors (relay may not respect filter)
|
|
691
|
+
if (filter.authors && filter.authors.length > 0) {
|
|
692
|
+
events = events.filter(event => filter.authors.includes(event.pubkey));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Cache results
|
|
696
|
+
this._eventCache.set(cacheKey, {
|
|
697
|
+
events,
|
|
698
|
+
timestamp: Date.now(),
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// Update reverse index for fast invalidation
|
|
702
|
+
this._indexCacheEntry(cacheKey, filter);
|
|
703
|
+
|
|
704
|
+
return events;
|
|
705
|
+
} finally {
|
|
706
|
+
// Clean up pending query after completion
|
|
707
|
+
pendingQueries.delete(cacheKey);
|
|
708
|
+
}
|
|
709
|
+
})();
|
|
710
|
+
|
|
711
|
+
// Store the pending query
|
|
712
|
+
pendingQueries.set(cacheKey, {
|
|
713
|
+
promise: queryPromise,
|
|
386
714
|
timestamp: Date.now(),
|
|
387
715
|
});
|
|
388
716
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
717
|
+
return queryPromise;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Limit cache size (called after cache operations)
|
|
722
|
+
* Note: LRU cache handles this automatically, kept for API compatibility
|
|
723
|
+
* @private
|
|
724
|
+
*/
|
|
725
|
+
_limitCacheSize() {
|
|
726
|
+
// LRU cache handles size limiting automatically
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Add cache entry to reverse index for fast invalidation
|
|
731
|
+
* @private
|
|
732
|
+
*/
|
|
733
|
+
_indexCacheEntry(cacheKey, filter) {
|
|
734
|
+
// Index by kinds for fast lookup during invalidation
|
|
735
|
+
if (filter.kinds) {
|
|
736
|
+
for (const kind of filter.kinds) {
|
|
737
|
+
if (!this._cacheIndex.has(kind)) {
|
|
738
|
+
this._cacheIndex.set(kind, new Set());
|
|
739
|
+
}
|
|
740
|
+
this._cacheIndex.get(kind).add(cacheKey);
|
|
741
|
+
}
|
|
393
742
|
}
|
|
743
|
+
}
|
|
394
744
|
|
|
395
|
-
|
|
745
|
+
/**
|
|
746
|
+
* Remove cache entry from reverse index
|
|
747
|
+
* @private
|
|
748
|
+
*/
|
|
749
|
+
_unindexCacheEntry(cacheKey) {
|
|
750
|
+
// Try to parse the filter from the cache key to remove from index
|
|
751
|
+
if (!cacheKey.startsWith('{')) return;
|
|
752
|
+
|
|
753
|
+
try {
|
|
754
|
+
const filter = JSON.parse(cacheKey);
|
|
755
|
+
if (filter.kinds) {
|
|
756
|
+
for (const kind of filter.kinds) {
|
|
757
|
+
const indexSet = this._cacheIndex.get(kind);
|
|
758
|
+
if (indexSet) {
|
|
759
|
+
indexSet.delete(cacheKey);
|
|
760
|
+
if (indexSet.size === 0) {
|
|
761
|
+
this._cacheIndex.delete(kind);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
} catch {
|
|
767
|
+
// Not a valid filter key, skip
|
|
768
|
+
}
|
|
396
769
|
}
|
|
397
770
|
|
|
398
771
|
/**
|
|
@@ -421,20 +794,42 @@ export class NostrClient {
|
|
|
421
794
|
|
|
422
795
|
/**
|
|
423
796
|
* Internal method to refresh a path from relays
|
|
797
|
+
* Throttled to avoid flooding the relay with repeated requests
|
|
424
798
|
* @private
|
|
425
799
|
*/
|
|
426
800
|
async _doBackgroundPathRefresh(path, kind, options) {
|
|
427
801
|
if (this.relays.length === 0) return;
|
|
428
802
|
|
|
803
|
+
// Throttle: Skip if we've refreshed this path recently
|
|
804
|
+
const lastRefresh = backgroundRefreshThrottle.get(path);
|
|
805
|
+
if (lastRefresh && Date.now() - lastRefresh < BACKGROUND_REFRESH_INTERVAL) {
|
|
806
|
+
return; // Skip - recently refreshed
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Mark as refreshed
|
|
810
|
+
backgroundRefreshThrottle.set(path, Date.now());
|
|
811
|
+
|
|
812
|
+
// Clean up old throttle entries periodically (keep map from growing)
|
|
813
|
+
if (backgroundRefreshThrottle.size > 1000) {
|
|
814
|
+
const cutoff = Date.now() - BACKGROUND_REFRESH_INTERVAL;
|
|
815
|
+
for (const [key, timestamp] of backgroundRefreshThrottle) {
|
|
816
|
+
if (timestamp < cutoff) {
|
|
817
|
+
backgroundRefreshThrottle.delete(key);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
429
822
|
const filter = {
|
|
430
823
|
kinds: [kind],
|
|
431
824
|
authors: options.authors || [this.publicKey],
|
|
432
825
|
'#d': [path],
|
|
433
826
|
limit: 1,
|
|
434
827
|
};
|
|
828
|
+
const cacheKey = JSON.stringify(filter);
|
|
435
829
|
|
|
830
|
+
// Use our query deduplication by calling query() instead of pool.querySync() directly
|
|
436
831
|
const timeout = options.timeout || 30000;
|
|
437
|
-
const events = await this.
|
|
832
|
+
const events = await this._queryRelaysAndCache(filter, cacheKey, timeout);
|
|
438
833
|
|
|
439
834
|
// Filter by author (relays may not respect filter)
|
|
440
835
|
const authorFiltered = events.filter(e =>
|
|
@@ -462,22 +857,35 @@ export class NostrClient {
|
|
|
462
857
|
|
|
463
858
|
/**
|
|
464
859
|
* Internal method to refresh a prefix from relays
|
|
860
|
+
* Throttled to avoid flooding the relay with repeated requests
|
|
465
861
|
* @private
|
|
466
862
|
*/
|
|
467
863
|
async _doBackgroundPrefixRefresh(prefix, kind, options) {
|
|
468
864
|
if (this.relays.length === 0) return;
|
|
469
865
|
|
|
866
|
+
// Throttle: Skip if we've refreshed this prefix recently
|
|
867
|
+
const throttleKey = `prefix:${prefix}`;
|
|
868
|
+
const lastRefresh = backgroundRefreshThrottle.get(throttleKey);
|
|
869
|
+
if (lastRefresh && Date.now() - lastRefresh < BACKGROUND_REFRESH_INTERVAL) {
|
|
870
|
+
return; // Skip - recently refreshed
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Mark as refreshed
|
|
874
|
+
backgroundRefreshThrottle.set(throttleKey, Date.now());
|
|
875
|
+
|
|
470
876
|
// Query with wildcard-ish filter (relays handle d-tag prefix matching)
|
|
471
877
|
const filter = {
|
|
472
878
|
kinds: [kind],
|
|
473
879
|
authors: options.authors || [this.publicKey],
|
|
474
880
|
limit: options.limit || 1000,
|
|
475
881
|
};
|
|
882
|
+
const cacheKey = JSON.stringify(filter);
|
|
476
883
|
|
|
884
|
+
// Use our query deduplication
|
|
477
885
|
const timeout = options.timeout || 30000;
|
|
478
|
-
let events = await this.
|
|
886
|
+
let events = await this._queryRelaysAndCache(filter, cacheKey, timeout);
|
|
479
887
|
|
|
480
|
-
// Filter by author
|
|
888
|
+
// Filter by author (already done by _queryRelaysAndCache, but double-check)
|
|
481
889
|
events = events.filter(e =>
|
|
482
890
|
(options.authors || [this.publicKey]).includes(e.pubkey)
|
|
483
891
|
);
|
|
@@ -584,6 +992,7 @@ export class NostrClient {
|
|
|
584
992
|
|
|
585
993
|
/**
|
|
586
994
|
* Subscribe to events
|
|
995
|
+
* Uses subscription deduplication to avoid creating multiple identical subscriptions
|
|
587
996
|
* @param {Object} filter - Nostr filter object
|
|
588
997
|
* @param {Function} onEvent - Callback for each event
|
|
589
998
|
* @param {Object} options - Subscription options
|
|
@@ -593,7 +1002,8 @@ export class NostrClient {
|
|
|
593
1002
|
// Ensure initialization is complete
|
|
594
1003
|
await this._initReady;
|
|
595
1004
|
|
|
596
|
-
const subId = `sub-${Date.now()}-${Math.random().toString(36).
|
|
1005
|
+
const subId = `sub-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
1006
|
+
const filterKey = JSON.stringify(filter);
|
|
597
1007
|
|
|
598
1008
|
// If no relays, check cache for matching events and trigger callbacks
|
|
599
1009
|
if (this.relays.length === 0) {
|
|
@@ -630,6 +1040,43 @@ export class NostrClient {
|
|
|
630
1040
|
};
|
|
631
1041
|
}
|
|
632
1042
|
|
|
1043
|
+
// Check if we already have an active subscription for this filter
|
|
1044
|
+
const existing = activeSubscriptions.get(filterKey);
|
|
1045
|
+
if (existing) {
|
|
1046
|
+
// Add callback to existing subscription
|
|
1047
|
+
existing.callbacks.add(onEvent);
|
|
1048
|
+
existing.refCount++;
|
|
1049
|
+
|
|
1050
|
+
// Return wrapper that removes this specific callback on unsubscribe
|
|
1051
|
+
return {
|
|
1052
|
+
id: subId,
|
|
1053
|
+
unsubscribe: () => {
|
|
1054
|
+
existing.callbacks.delete(onEvent);
|
|
1055
|
+
existing.refCount--;
|
|
1056
|
+
|
|
1057
|
+
// Only close actual subscription when no more callbacks
|
|
1058
|
+
if (existing.refCount === 0) {
|
|
1059
|
+
if (existing.subscription && existing.subscription.close) {
|
|
1060
|
+
existing.subscription.close();
|
|
1061
|
+
}
|
|
1062
|
+
activeSubscriptions.delete(filterKey);
|
|
1063
|
+
}
|
|
1064
|
+
this._subscriptions.delete(subId);
|
|
1065
|
+
},
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Create new subscription with shared callback dispatcher
|
|
1070
|
+
const callbacks = new Set([onEvent]);
|
|
1071
|
+
const subscriptionInfo = {
|
|
1072
|
+
callbacks,
|
|
1073
|
+
refCount: 1,
|
|
1074
|
+
subscription: null,
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
// Store before creating subscription to handle race conditions
|
|
1078
|
+
activeSubscriptions.set(filterKey, subscriptionInfo);
|
|
1079
|
+
|
|
633
1080
|
const sub = this.pool.subscribeMany(
|
|
634
1081
|
this.relays,
|
|
635
1082
|
[filter],
|
|
@@ -645,7 +1092,18 @@ export class NostrClient {
|
|
|
645
1092
|
}
|
|
646
1093
|
|
|
647
1094
|
this._cacheEvent(event);
|
|
648
|
-
|
|
1095
|
+
|
|
1096
|
+
// Dispatch to ALL registered callbacks for this subscription
|
|
1097
|
+
const subInfo = activeSubscriptions.get(filterKey);
|
|
1098
|
+
if (subInfo) {
|
|
1099
|
+
for (const cb of subInfo.callbacks) {
|
|
1100
|
+
try {
|
|
1101
|
+
cb(event);
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
console.warn('[nostr] Subscription callback error:', err.message);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
649
1107
|
},
|
|
650
1108
|
oneose: () => {
|
|
651
1109
|
if (options.onEOSE) options.onEOSE();
|
|
@@ -653,12 +1111,21 @@ export class NostrClient {
|
|
|
653
1111
|
}
|
|
654
1112
|
);
|
|
655
1113
|
|
|
1114
|
+
// Store the actual subscription object
|
|
1115
|
+
subscriptionInfo.subscription = sub;
|
|
656
1116
|
this._subscriptions.set(subId, sub);
|
|
657
1117
|
|
|
658
1118
|
return {
|
|
659
1119
|
id: subId,
|
|
660
1120
|
unsubscribe: () => {
|
|
661
|
-
|
|
1121
|
+
callbacks.delete(onEvent);
|
|
1122
|
+
subscriptionInfo.refCount--;
|
|
1123
|
+
|
|
1124
|
+
// Only close actual subscription when no more callbacks
|
|
1125
|
+
if (subscriptionInfo.refCount === 0) {
|
|
1126
|
+
if (sub.close) sub.close();
|
|
1127
|
+
activeSubscriptions.delete(filterKey);
|
|
1128
|
+
}
|
|
662
1129
|
this._subscriptions.delete(subId);
|
|
663
1130
|
},
|
|
664
1131
|
};
|
|
@@ -708,16 +1175,26 @@ export class NostrClient {
|
|
|
708
1175
|
|
|
709
1176
|
/**
|
|
710
1177
|
* Invalidate query caches that might be affected by a new event
|
|
1178
|
+
* Uses reverse index for O(1) lookup instead of O(n) scan
|
|
711
1179
|
* @private
|
|
712
1180
|
*/
|
|
713
1181
|
_invalidateQueryCachesForEvent(event) {
|
|
714
|
-
//
|
|
715
|
-
|
|
1182
|
+
// Use reverse index for fast lookup - only check caches that could match this event's kind
|
|
1183
|
+
const indexedKeys = this._cacheIndex.get(event.kind);
|
|
1184
|
+
if (!indexedKeys || indexedKeys.size === 0) {
|
|
1185
|
+
return; // No cached queries for this kind
|
|
1186
|
+
}
|
|
1187
|
+
|
|
716
1188
|
const keysToDelete = [];
|
|
717
1189
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1190
|
+
// Only iterate over cache entries that match the event's kind
|
|
1191
|
+
for (const cacheKey of indexedKeys) {
|
|
1192
|
+
const cached = this._eventCache.get(cacheKey);
|
|
1193
|
+
if (!cached) {
|
|
1194
|
+
// Cache entry was evicted, clean up index
|
|
1195
|
+
indexedKeys.delete(cacheKey);
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
721
1198
|
|
|
722
1199
|
try {
|
|
723
1200
|
const filter = JSON.parse(cacheKey);
|
|
@@ -726,38 +1203,42 @@ export class NostrClient {
|
|
|
726
1203
|
keysToDelete.push(cacheKey);
|
|
727
1204
|
}
|
|
728
1205
|
} catch {
|
|
729
|
-
// Not a JSON key,
|
|
1206
|
+
// Not a valid JSON key, clean up index
|
|
1207
|
+
indexedKeys.delete(cacheKey);
|
|
730
1208
|
}
|
|
731
1209
|
}
|
|
732
1210
|
|
|
733
1211
|
for (const key of keysToDelete) {
|
|
734
1212
|
this._eventCache.delete(key);
|
|
1213
|
+
this._unindexCacheEntry(key);
|
|
735
1214
|
}
|
|
736
1215
|
}
|
|
737
1216
|
|
|
738
1217
|
/**
|
|
739
|
-
* Cache event in memory and persist
|
|
1218
|
+
* Cache event in memory and persist (batched)
|
|
740
1219
|
* @private
|
|
741
1220
|
*/
|
|
742
1221
|
async _cacheEvent(event) {
|
|
743
|
-
// Cache in memory
|
|
1222
|
+
// Cache in memory (synchronous - immediate for local-first reads)
|
|
744
1223
|
this._cacheEventSync(event);
|
|
745
1224
|
|
|
746
|
-
//
|
|
1225
|
+
// Queue for batched persistence (async - batches writes for I/O efficiency)
|
|
747
1226
|
if (this.persistentStorage) {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
storageKey = dTag[1]; // Use d-tag as key for replaceable events
|
|
755
|
-
}
|
|
1227
|
+
// For replaceable events, use d-tag as key
|
|
1228
|
+
let storageKey = event.id;
|
|
1229
|
+
if (event.kind >= 30000 && event.kind < 40000) {
|
|
1230
|
+
const dTag = event.tags.find(t => t[0] === 'd');
|
|
1231
|
+
if (dTag && dTag[1]) {
|
|
1232
|
+
storageKey = dTag[1]; // Use d-tag as key for replaceable events
|
|
756
1233
|
}
|
|
1234
|
+
}
|
|
757
1235
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1236
|
+
// Queue for batched write (overwrites previous if same key)
|
|
1237
|
+
this._persistQueue.set(storageKey, event);
|
|
1238
|
+
|
|
1239
|
+
// Schedule batch flush if not already scheduled
|
|
1240
|
+
if (!this._persistTimer) {
|
|
1241
|
+
this._persistTimer = setTimeout(() => this._flushPersistQueue(), this._persistBatchMs);
|
|
761
1242
|
}
|
|
762
1243
|
}
|
|
763
1244
|
|
|
@@ -776,6 +1257,34 @@ export class NostrClient {
|
|
|
776
1257
|
}
|
|
777
1258
|
}
|
|
778
1259
|
|
|
1260
|
+
/**
|
|
1261
|
+
* Flush batched persistent writes
|
|
1262
|
+
* @private
|
|
1263
|
+
*/
|
|
1264
|
+
async _flushPersistQueue() {
|
|
1265
|
+
this._persistTimer = null;
|
|
1266
|
+
|
|
1267
|
+
if (!this.persistentStorage || this._persistQueue.size === 0) {
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// Take snapshot of current queue and clear it
|
|
1272
|
+
const toWrite = Array.from(this._persistQueue.entries());
|
|
1273
|
+
this._persistQueue.clear();
|
|
1274
|
+
|
|
1275
|
+
// Write all queued events
|
|
1276
|
+
const writePromises = toWrite.map(async ([key, event]) => {
|
|
1277
|
+
try {
|
|
1278
|
+
await this.persistentStorage.put(key, event);
|
|
1279
|
+
} catch (error) {
|
|
1280
|
+
console.warn(`Failed to persist event ${key}:`, error.message);
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
// Wait for all writes to complete
|
|
1285
|
+
await Promise.all(writePromises);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
779
1288
|
/**
|
|
780
1289
|
* Get cached events matching a filter
|
|
781
1290
|
* @private
|
|
@@ -892,13 +1401,32 @@ export class NostrClient {
|
|
|
892
1401
|
|
|
893
1402
|
/**
|
|
894
1403
|
* Close all connections and subscriptions
|
|
1404
|
+
* @param {Object} options - Close options
|
|
1405
|
+
* @param {boolean} options.flush - Flush pending writes before closing (default: true)
|
|
895
1406
|
*/
|
|
896
|
-
close() {
|
|
1407
|
+
async close(options = {}) {
|
|
1408
|
+
const shouldFlush = options.flush !== false;
|
|
1409
|
+
|
|
1410
|
+
// Flush pending persistent writes before closing
|
|
1411
|
+
if (shouldFlush && this._persistTimer) {
|
|
1412
|
+
clearTimeout(this._persistTimer);
|
|
1413
|
+
await this._flushPersistQueue();
|
|
1414
|
+
}
|
|
1415
|
+
|
|
897
1416
|
// Stop background sync service
|
|
898
1417
|
if (this.syncService) {
|
|
899
1418
|
this.syncService.stop();
|
|
900
1419
|
}
|
|
901
1420
|
|
|
1421
|
+
// Close long-lived author subscription
|
|
1422
|
+
const authorSub = authorSubscriptions.get(this.publicKey);
|
|
1423
|
+
if (authorSub && authorSub.subscription) {
|
|
1424
|
+
if (authorSub.subscription.close) {
|
|
1425
|
+
authorSub.subscription.close();
|
|
1426
|
+
}
|
|
1427
|
+
authorSubscriptions.delete(this.publicKey);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
902
1430
|
// Close all subscriptions
|
|
903
1431
|
for (const sub of this._subscriptions.values()) {
|
|
904
1432
|
if (sub.close) {
|
|
@@ -913,8 +1441,9 @@ export class NostrClient {
|
|
|
913
1441
|
// Close pool
|
|
914
1442
|
this.pool.close(this.relays);
|
|
915
1443
|
|
|
916
|
-
// Clear cache
|
|
1444
|
+
// Clear cache and index
|
|
917
1445
|
this._eventCache.clear();
|
|
1446
|
+
this._cacheIndex.clear();
|
|
918
1447
|
}
|
|
919
1448
|
|
|
920
1449
|
/**
|