mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-12 00:17:02 +02:00
feat: implement generic replaceable event cache
Add configurable cache system for replaceable Nostr events (contacts, relay lists, blossom servers, emoji lists, etc). Stores raw events and parses on-demand using applesauce helpers for better performance. Changes: - Add replaceableEvents Dexie table with composite key [pubkey+kind+d] - Implement ReplaceableEventCache service with LRU memory cache - Refactor RelayListCache and BlossomServerCache to use generic cache - Add useReplaceableEventCacheSync hook to AppShell - Configure CACHED_KINDS: [3, 10002, 10063, 10030] Benefits: - Adding new cached kinds requires only updating CACHED_KINDS array - No pre-parsing overhead - applesauce helpers cache internally - Single cache implementation eliminates code duplication - Backward compatible API maintained for existing services
This commit is contained in:
@@ -3,6 +3,7 @@ import { Terminal } from "lucide-react";
|
||||
import { useAccountSync } from "@/hooks/useAccountSync";
|
||||
import { useRelayListCacheSync } from "@/hooks/useRelayListCacheSync";
|
||||
import { useBlossomServerCacheSync } from "@/hooks/useBlossomServerCacheSync";
|
||||
import { useReplaceableEventCacheSync } from "@/hooks/useReplaceableEventCacheSync";
|
||||
import { useRelayState } from "@/hooks/useRelayState";
|
||||
import relayStateManager from "@/services/relay-state-manager";
|
||||
import { TabBar } from "../TabBar";
|
||||
@@ -29,6 +30,9 @@ export function AppShell({ children, hideBottomBar = false }: AppShellProps) {
|
||||
// Auto-cache kind:10063 blossom server lists from EventStore to Dexie
|
||||
useBlossomServerCacheSync();
|
||||
|
||||
// Auto-cache generic replaceable events (contacts, relay lists, blossom servers, emoji lists, etc.)
|
||||
useReplaceableEventCacheSync();
|
||||
|
||||
// Initialize global relay state manager
|
||||
useEffect(() => {
|
||||
relayStateManager.initialize().catch((err) => {
|
||||
|
||||
26
src/hooks/useReplaceableEventCacheSync.ts
Normal file
26
src/hooks/useReplaceableEventCacheSync.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Hook to keep generic replaceable event cache in sync with EventStore
|
||||
*
|
||||
* Subscribes to configured kinds (contacts, relay lists, blossom servers, emoji lists, etc.)
|
||||
* and automatically caches them in Dexie.
|
||||
*
|
||||
* Should be used once at app root level.
|
||||
*/
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useEventStore } from "applesauce-react/hooks";
|
||||
import replaceableEventCache from "@/services/replaceable-event-cache";
|
||||
|
||||
export function useReplaceableEventCacheSync() {
|
||||
const eventStore = useEventStore();
|
||||
|
||||
useEffect(() => {
|
||||
// Subscribe to EventStore for auto-caching
|
||||
replaceableEventCache.subscribeToEventStore(eventStore);
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
replaceableEventCache.unsubscribe();
|
||||
};
|
||||
}, [eventStore]);
|
||||
}
|
||||
@@ -1,151 +1,39 @@
|
||||
/**
|
||||
* Blossom Server Cache Service
|
||||
*
|
||||
* Caches BUD-03 blossom server lists (kind:10063) in Dexie for fast access.
|
||||
* Reduces network requests and improves cold start performance.
|
||||
* Wrapper around generic ReplaceableEventCache for BUD-03 blossom server lists (kind:10063).
|
||||
* Provides convenient helpers for accessing blossom servers.
|
||||
*
|
||||
* Auto-caches kind:10063 events from EventStore when subscribed.
|
||||
* Now uses the generic cache for storage - parsing happens on-demand.
|
||||
*/
|
||||
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
import { getServersFromEvent } from "./blossom";
|
||||
import db, { CachedBlossomServerList } from "./db";
|
||||
import replaceableEventCache from "./replaceable-event-cache";
|
||||
import type { IEventStore } from "applesauce-core/event-store";
|
||||
import type { Subscription } from "rxjs";
|
||||
|
||||
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
||||
const MAX_MEMORY_CACHE = 100; // LRU cache size
|
||||
const BLOSSOM_SERVER_LIST_KIND = 10063;
|
||||
|
||||
class BlossomServerCache {
|
||||
private eventStoreSubscription: Subscription | null = null;
|
||||
private memoryCache = new Map<string, CachedBlossomServerList>();
|
||||
private cacheOrder: string[] = [];
|
||||
|
||||
/**
|
||||
* Subscribe to EventStore to auto-cache kind:10063 events
|
||||
* @deprecated - Now handled by generic ReplaceableEventCache
|
||||
* Kept for backward compatibility with existing code
|
||||
*/
|
||||
subscribeToEventStore(eventStore: IEventStore): void {
|
||||
if (this.eventStoreSubscription) {
|
||||
console.warn("[BlossomServerCache] Already subscribed to EventStore");
|
||||
return;
|
||||
}
|
||||
|
||||
// Subscribe to kind:10063 events
|
||||
this.eventStoreSubscription = eventStore
|
||||
.filters({ kinds: [10063] })
|
||||
.subscribe((event: NostrEvent) => {
|
||||
// Cache each kind:10063 event as it arrives
|
||||
this.set(event);
|
||||
});
|
||||
|
||||
console.log(
|
||||
"[BlossomServerCache] Subscribed to EventStore for kind:10063 events",
|
||||
subscribeToEventStore(_eventStore: IEventStore): void {
|
||||
console.warn(
|
||||
"[BlossomServerCache] subscribeToEventStore is deprecated - kind:10063 is now auto-cached by ReplaceableEventCache",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from EventStore
|
||||
* @deprecated - Now handled by generic ReplaceableEventCache
|
||||
* Kept for backward compatibility with existing code
|
||||
*/
|
||||
unsubscribe(): void {
|
||||
if (this.eventStoreSubscription) {
|
||||
this.eventStoreSubscription.unsubscribe();
|
||||
this.eventStoreSubscription = null;
|
||||
console.log("[BlossomServerCache] Unsubscribed from EventStore");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached blossom server list for a pubkey
|
||||
* Returns undefined if not cached or stale
|
||||
*/
|
||||
async get(pubkey: string): Promise<CachedBlossomServerList | undefined> {
|
||||
try {
|
||||
const cached = await db.blossomServers.get(pubkey);
|
||||
|
||||
// Check if stale (>24 hours)
|
||||
if (cached && Date.now() - cached.updatedAt < CACHE_TTL) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Stale or not found
|
||||
if (cached) {
|
||||
console.debug(
|
||||
`[BlossomServerCache] Cached server list for ${pubkey.slice(0, 8)} is stale (${Math.floor((Date.now() - cached.updatedAt) / 1000 / 60)}min old)`,
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[BlossomServerCache] Error reading cache for ${pubkey.slice(0, 8)}:`,
|
||||
error,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store blossom server list event in cache
|
||||
*/
|
||||
async set(event: NostrEvent): Promise<void> {
|
||||
try {
|
||||
if (event.kind !== 10063) {
|
||||
console.warn(
|
||||
`[BlossomServerCache] Attempted to cache non-10063 event (kind ${event.kind})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse servers from event
|
||||
const servers = getServersFromEvent(event);
|
||||
|
||||
// Store in Dexie and memory cache
|
||||
const cachedEntry: CachedBlossomServerList = {
|
||||
pubkey: event.pubkey,
|
||||
event,
|
||||
servers,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
await db.blossomServers.put(cachedEntry);
|
||||
|
||||
// Also populate memory cache
|
||||
this.memoryCache.set(event.pubkey, cachedEntry);
|
||||
this.cacheOrder.push(event.pubkey);
|
||||
this.evictOldest();
|
||||
|
||||
console.debug(
|
||||
`[BlossomServerCache] Cached ${servers.length} server(s) for ${event.pubkey.slice(0, 8)}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[BlossomServerCache] Error caching server list for ${event.pubkey.slice(0, 8)}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update LRU order for a pubkey
|
||||
*/
|
||||
private updateLRU(pubkey: string): void {
|
||||
const index = this.cacheOrder.indexOf(pubkey);
|
||||
if (index > -1) {
|
||||
this.cacheOrder.splice(index, 1);
|
||||
}
|
||||
this.cacheOrder.push(pubkey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict oldest entries from memory cache if over limit
|
||||
*/
|
||||
private evictOldest(): void {
|
||||
while (this.cacheOrder.length > MAX_MEMORY_CACHE) {
|
||||
const oldest = this.cacheOrder.shift();
|
||||
if (oldest) {
|
||||
this.memoryCache.delete(oldest);
|
||||
}
|
||||
}
|
||||
console.warn(
|
||||
"[BlossomServerCache] unsubscribe is deprecated - managed by ReplaceableEventCache",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,129 +42,42 @@ class BlossomServerCache {
|
||||
* Returns null if not in memory cache
|
||||
*/
|
||||
getServersSync(pubkey: string): string[] | null {
|
||||
const memCached = this.memoryCache.get(pubkey);
|
||||
if (memCached && Date.now() - memCached.updatedAt < CACHE_TTL) {
|
||||
this.updateLRU(pubkey);
|
||||
return memCached.servers;
|
||||
}
|
||||
return null;
|
||||
const event = replaceableEventCache.getSync(
|
||||
pubkey,
|
||||
BLOSSOM_SERVER_LIST_KIND,
|
||||
);
|
||||
if (!event) return null;
|
||||
|
||||
// Parse on-demand
|
||||
return getServersFromEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blossom servers for a pubkey from cache
|
||||
*/
|
||||
async getServers(pubkey: string): Promise<string[] | null> {
|
||||
// Check memory cache first (< 1ms)
|
||||
const memCached = this.memoryCache.get(pubkey);
|
||||
if (memCached && Date.now() - memCached.updatedAt < CACHE_TTL) {
|
||||
this.updateLRU(pubkey);
|
||||
return memCached.servers;
|
||||
}
|
||||
const event = await replaceableEventCache.getEvent(
|
||||
pubkey,
|
||||
BLOSSOM_SERVER_LIST_KIND,
|
||||
);
|
||||
if (!event) return null;
|
||||
|
||||
// Then check Dexie (5-10ms)
|
||||
const cached = await this.get(pubkey);
|
||||
if (cached) {
|
||||
// Populate memory cache for next time
|
||||
this.memoryCache.set(pubkey, cached);
|
||||
this.cacheOrder.push(pubkey);
|
||||
this.evictOldest();
|
||||
return cached.servers;
|
||||
}
|
||||
|
||||
return null;
|
||||
// Parse on-demand
|
||||
return getServersFromEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have a valid cache entry for a pubkey
|
||||
*/
|
||||
async has(pubkey: string): Promise<boolean> {
|
||||
const cached = await this.get(pubkey);
|
||||
return cached !== undefined;
|
||||
return replaceableEventCache.has(pubkey, BLOSSOM_SERVER_LIST_KIND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate (delete) cache entry for a pubkey
|
||||
*/
|
||||
async invalidate(pubkey: string): Promise<void> {
|
||||
try {
|
||||
await db.blossomServers.delete(pubkey);
|
||||
// Also remove from memory cache
|
||||
this.memoryCache.delete(pubkey);
|
||||
const index = this.cacheOrder.indexOf(pubkey);
|
||||
if (index > -1) {
|
||||
this.cacheOrder.splice(index, 1);
|
||||
}
|
||||
console.debug(
|
||||
`[BlossomServerCache] Invalidated cache for ${pubkey.slice(0, 8)}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[BlossomServerCache] Error invalidating cache for ${pubkey.slice(0, 8)}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached blossom server lists
|
||||
*/
|
||||
async clear(): Promise<void> {
|
||||
try {
|
||||
await db.blossomServers.clear();
|
||||
// Also clear memory cache
|
||||
this.memoryCache.clear();
|
||||
this.cacheOrder = [];
|
||||
console.log("[BlossomServerCache] Cleared all cached server lists");
|
||||
} catch (error) {
|
||||
console.error("[BlossomServerCache] Error clearing cache:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
async getStats(): Promise<{
|
||||
count: number;
|
||||
oldestEntry: number | null;
|
||||
newestEntry: number | null;
|
||||
memoryCacheSize: number;
|
||||
memoryCacheLimit: number;
|
||||
}> {
|
||||
try {
|
||||
const count = await db.blossomServers.count();
|
||||
const all = await db.blossomServers.toArray();
|
||||
|
||||
if (all.length === 0) {
|
||||
return {
|
||||
count: 0,
|
||||
oldestEntry: null,
|
||||
newestEntry: null,
|
||||
memoryCacheSize: this.memoryCache.size,
|
||||
memoryCacheLimit: MAX_MEMORY_CACHE,
|
||||
};
|
||||
}
|
||||
|
||||
const timestamps = all.map((entry) => entry.updatedAt);
|
||||
const oldest = Math.min(...timestamps);
|
||||
const newest = Math.max(...timestamps);
|
||||
|
||||
return {
|
||||
count,
|
||||
oldestEntry: oldest,
|
||||
newestEntry: newest,
|
||||
memoryCacheSize: this.memoryCache.size,
|
||||
memoryCacheLimit: MAX_MEMORY_CACHE,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[BlossomServerCache] Error getting stats:", error);
|
||||
return {
|
||||
count: 0,
|
||||
oldestEntry: null,
|
||||
newestEntry: null,
|
||||
memoryCacheSize: this.memoryCache.size,
|
||||
memoryCacheLimit: MAX_MEMORY_CACHE,
|
||||
};
|
||||
}
|
||||
return replaceableEventCache.invalidate(pubkey, BLOSSOM_SERVER_LIST_KIND);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,18 @@ export interface LocalSpellbook {
|
||||
deletedAt?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic cache for replaceable events (kind 3, 10002, 10063, 10030, etc.)
|
||||
* Stores raw events - parsing happens on-demand using applesauce helpers
|
||||
*/
|
||||
export interface CachedReplaceableEvent {
|
||||
pubkey: string;
|
||||
kind: number;
|
||||
d: string; // d-tag for parameterized replaceables (30000-39999), empty string for normal
|
||||
event: NostrEvent;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
class GrimoireDb extends Dexie {
|
||||
profiles!: Table<Profile>;
|
||||
nip05!: Table<Nip05>;
|
||||
@@ -96,6 +108,7 @@ class GrimoireDb extends Dexie {
|
||||
relayLists!: Table<CachedRelayList>;
|
||||
relayLiveness!: Table<RelayLivenessEntry>;
|
||||
blossomServers!: Table<CachedBlossomServerList>;
|
||||
replaceableEvents!: Table<CachedReplaceableEvent>;
|
||||
spells!: Table<LocalSpell>;
|
||||
spellbooks!: Table<LocalSpellbook>;
|
||||
|
||||
@@ -333,6 +346,21 @@ class GrimoireDb extends Dexie {
|
||||
spells: "&id, alias, createdAt, isPublished, deletedAt",
|
||||
spellbooks: "&id, slug, title, createdAt, isPublished, deletedAt",
|
||||
});
|
||||
|
||||
// Version 16: Add generic replaceable event cache
|
||||
this.version(16).stores({
|
||||
profiles: "&pubkey",
|
||||
nip05: "&nip05",
|
||||
nips: "&id",
|
||||
relayInfo: "&url",
|
||||
relayAuthPreferences: "&url",
|
||||
relayLists: "&pubkey, updatedAt",
|
||||
relayLiveness: "&url",
|
||||
blossomServers: "&pubkey, updatedAt",
|
||||
replaceableEvents: "[pubkey+kind+d], [pubkey+kind], kind, updatedAt",
|
||||
spells: "&id, alias, createdAt, isPublished, deletedAt",
|
||||
spellbooks: "&id, slug, title, createdAt, isPublished, deletedAt",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,177 +1,40 @@
|
||||
/**
|
||||
* Relay List Cache Service
|
||||
*
|
||||
* Caches NIP-65 relay lists (kind:10002) in Dexie for fast access.
|
||||
* Reduces network requests and improves cold start performance.
|
||||
* Wrapper around generic ReplaceableEventCache for NIP-65 relay lists (kind:10002).
|
||||
* Provides convenient helpers for accessing inbox/outbox relays.
|
||||
*
|
||||
* Auto-caches kind:10002 events from EventStore when subscribed.
|
||||
* Now uses the generic cache for storage - parsing happens on-demand using applesauce helpers.
|
||||
*/
|
||||
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
import { getInboxes, getOutboxes } from "applesauce-core/helpers";
|
||||
import { normalizeRelayURL } from "@/lib/relay-url";
|
||||
import db, { CachedRelayList } from "./db";
|
||||
import replaceableEventCache from "./replaceable-event-cache";
|
||||
import type { IEventStore } from "applesauce-core/event-store";
|
||||
import type { Subscription } from "rxjs";
|
||||
|
||||
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
||||
const MAX_MEMORY_CACHE = 100; // LRU cache size
|
||||
const RELAY_LIST_KIND = 10002;
|
||||
|
||||
class RelayListCache {
|
||||
private eventStoreSubscription: Subscription | null = null;
|
||||
private memoryCache = new Map<string, CachedRelayList>();
|
||||
private cacheOrder: string[] = [];
|
||||
|
||||
/**
|
||||
* Subscribe to EventStore to auto-cache kind:10002 events
|
||||
* @deprecated - Now handled by generic ReplaceableEventCache
|
||||
* Kept for backward compatibility with existing code
|
||||
*/
|
||||
subscribeToEventStore(eventStore: IEventStore): void {
|
||||
if (this.eventStoreSubscription) {
|
||||
console.warn("[RelayListCache] Already subscribed to EventStore");
|
||||
return;
|
||||
}
|
||||
|
||||
// Subscribe to kind:10002 events
|
||||
this.eventStoreSubscription = eventStore
|
||||
.filters({ kinds: [10002] })
|
||||
.subscribe((event: NostrEvent) => {
|
||||
// Cache each kind:10002 event as it arrives
|
||||
this.set(event);
|
||||
});
|
||||
|
||||
console.log(
|
||||
"[RelayListCache] Subscribed to EventStore for kind:10002 events",
|
||||
subscribeToEventStore(_eventStore: IEventStore): void {
|
||||
console.warn(
|
||||
"[RelayListCache] subscribeToEventStore is deprecated - kind:10002 is now auto-cached by ReplaceableEventCache",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from EventStore
|
||||
* @deprecated - Now handled by generic ReplaceableEventCache
|
||||
* Kept for backward compatibility with existing code
|
||||
*/
|
||||
unsubscribe(): void {
|
||||
if (this.eventStoreSubscription) {
|
||||
this.eventStoreSubscription.unsubscribe();
|
||||
this.eventStoreSubscription = null;
|
||||
console.log("[RelayListCache] Unsubscribed from EventStore");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached relay list for a pubkey
|
||||
* Returns undefined if not cached or stale
|
||||
*/
|
||||
async get(pubkey: string): Promise<CachedRelayList | undefined> {
|
||||
try {
|
||||
const cached = await db.relayLists.get(pubkey);
|
||||
|
||||
// Check if stale (>24 hours)
|
||||
if (cached && Date.now() - cached.updatedAt < CACHE_TTL) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Stale or not found
|
||||
if (cached) {
|
||||
console.debug(
|
||||
`[RelayListCache] Cached relay list for ${pubkey.slice(0, 8)} is stale (${Math.floor((Date.now() - cached.updatedAt) / 1000 / 60)}min old)`,
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[RelayListCache] Error reading cache for ${pubkey.slice(0, 8)}:`,
|
||||
error,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store relay list event in cache
|
||||
*/
|
||||
async set(event: NostrEvent): Promise<void> {
|
||||
try {
|
||||
if (event.kind !== 10002) {
|
||||
console.warn(
|
||||
`[RelayListCache] Attempted to cache non-10002 event (kind ${event.kind})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse relays from event
|
||||
const readRelays = getInboxes(event);
|
||||
const writeRelays = getOutboxes(event);
|
||||
|
||||
// Normalize URLs and filter invalid ones
|
||||
const normalizedRead = readRelays
|
||||
.map((url) => {
|
||||
try {
|
||||
return normalizeRelayURL(url);
|
||||
} catch {
|
||||
console.warn(`[RelayListCache] Invalid read relay URL: ${url}`);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((url): url is string => url !== null);
|
||||
|
||||
const normalizedWrite = writeRelays
|
||||
.map((url) => {
|
||||
try {
|
||||
return normalizeRelayURL(url);
|
||||
} catch {
|
||||
console.warn(`[RelayListCache] Invalid write relay URL: ${url}`);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((url): url is string => url !== null);
|
||||
|
||||
// Store in Dexie and memory cache
|
||||
const cachedEntry: CachedRelayList = {
|
||||
pubkey: event.pubkey,
|
||||
event,
|
||||
read: normalizedRead,
|
||||
write: normalizedWrite,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
await db.relayLists.put(cachedEntry);
|
||||
|
||||
// Also populate memory cache
|
||||
this.memoryCache.set(event.pubkey, cachedEntry);
|
||||
this.cacheOrder.push(event.pubkey);
|
||||
this.evictOldest();
|
||||
|
||||
console.debug(
|
||||
`[RelayListCache] Cached relay list for ${event.pubkey.slice(0, 8)} (${normalizedWrite.length} write, ${normalizedRead.length} read)`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[RelayListCache] Error caching relay list for ${event.pubkey.slice(0, 8)}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update LRU order for a pubkey
|
||||
*/
|
||||
private updateLRU(pubkey: string): void {
|
||||
const index = this.cacheOrder.indexOf(pubkey);
|
||||
if (index > -1) {
|
||||
this.cacheOrder.splice(index, 1);
|
||||
}
|
||||
this.cacheOrder.push(pubkey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict oldest entries from memory cache if over limit
|
||||
*/
|
||||
private evictOldest(): void {
|
||||
while (this.cacheOrder.length > MAX_MEMORY_CACHE) {
|
||||
const oldest = this.cacheOrder.shift();
|
||||
if (oldest) {
|
||||
this.memoryCache.delete(oldest);
|
||||
}
|
||||
}
|
||||
console.warn(
|
||||
"[RelayListCache] unsubscribe is deprecated - managed by ReplaceableEventCache",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,153 +43,66 @@ class RelayListCache {
|
||||
* Returns null if not in memory cache
|
||||
*/
|
||||
getOutboxRelaysSync(pubkey: string): string[] | null {
|
||||
const memCached = this.memoryCache.get(pubkey);
|
||||
if (memCached && Date.now() - memCached.updatedAt < CACHE_TTL) {
|
||||
this.updateLRU(pubkey);
|
||||
return memCached.write;
|
||||
}
|
||||
return null;
|
||||
const event = replaceableEventCache.getSync(pubkey, RELAY_LIST_KIND);
|
||||
if (!event) return null;
|
||||
|
||||
// Parse and normalize on-demand (applesauce caches this)
|
||||
const writeRelays = getOutboxes(event);
|
||||
return this.normalizeRelays(writeRelays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get outbox (write) relays for a pubkey from cache
|
||||
*/
|
||||
async getOutboxRelays(pubkey: string): Promise<string[] | null> {
|
||||
// Check memory cache first (< 1ms)
|
||||
const memCached = this.memoryCache.get(pubkey);
|
||||
if (memCached && Date.now() - memCached.updatedAt < CACHE_TTL) {
|
||||
this.updateLRU(pubkey);
|
||||
return memCached.write;
|
||||
}
|
||||
const event = await replaceableEventCache.getEvent(pubkey, RELAY_LIST_KIND);
|
||||
if (!event) return null;
|
||||
|
||||
// Then check Dexie (5-10ms)
|
||||
const cached = await this.get(pubkey);
|
||||
if (cached) {
|
||||
// Populate memory cache for next time
|
||||
this.memoryCache.set(pubkey, cached);
|
||||
this.cacheOrder.push(pubkey);
|
||||
this.evictOldest();
|
||||
return cached.write;
|
||||
}
|
||||
|
||||
return null;
|
||||
// Parse and normalize on-demand (applesauce caches this)
|
||||
const writeRelays = getOutboxes(event);
|
||||
return this.normalizeRelays(writeRelays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inbox (read) relays for a pubkey from cache
|
||||
*/
|
||||
async getInboxRelays(pubkey: string): Promise<string[] | null> {
|
||||
// Check memory cache first (< 1ms)
|
||||
const memCached = this.memoryCache.get(pubkey);
|
||||
if (memCached && Date.now() - memCached.updatedAt < CACHE_TTL) {
|
||||
this.updateLRU(pubkey);
|
||||
return memCached.read;
|
||||
}
|
||||
const event = await replaceableEventCache.getEvent(pubkey, RELAY_LIST_KIND);
|
||||
if (!event) return null;
|
||||
|
||||
// Then check Dexie (5-10ms)
|
||||
const cached = await this.get(pubkey);
|
||||
if (cached) {
|
||||
// Populate memory cache for next time
|
||||
this.memoryCache.set(pubkey, cached);
|
||||
this.cacheOrder.push(pubkey);
|
||||
this.evictOldest();
|
||||
return cached.read;
|
||||
}
|
||||
// Parse and normalize on-demand (applesauce caches this)
|
||||
const readRelays = getInboxes(event);
|
||||
return this.normalizeRelays(readRelays);
|
||||
}
|
||||
|
||||
return null;
|
||||
/**
|
||||
* Normalize relay URLs and filter invalid ones
|
||||
*/
|
||||
private normalizeRelays(relays: string[]): string[] {
|
||||
return relays
|
||||
.map((url) => {
|
||||
try {
|
||||
return normalizeRelayURL(url);
|
||||
} catch {
|
||||
console.warn(`[RelayListCache] Invalid relay URL: ${url}`);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((url): url is string => url !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have a valid cache entry for a pubkey
|
||||
*/
|
||||
async has(pubkey: string): Promise<boolean> {
|
||||
const cached = await this.get(pubkey);
|
||||
return cached !== undefined;
|
||||
return replaceableEventCache.has(pubkey, RELAY_LIST_KIND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate (delete) cache entry for a pubkey
|
||||
*/
|
||||
async invalidate(pubkey: string): Promise<void> {
|
||||
try {
|
||||
await db.relayLists.delete(pubkey);
|
||||
// Also remove from memory cache
|
||||
this.memoryCache.delete(pubkey);
|
||||
const index = this.cacheOrder.indexOf(pubkey);
|
||||
if (index > -1) {
|
||||
this.cacheOrder.splice(index, 1);
|
||||
}
|
||||
console.debug(
|
||||
`[RelayListCache] Invalidated cache for ${pubkey.slice(0, 8)}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[RelayListCache] Error invalidating cache for ${pubkey.slice(0, 8)}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached relay lists
|
||||
*/
|
||||
async clear(): Promise<void> {
|
||||
try {
|
||||
await db.relayLists.clear();
|
||||
// Also clear memory cache
|
||||
this.memoryCache.clear();
|
||||
this.cacheOrder = [];
|
||||
console.log("[RelayListCache] Cleared all cached relay lists");
|
||||
} catch (error) {
|
||||
console.error("[RelayListCache] Error clearing cache:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
async getStats(): Promise<{
|
||||
count: number;
|
||||
oldestEntry: number | null;
|
||||
newestEntry: number | null;
|
||||
memoryCacheSize: number;
|
||||
memoryCacheLimit: number;
|
||||
}> {
|
||||
try {
|
||||
const count = await db.relayLists.count();
|
||||
const all = await db.relayLists.toArray();
|
||||
|
||||
if (all.length === 0) {
|
||||
return {
|
||||
count: 0,
|
||||
oldestEntry: null,
|
||||
newestEntry: null,
|
||||
memoryCacheSize: this.memoryCache.size,
|
||||
memoryCacheLimit: MAX_MEMORY_CACHE,
|
||||
};
|
||||
}
|
||||
|
||||
const timestamps = all.map((entry) => entry.updatedAt);
|
||||
const oldest = Math.min(...timestamps);
|
||||
const newest = Math.max(...timestamps);
|
||||
|
||||
return {
|
||||
count,
|
||||
oldestEntry: oldest,
|
||||
newestEntry: newest,
|
||||
memoryCacheSize: this.memoryCache.size,
|
||||
memoryCacheLimit: MAX_MEMORY_CACHE,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[RelayListCache] Error getting stats:", error);
|
||||
return {
|
||||
count: 0,
|
||||
oldestEntry: null,
|
||||
newestEntry: null,
|
||||
memoryCacheSize: this.memoryCache.size,
|
||||
memoryCacheLimit: MAX_MEMORY_CACHE,
|
||||
};
|
||||
}
|
||||
return replaceableEventCache.invalidate(pubkey, RELAY_LIST_KIND);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -123,8 +123,7 @@ async function getOutboxRelaysForPubkey(
|
||||
return [];
|
||||
}
|
||||
|
||||
// Cache the event for next time
|
||||
relayListCache.set(event);
|
||||
// Event will be auto-cached by ReplaceableEventCache via EventStore subscription
|
||||
console.debug(
|
||||
`[RelaySelection] Cache miss for ${pubkey.slice(0, 8)}, loaded from EventStore`,
|
||||
);
|
||||
@@ -221,8 +220,7 @@ async function getInboxRelaysForPubkey(
|
||||
return [];
|
||||
}
|
||||
|
||||
// Cache the event for next time
|
||||
relayListCache.set(event);
|
||||
// Event will be auto-cached by ReplaceableEventCache via EventStore subscription
|
||||
console.debug(
|
||||
`[RelaySelection] Cache miss for ${pubkey.slice(0, 8)}, loaded from EventStore`,
|
||||
);
|
||||
|
||||
431
src/services/replaceable-event-cache.ts
Normal file
431
src/services/replaceable-event-cache.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
/**
|
||||
* Generic Replaceable Event Cache Service
|
||||
*
|
||||
* Caches configured replaceable/parameterized events in Dexie for fast access.
|
||||
* Stores raw events - parsing happens on-demand using applesauce helpers.
|
||||
*
|
||||
* Supports:
|
||||
* - Normal replaceable events (10000-19999) - one per pubkey+kind
|
||||
* - Parameterized replaceable events (30000-39999) - multiple per pubkey+kind (by d-tag)
|
||||
* - Contact lists (kind 3) - special case, treated as replaceable
|
||||
*
|
||||
* Auto-caches events from EventStore when subscribed.
|
||||
*/
|
||||
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
import { getTagValue } from "applesauce-core/helpers/event";
|
||||
import db, { CachedReplaceableEvent } from "./db";
|
||||
import type { IEventStore } from "applesauce-core/event-store";
|
||||
import type { Subscription } from "rxjs";
|
||||
|
||||
const DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
||||
const MAX_MEMORY_CACHE = 200; // LRU cache size
|
||||
|
||||
/**
|
||||
* Kinds to cache (add more as needed)
|
||||
* - 3: Contact list (kind:3, NIP-02)
|
||||
* - 10002: Relay list (kind:10002, NIP-65)
|
||||
* - 10063: Blossom server list (kind:10063, BUD-03)
|
||||
* - 10030: User emoji list (kind:10030, NIP-30)
|
||||
*/
|
||||
export const CACHED_KINDS = [3, 10002, 10063, 10030];
|
||||
|
||||
/**
|
||||
* Check if a kind is parameterized replaceable (30000-39999)
|
||||
*/
|
||||
function isParameterizedReplaceable(kind: number): boolean {
|
||||
return kind >= 30000 && kind <= 39999;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build cache key for memory cache: "pubkey:kind:d"
|
||||
*/
|
||||
function buildCacheKey(pubkey: string, kind: number, d: string = ""): string {
|
||||
return `${pubkey}:${kind}:${d}`;
|
||||
}
|
||||
|
||||
class ReplaceableEventCache {
|
||||
private eventStoreSubscription: Subscription | null = null;
|
||||
private memoryCache = new Map<string, CachedReplaceableEvent>();
|
||||
private cacheOrder: string[] = [];
|
||||
private ttl: number = DEFAULT_TTL;
|
||||
|
||||
/**
|
||||
* Subscribe to EventStore to auto-cache configured kinds
|
||||
*/
|
||||
subscribeToEventStore(eventStore: IEventStore): void {
|
||||
if (this.eventStoreSubscription) {
|
||||
console.warn("[ReplaceableEventCache] Already subscribed to EventStore");
|
||||
return;
|
||||
}
|
||||
|
||||
// Subscribe to all configured kinds
|
||||
this.eventStoreSubscription = eventStore
|
||||
.filters({ kinds: CACHED_KINDS })
|
||||
.subscribe((event: NostrEvent) => {
|
||||
// Cache each event as it arrives
|
||||
this.set(event);
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[ReplaceableEventCache] Subscribed to EventStore for kinds: ${CACHED_KINDS.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from EventStore
|
||||
*/
|
||||
unsubscribe(): void {
|
||||
if (this.eventStoreSubscription) {
|
||||
this.eventStoreSubscription.unsubscribe();
|
||||
this.eventStoreSubscription = null;
|
||||
console.log("[ReplaceableEventCache] Unsubscribed from EventStore");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached event for a pubkey+kind (and optional d-tag)
|
||||
* Returns undefined if not cached or stale
|
||||
*/
|
||||
async get(
|
||||
pubkey: string,
|
||||
kind: number,
|
||||
d: string = "",
|
||||
): Promise<CachedReplaceableEvent | undefined> {
|
||||
try {
|
||||
const cached = await db.replaceableEvents.get([pubkey, kind, d]);
|
||||
|
||||
// Check if stale
|
||||
if (cached && Date.now() - cached.updatedAt < this.ttl) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Stale or not found
|
||||
if (cached) {
|
||||
const age = Math.floor((Date.now() - cached.updatedAt) / 1000 / 60);
|
||||
console.debug(
|
||||
`[ReplaceableEventCache] kind:${kind} for ${pubkey.slice(0, 8)}${d ? `/${d}` : ""} is stale (${age}min old)`,
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[ReplaceableEventCache] Error reading kind:${kind} for ${pubkey.slice(0, 8)}:`,
|
||||
error,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store replaceable event in cache
|
||||
*/
|
||||
async set(event: NostrEvent): Promise<void> {
|
||||
try {
|
||||
if (!CACHED_KINDS.includes(event.kind)) {
|
||||
console.warn(
|
||||
`[ReplaceableEventCache] Attempted to cache unconfigured kind ${event.kind}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract d-tag for parameterized replaceable events
|
||||
const d = isParameterizedReplaceable(event.kind)
|
||||
? getTagValue(event, "d") || ""
|
||||
: "";
|
||||
|
||||
// Store in Dexie and memory cache
|
||||
const cachedEntry: CachedReplaceableEvent = {
|
||||
pubkey: event.pubkey,
|
||||
kind: event.kind,
|
||||
d,
|
||||
event,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
await db.replaceableEvents.put(cachedEntry);
|
||||
|
||||
// Also populate memory cache
|
||||
const cacheKey = buildCacheKey(event.pubkey, event.kind, d);
|
||||
this.memoryCache.set(cacheKey, cachedEntry);
|
||||
this.cacheOrder.push(cacheKey);
|
||||
this.evictOldest();
|
||||
|
||||
console.debug(
|
||||
`[ReplaceableEventCache] Cached kind:${event.kind} for ${event.pubkey.slice(0, 8)}${d ? `/${d}` : ""}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[ReplaceableEventCache] Error caching kind:${event.kind} for ${event.pubkey.slice(0, 8)}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update LRU order for a cache key
|
||||
*/
|
||||
private updateLRU(cacheKey: string): void {
|
||||
const index = this.cacheOrder.indexOf(cacheKey);
|
||||
if (index > -1) {
|
||||
this.cacheOrder.splice(index, 1);
|
||||
}
|
||||
this.cacheOrder.push(cacheKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict oldest entries from memory cache if over limit
|
||||
*/
|
||||
private evictOldest(): void {
|
||||
while (this.cacheOrder.length > MAX_MEMORY_CACHE) {
|
||||
const oldest = this.cacheOrder.shift();
|
||||
if (oldest) {
|
||||
this.memoryCache.delete(oldest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get event from memory cache only (synchronous, fast)
|
||||
* Returns null if not in memory cache or stale
|
||||
*/
|
||||
getSync(pubkey: string, kind: number, d: string = ""): NostrEvent | null {
|
||||
const cacheKey = buildCacheKey(pubkey, kind, d);
|
||||
const memCached = this.memoryCache.get(cacheKey);
|
||||
|
||||
if (memCached && Date.now() - memCached.updatedAt < this.ttl) {
|
||||
this.updateLRU(cacheKey);
|
||||
return memCached.event;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get event for a pubkey+kind from cache (async, checks Dexie)
|
||||
*/
|
||||
async getEvent(
|
||||
pubkey: string,
|
||||
kind: number,
|
||||
d: string = "",
|
||||
): Promise<NostrEvent | null> {
|
||||
// Check memory cache first (< 1ms)
|
||||
const cacheKey = buildCacheKey(pubkey, kind, d);
|
||||
const memCached = this.memoryCache.get(cacheKey);
|
||||
|
||||
if (memCached && Date.now() - memCached.updatedAt < this.ttl) {
|
||||
this.updateLRU(cacheKey);
|
||||
return memCached.event;
|
||||
}
|
||||
|
||||
// Then check Dexie (5-10ms)
|
||||
const cached = await this.get(pubkey, kind, d);
|
||||
if (cached) {
|
||||
// Populate memory cache for next time
|
||||
this.memoryCache.set(cacheKey, cached);
|
||||
this.cacheOrder.push(cacheKey);
|
||||
this.evictOldest();
|
||||
return cached.event;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all events for a pubkey+kind (for parameterized replaceables)
|
||||
* Returns array of events, useful for kinds like 30000-39999 with multiple d-tags
|
||||
*/
|
||||
async getAllForKind(pubkey: string, kind: number): Promise<NostrEvent[]> {
|
||||
try {
|
||||
const cached = await db.replaceableEvents
|
||||
.where("[pubkey+kind]")
|
||||
.equals([pubkey, kind])
|
||||
.toArray();
|
||||
|
||||
// Filter out stale entries
|
||||
const fresh = cached.filter(
|
||||
(entry) => Date.now() - entry.updatedAt < this.ttl,
|
||||
);
|
||||
|
||||
return fresh.map((entry) => entry.event);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[ReplaceableEventCache] Error reading all kind:${kind} for ${pubkey.slice(0, 8)}:`,
|
||||
error,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have a valid cache entry
|
||||
*/
|
||||
async has(pubkey: string, kind: number, d: string = ""): Promise<boolean> {
|
||||
const cached = await this.get(pubkey, kind, d);
|
||||
return cached !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate (delete) cache entry
|
||||
*/
|
||||
async invalidate(
|
||||
pubkey: string,
|
||||
kind: number,
|
||||
d: string = "",
|
||||
): Promise<void> {
|
||||
try {
|
||||
await db.replaceableEvents.delete([pubkey, kind, d]);
|
||||
|
||||
// Also remove from memory cache
|
||||
const cacheKey = buildCacheKey(pubkey, kind, d);
|
||||
this.memoryCache.delete(cacheKey);
|
||||
const index = this.cacheOrder.indexOf(cacheKey);
|
||||
if (index > -1) {
|
||||
this.cacheOrder.splice(index, 1);
|
||||
}
|
||||
|
||||
console.debug(
|
||||
`[ReplaceableEventCache] Invalidated kind:${kind} for ${pubkey.slice(0, 8)}${d ? `/${d}` : ""}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[ReplaceableEventCache] Error invalidating kind:${kind} for ${pubkey.slice(0, 8)}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate all entries for a pubkey+kind (useful for parameterized replaceables)
|
||||
*/
|
||||
async invalidateKind(pubkey: string, kind: number): Promise<void> {
|
||||
try {
|
||||
const count = await db.replaceableEvents
|
||||
.where("[pubkey+kind]")
|
||||
.equals([pubkey, kind])
|
||||
.delete();
|
||||
|
||||
// Also remove from memory cache
|
||||
const keysToDelete: string[] = [];
|
||||
for (const key of this.memoryCache.keys()) {
|
||||
if (key.startsWith(`${pubkey}:${kind}:`)) {
|
||||
keysToDelete.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of keysToDelete) {
|
||||
this.memoryCache.delete(key);
|
||||
const index = this.cacheOrder.indexOf(key);
|
||||
if (index > -1) {
|
||||
this.cacheOrder.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
console.debug(
|
||||
`[ReplaceableEventCache] Invalidated ${count} kind:${kind} entries for ${pubkey.slice(0, 8)}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[ReplaceableEventCache] Error invalidating kind:${kind} for ${pubkey.slice(0, 8)}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached events
|
||||
*/
|
||||
async clear(): Promise<void> {
|
||||
try {
|
||||
await db.replaceableEvents.clear();
|
||||
this.memoryCache.clear();
|
||||
this.cacheOrder = [];
|
||||
console.log("[ReplaceableEventCache] Cleared all cached events");
|
||||
} catch (error) {
|
||||
console.error("[ReplaceableEventCache] Error clearing cache:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
async getStats(): Promise<{
|
||||
count: number;
|
||||
byKind: Record<number, number>;
|
||||
oldestEntry: number | null;
|
||||
newestEntry: number | null;
|
||||
memoryCacheSize: number;
|
||||
memoryCacheLimit: number;
|
||||
}> {
|
||||
try {
|
||||
const count = await db.replaceableEvents.count();
|
||||
const all = await db.replaceableEvents.toArray();
|
||||
|
||||
const byKind: Record<number, number> = {};
|
||||
for (const entry of all) {
|
||||
byKind[entry.kind] = (byKind[entry.kind] || 0) + 1;
|
||||
}
|
||||
|
||||
if (all.length === 0) {
|
||||
return {
|
||||
count: 0,
|
||||
byKind: {},
|
||||
oldestEntry: null,
|
||||
newestEntry: null,
|
||||
memoryCacheSize: this.memoryCache.size,
|
||||
memoryCacheLimit: MAX_MEMORY_CACHE,
|
||||
};
|
||||
}
|
||||
|
||||
const timestamps = all.map((entry) => entry.updatedAt);
|
||||
const oldest = Math.min(...timestamps);
|
||||
const newest = Math.max(...timestamps);
|
||||
|
||||
return {
|
||||
count,
|
||||
byKind,
|
||||
oldestEntry: oldest,
|
||||
newestEntry: newest,
|
||||
memoryCacheSize: this.memoryCache.size,
|
||||
memoryCacheLimit: MAX_MEMORY_CACHE,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[ReplaceableEventCache] Error getting stats:", error);
|
||||
return {
|
||||
count: 0,
|
||||
byKind: {},
|
||||
oldestEntry: null,
|
||||
newestEntry: null,
|
||||
memoryCacheSize: this.memoryCache.size,
|
||||
memoryCacheLimit: MAX_MEMORY_CACHE,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up stale entries older than TTL
|
||||
*/
|
||||
async cleanStale(): Promise<number> {
|
||||
try {
|
||||
const cutoff = Date.now() - this.ttl;
|
||||
const count = await db.replaceableEvents
|
||||
.where("updatedAt")
|
||||
.below(cutoff)
|
||||
.delete();
|
||||
|
||||
console.log(
|
||||
`[ReplaceableEventCache] Cleaned ${count} stale entries older than ${Math.floor(this.ttl / 1000 / 60 / 60)}h`,
|
||||
);
|
||||
|
||||
return count;
|
||||
} catch (error) {
|
||||
console.error("[ReplaceableEventCache] Error cleaning stale:", error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const replaceableEventCache = new ReplaceableEventCache();
|
||||
export default replaceableEventCache;
|
||||
Reference in New Issue
Block a user