Browse Source

bug-fixes

Nostr-Signature: 2040e0adbed520ee9a21c6a1c7df48fae27021c1d3474b584388cd5ddafc6a49 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 893b4881e3876c0f556e3be991e9c6e99c9f5933bc9755e4075c1d0bfea95750b2318f3d3409d689c7e9a862cf053db0e7d3083ee28cf48ffbe794583c3ad783
main
Silberengel 3 weeks ago
parent
commit
2854b395c6
  1. 1
      nostr/commit-signatures.jsonl
  2. 55
      src/lib/services/nostr/nostr-client.ts
  3. 336
      src/lib/services/nostr/persistent-event-cache.ts

1
nostr/commit-signatures.jsonl

@ -18,3 +18,4 @@
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771584107,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","pubkey lookup for maintainer\ninclude all tags in the r.a. preset\nupdate client tags on publish\nadd verification/correction step"]],"content":"Signed commit: pubkey lookup for maintainer\ninclude all tags in the r.a. preset\nupdate client tags on publish\nadd verification/correction step","id":"cc27d54e23cecca7e126e7a1b9e0881ee9c9addf39a97841992ac35422221e5d","sig":"7c5e7173e4bfc17a71cec49c8ac2fad15ecab3a84ef53ac90ba7ab6f1c051e2e6d108cecfa075917b6be8a9d1d54d3995595a0b95c004995ec89fe8a621315cd"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771584107,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","pubkey lookup for maintainer\ninclude all tags in the r.a. preset\nupdate client tags on publish\nadd verification/correction step"]],"content":"Signed commit: pubkey lookup for maintainer\ninclude all tags in the r.a. preset\nupdate client tags on publish\nadd verification/correction step","id":"cc27d54e23cecca7e126e7a1b9e0881ee9c9addf39a97841992ac35422221e5d","sig":"7c5e7173e4bfc17a71cec49c8ac2fad15ecab3a84ef53ac90ba7ab6f1c051e2e6d108cecfa075917b6be8a9d1d54d3995595a0b95c004995ec89fe8a621315cd"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771584611,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix login persistence"]],"content":"Signed commit: fix login persistence","id":"e02d4dbaf56fb0498ca6871ae25bd5da1061eeca1d28c88d54ff5f6549982f11","sig":"647fa0385224b33546c55c786b3c2cf3b2cfab5de9f9748ce814e40e8c6819131ebb9e86d7682bffa327e3b690297f17bcfb2f6b2d5fb6b65e1d9474d66659b1"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771584611,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix login persistence"]],"content":"Signed commit: fix login persistence","id":"e02d4dbaf56fb0498ca6871ae25bd5da1061eeca1d28c88d54ff5f6549982f11","sig":"647fa0385224b33546c55c786b3c2cf3b2cfab5de9f9748ce814e40e8c6819131ebb9e86d7682bffa327e3b690297f17bcfb2f6b2d5fb6b65e1d9474d66659b1"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771587832,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","implemented IndexedDB to organize the persistent event cache\nbackground deletion removal\ncorrected and expanded search and added cancel button\nshow maintainers on the search result cards\nremove code search\nremoved hard-coded theme classes"]],"content":"Signed commit: implemented IndexedDB to organize the persistent event cache\nbackground deletion removal\ncorrected and expanded search and added cancel button\nshow maintainers on the search result cards\nremove code search\nremoved hard-coded theme classes","id":"8080f3cad9abacfc9a5fe08bc26744ff8444d0228ea8a6e8a449c8c2704885d6","sig":"70120c99f5e8a1e9df6d74af756a51641c4998265b9233d5a7d187d9e21302dc6377ae274b07be4d6515af1dabfada43fa9af1a087a34e2879b028ac34e551ca"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771587832,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","implemented IndexedDB to organize the persistent event cache\nbackground deletion removal\ncorrected and expanded search and added cancel button\nshow maintainers on the search result cards\nremove code search\nremoved hard-coded theme classes"]],"content":"Signed commit: implemented IndexedDB to organize the persistent event cache\nbackground deletion removal\ncorrected and expanded search and added cancel button\nshow maintainers on the search result cards\nremove code search\nremoved hard-coded theme classes","id":"8080f3cad9abacfc9a5fe08bc26744ff8444d0228ea8a6e8a449c8c2704885d6","sig":"70120c99f5e8a1e9df6d74af756a51641c4998265b9233d5a7d187d9e21302dc6377ae274b07be4d6515af1dabfada43fa9af1a087a34e2879b028ac34e551ca"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771604372,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"9a1ba983e0b0db8cff3675a078a376df5c9ad351c3988ea893f3e8084a65a1e6","sig":"724a326cbd6a33f1ff6a2c37b242c7571e35149281609e9eb1c6a197422a13834d9ac2f5d0719026bc66126bd0022df49adf50aa08af93dd95076f407b0f0456"}

55
src/lib/services/nostr/nostr-client.ts

@ -6,7 +6,7 @@ import type { NostrEvent, NostrFilter } from '../../types/nostr.js';
import logger from '../logger.js'; import logger from '../logger.js';
import { isNIP07Available, getPublicKeyWithNIP07, signEventWithNIP07 } from './nip07-signer.js'; import { isNIP07Available, getPublicKeyWithNIP07, signEventWithNIP07 } from './nip07-signer.js';
import { shouldUseTor, getTorProxy } from '../../utils/tor.js'; import { shouldUseTor, getTorProxy } from '../../utils/tor.js';
import { eventCache } from './event-cache.js'; // Removed separate in-memory cache - persistent cache now has built-in memory layer
import { KIND } from '../../types/nostr.js'; import { KIND } from '../../types/nostr.js';
// Replaceable event kinds (only latest per pubkey matters) // Replaceable event kinds (only latest per pubkey matters)
@ -223,10 +223,24 @@ export class NostrClient {
const hasSearchQuery = filters.some(f => f.search && f.search.trim().length > 0); const hasSearchQuery = filters.some(f => f.search && f.search.trim().length > 0);
if (!hasSearchQuery) { if (!hasSearchQuery) {
// 1. Check persistent cache first (IndexedDB) - only in browser // Check persistent cache (has built-in in-memory layer for fast access)
const persistentCache = await getPersistentCache(); const persistentCache = await getPersistentCache();
if (persistentCache) { if (persistentCache) {
try { try {
// First try synchronous memory cache (fast)
const memoryCached = persistentCache.getSync(filters);
if (memoryCached && memoryCached.length > 0) {
logger.debug({ filters, cachedCount: memoryCached.length }, 'Returning cached events from memory');
// Return cached events immediately, but also fetch from relays in background to update cache
this.fetchAndMergeFromRelays(filters, memoryCached).catch(err => {
logger.debug({ error: err, filters }, 'Background fetch failed, using cached events');
});
return memoryCached;
}
// If not in memory, check IndexedDB (async)
const cachedEvents = await persistentCache.get(filters); const cachedEvents = await persistentCache.get(filters);
if (cachedEvents && cachedEvents.length > 0) { if (cachedEvents && cachedEvents.length > 0) {
logger.debug({ filters, cachedCount: cachedEvents.length }, 'Returning cached events from IndexedDB'); logger.debug({ filters, cachedCount: cachedEvents.length }, 'Returning cached events from IndexedDB');
@ -242,24 +256,6 @@ export class NostrClient {
logger.debug({ error, filters }, 'Error reading from persistent cache, falling back'); logger.debug({ error, filters }, 'Error reading from persistent cache, falling back');
} }
} }
// 2. Check in-memory cache as fallback
const memoryCached = eventCache.get(filters);
if (memoryCached !== null && memoryCached.length > 0) {
logger.debug({ filters, cachedCount: memoryCached.length }, 'Returning cached events from memory');
// Also store in persistent cache and fetch from relays in background
if (persistentCache) {
persistentCache.set(filters, memoryCached).catch(err => {
logger.debug({ error: err }, 'Failed to persist cache');
});
}
this.fetchAndMergeFromRelays(filters, memoryCached).catch(err => {
logger.debug({ error: err, filters }, 'Background fetch failed');
});
return memoryCached;
}
} else { } else {
logger.debug({ filters }, 'Skipping cache for search query'); logger.debug({ filters }, 'Skipping cache for search query');
} }
@ -332,12 +328,11 @@ export class NostrClient {
// Get persistent cache once (if available) // Get persistent cache once (if available)
const persistentCache = await getPersistentCache(); const persistentCache = await getPersistentCache();
// Cache in both persistent and in-memory caches // Cache in persistent cache (has built-in in-memory layer)
// For kind 0 (profile) events, also cache individually by pubkey // For kind 0 (profile) events, also cache individually by pubkey
const profileEvents = finalEvents.filter(e => e.kind === 0); const profileEvents = finalEvents.filter(e => e.kind === 0);
for (const profileEvent of profileEvents) { for (const profileEvent of profileEvents) {
eventCache.setProfile(profileEvent.pubkey, profileEvent); // Cache profile in persistent cache (which also updates its memory layer)
// Also cache in persistent cache if available
if (persistentCache) { if (persistentCache) {
persistentCache.setProfile(profileEvent.pubkey, profileEvent).catch(err => { persistentCache.setProfile(profileEvent.pubkey, profileEvent).catch(err => {
logger.debug({ error: err, pubkey: profileEvent.pubkey }, 'Failed to cache profile'); logger.debug({ error: err, pubkey: profileEvent.pubkey }, 'Failed to cache profile');
@ -352,10 +347,7 @@ export class NostrClient {
// Cache successful fetches for 5 minutes, empty results for 1 minute // Cache successful fetches for 5 minutes, empty results for 1 minute
const ttl = finalEvents.length > 0 ? 5 * 60 * 1000 : 60 * 1000; const ttl = finalEvents.length > 0 ? 5 * 60 * 1000 : 60 * 1000;
// Update in-memory cache // Update persistent cache (which also updates its built-in memory layer)
eventCache.set(filters, finalEvents, ttl);
// Update persistent cache (async, don't wait) - only in browser
if (persistentCache) { if (persistentCache) {
persistentCache.set(filters, finalEvents, ttl).catch(err => { persistentCache.set(filters, finalEvents, ttl).catch(err => {
logger.debug({ error: err, filters }, 'Failed to update persistent cache'); logger.debug({ error: err, filters }, 'Failed to update persistent cache');
@ -427,10 +419,7 @@ export class NostrClient {
const deletionEvents = Array.from(uniqueDeletionEvents.values()); const deletionEvents = Array.from(uniqueDeletionEvents.values());
if (deletionEvents.length > 0) { if (deletionEvents.length > 0) {
// Process deletions in in-memory cache // Process deletions in persistent cache (which also handles its memory layer)
eventCache.processDeletionEvents(deletionEvents);
// Process deletions in persistent cache
const persistentCache = await getPersistentCache(); const persistentCache = await getPersistentCache();
if (persistentCache && typeof persistentCache.processDeletionEvents === 'function') { if (persistentCache && typeof persistentCache.processDeletionEvents === 'function') {
await persistentCache.processDeletionEvents(deletionEvents); await persistentCache.processDeletionEvents(deletionEvents);
@ -599,9 +588,7 @@ export class NostrClient {
// Invalidate cache for events from this pubkey (new event published) // Invalidate cache for events from this pubkey (new event published)
// This ensures fresh data on next fetch // This ensures fresh data on next fetch
if (success.length > 0) { if (success.length > 0) {
eventCache.invalidatePubkey(event.pubkey); // Invalidate persistent cache (which also handles its memory layer)
// Also invalidate persistent cache
const persistentCache = await getPersistentCache(); const persistentCache = await getPersistentCache();
if (persistentCache) { if (persistentCache) {
persistentCache.invalidatePubkey(event.pubkey).catch(err => { persistentCache.invalidatePubkey(event.pubkey).catch(err => {

336
src/lib/services/nostr/persistent-event-cache.ts

@ -139,6 +139,12 @@ function eventMatchesAnyFilter(event: NostrEvent, filters: NostrFilter[]): boole
return filters.some(filter => eventMatchesFilter(event, filter)); return filters.some(filter => eventMatchesFilter(event, filter));
} }
interface InMemoryCacheEntry {
events: NostrEvent[];
timestamp: number;
ttl: number;
}
export class PersistentEventCache { export class PersistentEventCache {
private db: IDBDatabase | null = null; private db: IDBDatabase | null = null;
private initPromise: Promise<void> | null = null; private initPromise: Promise<void> | null = null;
@ -147,6 +153,12 @@ export class PersistentEventCache {
private maxCacheAge: number = 7 * 24 * 60 * 60 * 1000; // 7 days max age private maxCacheAge: number = 7 * 24 * 60 * 60 * 1000; // 7 days max age
private writeQueue: Array<() => Promise<void>> = []; private writeQueue: Array<() => Promise<void>> = [];
private isProcessingQueue: boolean = false; private isProcessingQueue: boolean = false;
private queueProcessingPromise: Promise<void> | null = null;
// In-memory read-through cache for fast synchronous access
// This eliminates the need for a separate in-memory cache
private memoryCache: Map<string, InMemoryCacheEntry> = new Map();
private maxMemoryCacheSize: number = 1000; // Limit memory cache size
constructor() { constructor() {
this.init(); this.init();
@ -208,8 +220,24 @@ export class PersistentEventCache {
/** /**
* Get events from cache that match the filters * Get events from cache that match the filters
* Uses in-memory read-through cache for fast synchronous access
*/ */
async get(filters: NostrFilter[]): Promise<NostrEvent[] | null> { async get(filters: NostrFilter[]): Promise<NostrEvent[] | null> {
const filterKey = generateMultiFilterKey(filters);
const now = Date.now();
// 1. Check in-memory cache first (synchronous, fast)
const memoryEntry = this.memoryCache.get(filterKey);
if (memoryEntry) {
const age = now - memoryEntry.timestamp;
if (age < memoryEntry.ttl) {
// Cache hit - return immediately
return memoryEntry.events;
}
// Expired in memory, but might still be in IndexedDB - continue to check
}
// 2. Check IndexedDB (async, slower but persistent)
await this.init(); await this.init();
if (!this.db) { if (!this.db) {
@ -217,8 +245,6 @@ export class PersistentEventCache {
} }
try { try {
const filterKey = generateMultiFilterKey(filters);
// Check filter cache first // Check filter cache first
const filterEntry = await this.getFilterEntry(filterKey); const filterEntry = await this.getFilterEntry(filterKey);
if (!filterEntry) { if (!filterEntry) {
@ -226,7 +252,6 @@ export class PersistentEventCache {
} }
// Check if filter cache is expired // Check if filter cache is expired
const now = Date.now();
if (now - filterEntry.cachedAt > filterEntry.ttl) { if (now - filterEntry.cachedAt > filterEntry.ttl) {
// Expired, but we can still return events if they exist // Expired, but we can still return events if they exist
// Don't delete, just mark as stale // Don't delete, just mark as stale
@ -274,13 +299,64 @@ export class PersistentEventCache {
// Sort by created_at descending // Sort by created_at descending
result.sort((a, b) => b.created_at - a.created_at); result.sort((a, b) => b.created_at - a.created_at);
return result.length > 0 ? result : null; // Update in-memory cache with result from IndexedDB
if (result.length > 0) {
const ttl = filterEntry.ttl;
this.updateMemoryCache(filterKey, result, ttl);
return result;
}
return null;
} catch (error) { } catch (error) {
logger.error({ error, filters }, 'Error reading from event cache'); logger.error({ error, filters }, 'Error reading from event cache');
return null; return null;
} }
} }
/**
* Update in-memory cache and enforce size limit
*/
private updateMemoryCache(filterKey: string, events: NostrEvent[], ttl: number): void {
// Enforce size limit - remove oldest entries if needed
if (this.memoryCache.size >= this.maxMemoryCacheSize) {
// Remove oldest entry (simple FIFO - could be improved with LRU)
const firstKey = this.memoryCache.keys().next().value;
if (firstKey) {
this.memoryCache.delete(firstKey);
}
}
this.memoryCache.set(filterKey, {
events,
timestamp: Date.now(),
ttl
});
}
/**
* Get events synchronously from in-memory cache only (for fast access)
* Returns null if not in memory cache - use async get() for full cache access
*/
getSync(filters: NostrFilter[]): NostrEvent[] | null {
const filterKey = generateMultiFilterKey(filters);
const memoryEntry = this.memoryCache.get(filterKey);
if (!memoryEntry) {
return null;
}
const now = Date.now();
const age = now - memoryEntry.timestamp;
if (age < memoryEntry.ttl) {
return memoryEntry.events;
}
// Expired
this.memoryCache.delete(filterKey);
return null;
}
/** /**
* Get filter cache entry * Get filter cache entry
*/ */
@ -302,27 +378,42 @@ export class PersistentEventCache {
/** /**
* Process write queue to prevent concurrent IndexedDB transactions * Process write queue to prevent concurrent IndexedDB transactions
* Ensures only one processor runs at a time by tracking a promise
*/ */
private async processWriteQueue(): Promise<void> { private async processWriteQueue(): Promise<void> {
if (this.isProcessingQueue || this.writeQueue.length === 0) { // If already processing, wait for the current processor to finish
return; if (this.queueProcessingPromise) {
return this.queueProcessingPromise;
} }
this.isProcessingQueue = true; // If queue is empty, nothing to do
if (this.writeQueue.length === 0) {
return;
}
while (this.writeQueue.length > 0) { // Create a promise that processes the queue
const writeFn = this.writeQueue.shift(); this.queueProcessingPromise = (async () => {
if (writeFn) { this.isProcessingQueue = true;
try {
await writeFn(); try {
} catch (error) { while (this.writeQueue.length > 0) {
// Log but continue processing queue const writeFn = this.writeQueue.shift();
logger.debug({ error }, 'Error in write queue item'); if (writeFn) {
try {
await writeFn();
} catch (error) {
// Log but continue processing queue
logger.debug({ error }, 'Error in write queue item');
}
}
} }
} finally {
this.isProcessingQueue = false;
this.queueProcessingPromise = null;
} }
} })();
this.isProcessingQueue = false; return this.queueProcessingPromise;
} }
/** /**
@ -354,7 +445,8 @@ export class PersistentEventCache {
} }
}); });
// Process queue asynchronously // Process queue asynchronously (don't await, but track the promise)
// Multiple calls will share the same processing promise
this.processWriteQueue().catch(err => { this.processWriteQueue().catch(err => {
if (!resolved) { if (!resolved) {
resolved = true; resolved = true;
@ -391,6 +483,18 @@ export class PersistentEventCache {
const existingEntry = await this.getFilterEntry(filterKey); const existingEntry = await this.getFilterEntry(filterKey);
const existingEventIds = new Set(existingEntry?.eventIds || []); const existingEventIds = new Set(existingEntry?.eventIds || []);
// Quick check: if all events already exist and TTL hasn't expired, skip the write
const allEventsExist = events.length > 0 && events.every(e => existingEventIds.has(e.id));
if (allEventsExist && existingEntry) {
const now = Date.now();
const age = now - existingEntry.cachedAt;
// If cache is still fresh (within 80% of TTL), skip the write
if (age < (existingEntry.ttl * 0.8)) {
logger.debug({ filterKey, eventCount: events.length }, 'All events already cached and fresh, skipping write');
return;
}
}
// Use a single transaction for all operations // Use a single transaction for all operations
const transaction = this.db.transaction([STORE_EVENTS, STORE_PROFILES, STORE_FILTERS], 'readwrite'); const transaction = this.db.transaction([STORE_EVENTS, STORE_PROFILES, STORE_FILTERS], 'readwrite');
const eventStore = transaction.objectStore(STORE_EVENTS); const eventStore = transaction.objectStore(STORE_EVENTS);
@ -404,11 +508,17 @@ export class PersistentEventCache {
// For replaceable events, check if we have a newer version for this pubkey // For replaceable events, check if we have a newer version for this pubkey
if (REPLACEABLE_KINDS.includes(event.kind)) { if (REPLACEABLE_KINDS.includes(event.kind)) {
// Check if we already have a newer replaceable event for this pubkey // Check if we already have a newer replaceable event for this pubkey
const existingProfile = await this.getProfile(event.pubkey); // Use the same transaction instead of calling getProfile (which creates a new transaction)
if (existingProfile && existingProfile.kind === event.kind && existingProfile.created_at >= event.created_at) { const existingProfile = await new Promise<CachedEvent | undefined>((resolve) => {
const req = profileStore.get(event.pubkey);
req.onsuccess = () => resolve(req.result);
req.onerror = () => resolve(undefined);
});
if (existingProfile && existingProfile.event.kind === event.kind && existingProfile.event.created_at >= event.created_at) {
// Existing event is newer or same, skip // Existing event is newer or same, skip
if (existingEventIds.has(existingProfile.id)) { if (existingEventIds.has(existingProfile.event.id)) {
newEventIds.push(existingProfile.id); newEventIds.push(existingProfile.event.id);
} }
continue; continue;
} }
@ -427,11 +537,31 @@ export class PersistentEventCache {
filterKey filterKey
}; };
await new Promise<void>((resolve, reject) => { try {
const request = eventStore.put(cached); await new Promise<void>((resolve, reject) => {
request.onsuccess = () => resolve(); const request = eventStore.put(cached);
request.onerror = () => reject(request.error); request.onsuccess = () => resolve();
}); request.onerror = () => {
const err = request.error;
// Handle transaction errors gracefully
if (err instanceof DOMException &&
(err.name === 'TransactionInactiveError' || err.name === 'InvalidStateError')) {
logger.debug({ error: err }, 'IndexedDB request error, transaction inactive');
resolve(); // Don't reject, just skip this write
return;
}
reject(err);
};
});
} catch (err) {
// If it's a transaction error, skip this event and continue
if (err instanceof DOMException &&
(err.name === 'TransactionInactiveError' || err.name === 'InvalidStateError')) {
logger.debug({ error: err }, 'IndexedDB transaction error during event store, skipping');
continue; // Skip this event
}
throw err; // Re-throw other errors
}
newEventIds.push(event.id); newEventIds.push(event.id);
@ -444,11 +574,32 @@ export class PersistentEventCache {
}); });
if (!existingProfile || event.created_at > existingProfile.event.created_at) { if (!existingProfile || event.created_at > existingProfile.event.created_at) {
await new Promise<void>((resolve, reject) => { try {
const req = profileStore.put({ pubkey: event.pubkey, ...cached }); await new Promise<void>((resolve, reject) => {
req.onsuccess = () => resolve(); const req = profileStore.put({ pubkey: event.pubkey, ...cached });
req.onerror = () => reject(req.error); req.onsuccess = () => resolve();
}); req.onerror = () => {
const err = req.error;
// Handle transaction errors gracefully
if (err instanceof DOMException &&
(err.name === 'TransactionInactiveError' || err.name === 'InvalidStateError')) {
logger.debug({ error: err }, 'IndexedDB request error in profile store, transaction inactive');
resolve(); // Don't reject, just skip this write
return;
}
reject(err);
};
});
} catch (err) {
// If it's a transaction error, skip this profile update and continue
if (err instanceof DOMException &&
(err.name === 'TransactionInactiveError' || err.name === 'InvalidStateError')) {
logger.debug({ error: err }, 'IndexedDB transaction error during profile store, skipping');
// Continue processing other events
} else {
throw err; // Re-throw other errors
}
}
} }
} }
} }
@ -464,17 +615,53 @@ export class PersistentEventCache {
ttl: effectiveTTL ttl: effectiveTTL
}; };
await new Promise<void>((resolve, reject) => { try {
const request = filterStore.put(filterEntry); await new Promise<void>((resolve, reject) => {
request.onsuccess = () => resolve(); const request = filterStore.put(filterEntry);
request.onerror = () => reject(request.error); request.onsuccess = () => resolve();
}); request.onerror = () => {
const err = request.error;
// Handle transaction errors gracefully
if (err instanceof DOMException &&
(err.name === 'TransactionInactiveError' || err.name === 'InvalidStateError')) {
logger.debug({ error: err }, 'IndexedDB request error in filter store, transaction inactive');
resolve(); // Don't reject, just skip this write
return;
}
reject(err);
};
});
// Wait for transaction to complete // Wait for transaction to complete
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
transaction.oncomplete = () => resolve(); transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error); transaction.onerror = () => {
}); const err = transaction.error;
// Handle transaction errors gracefully
if (err instanceof DOMException &&
(err.name === 'TransactionInactiveError' || err.name === 'InvalidStateError')) {
logger.debug({ error: err }, 'IndexedDB transaction error, transaction inactive');
resolve(); // Don't reject, just skip
return;
}
reject(err);
};
});
} catch (err) {
// If it's a transaction error, handle gracefully
if (err instanceof DOMException &&
(err.name === 'TransactionInactiveError' || err.name === 'InvalidStateError')) {
logger.debug({ error: err }, 'IndexedDB transaction error during filter update, skipping');
return; // Don't throw, just skip this write
}
throw err; // Re-throw other errors
}
// Also update in-memory cache for fast access
// Use the events we're setting (they'll be merged with existing in get() if needed)
if (events.length > 0) {
this.updateMemoryCache(filterKey, events, effectiveTTL);
}
logger.debug({ logger.debug({
filterKey, filterKey,
@ -483,16 +670,46 @@ export class PersistentEventCache {
ttl: effectiveTTL ttl: effectiveTTL
}, 'Cached events in IndexedDB'); }, 'Cached events in IndexedDB');
} catch (error) { } catch (error) {
// Check if it's a quota exceeded error or other recoverable error // Check error message first (works for all error types)
const errorMessage = error instanceof Error ? error.message : String(error);
const errorName = error instanceof DOMException ? error.name : '';
// Check if it's a quota exceeded error
if (error instanceof DOMException && error.name === 'QuotaExceededError') {
logger.warn({ error, filters }, 'IndexedDB quota exceeded, skipping cache write');
return; // Don't throw, just skip this write
}
// Check if it's any transaction-related error (by name or message)
// DOMException can have various names: TransactionInactiveError, InvalidStateError, AbortError, etc.
// Also check for any DOMException that might be transaction-related
const isTransactionError =
(error instanceof DOMException && (
error.name === 'TransactionInactiveError' ||
error.name === 'InvalidStateError' ||
error.name === 'AbortError' ||
error.name === 'ConstraintError' ||
error.name === 'DataError'
)) ||
errorMessage.toLowerCase().includes('transaction') ||
errorMessage.toLowerCase().includes('indexeddb') ||
errorMessage.toLowerCase().includes('inactive') ||
errorName.toLowerCase().includes('transaction') ||
errorName.toLowerCase().includes('inactive');
if (isTransactionError) {
// All transaction-related errors should be logged as debug, not error
logger.debug({ error, filters, errorName }, 'IndexedDB transaction error, likely concurrent write, skipping');
return; // Don't throw, just skip this write
}
// For any other DOMException, treat as potentially recoverable and log as debug
if (error instanceof DOMException) { if (error instanceof DOMException) {
if (error.name === 'QuotaExceededError') { logger.debug({ error, filters, errorName }, 'IndexedDB error (DOMException), skipping cache write');
logger.warn({ error, filters }, 'IndexedDB quota exceeded, skipping cache write'); return; // Don't throw, just skip this write
return; // Don't throw, just skip this write
} else if (error.name === 'TransactionInactiveError' || error.name === 'InvalidStateError') {
logger.debug({ error, filters }, 'IndexedDB transaction error, likely concurrent write, skipping');
return; // Don't throw, just skip this write
}
} }
// Only log as ERROR if it's not a DOMException or transaction-related error
logger.error({ error, filters }, 'Error writing to event cache'); logger.error({ error, filters }, 'Error writing to event cache');
throw error; // Re-throw other errors throw error; // Re-throw other errors
} }
@ -604,6 +821,14 @@ export class PersistentEventCache {
async invalidatePubkey(pubkey: string): Promise<void> { async invalidatePubkey(pubkey: string): Promise<void> {
await this.init(); await this.init();
// Clear in-memory cache entries that might contain events from this pubkey
// We need to check each entry and remove if it contains events from this pubkey
for (const [key, entry] of this.memoryCache.entries()) {
if (entry.events.some(e => e.pubkey === pubkey)) {
this.memoryCache.delete(key);
}
}
if (!this.db) { if (!this.db) {
return; return;
} }
@ -983,6 +1208,17 @@ export class PersistentEventCache {
}); });
} }
// Clear in-memory cache entries that contain deleted events
for (const [key, entry] of this.memoryCache.entries()) {
const hasDeletedEvent = entry.events.some(e =>
deletedEventIds.has(e.id) ||
(REPLACEABLE_KINDS.includes(e.kind) && deletedAddresses.has(`${e.kind}:${e.pubkey}:${e.tags.find(t => t[0] === 'd')?.[1] || ''}`))
);
if (hasDeletedEvent) {
this.memoryCache.delete(key);
}
}
if (removedCount > 0) { if (removedCount > 0) {
logger.debug({ removedCount, deletedEventIds: deletedEventIds.size, deletedAddresses: deletedAddresses.size }, 'Processed deletion events and removed from cache'); logger.debug({ removedCount, deletedEventIds: deletedEventIds.size, deletedAddresses: deletedAddresses.size }, 'Processed deletion events and removed from cache');
} }

Loading…
Cancel
Save