graph-cache.service.ts raw

   1  import { GraphResponse } from '@/types/graph'
   2  import { TGraphQueryCapability } from '@/types'
   3  
   4  const DB_NAME = 'smesh-graph-cache'
   5  const DB_VERSION = 1
   6  
   7  // Store names
   8  const STORES = {
   9    FOLLOW_GRAPH: 'followGraphResults',
  10    THREAD: 'threadResults',
  11    RELAY_CAPABILITIES: 'relayCapabilities'
  12  }
  13  
  14  // Cache expiry times (in milliseconds)
  15  const CACHE_EXPIRY = {
  16    FOLLOW_GRAPH: 5 * 60 * 1000, // 5 minutes
  17    THREAD: 10 * 60 * 1000, // 10 minutes
  18    RELAY_CAPABILITY: 60 * 60 * 1000 // 1 hour
  19  }
  20  
  21  interface CachedEntry<T> {
  22    data: T
  23    timestamp: number
  24  }
  25  
  26  class GraphCacheService {
  27    static instance: GraphCacheService
  28    private db: IDBDatabase | null = null
  29    private dbPromise: Promise<IDBDatabase> | null = null
  30  
  31    public static getInstance(): GraphCacheService {
  32      if (!GraphCacheService.instance) {
  33        GraphCacheService.instance = new GraphCacheService()
  34      }
  35      return GraphCacheService.instance
  36    }
  37  
  38    private async getDB(): Promise<IDBDatabase> {
  39      if (this.db) return this.db
  40      if (this.dbPromise) return this.dbPromise
  41  
  42      this.dbPromise = new Promise((resolve, reject) => {
  43        const request = indexedDB.open(DB_NAME, DB_VERSION)
  44  
  45        request.onerror = () => {
  46          console.error('Failed to open graph cache database:', request.error)
  47          reject(request.error)
  48        }
  49  
  50        request.onsuccess = () => {
  51          this.db = request.result
  52          resolve(request.result)
  53        }
  54  
  55        request.onupgradeneeded = (event) => {
  56          const db = (event.target as IDBOpenDBRequest).result
  57  
  58          // Create stores if they don't exist
  59          if (!db.objectStoreNames.contains(STORES.FOLLOW_GRAPH)) {
  60            db.createObjectStore(STORES.FOLLOW_GRAPH)
  61          }
  62          if (!db.objectStoreNames.contains(STORES.THREAD)) {
  63            db.createObjectStore(STORES.THREAD)
  64          }
  65          if (!db.objectStoreNames.contains(STORES.RELAY_CAPABILITIES)) {
  66            db.createObjectStore(STORES.RELAY_CAPABILITIES)
  67          }
  68        }
  69      })
  70  
  71      return this.dbPromise
  72    }
  73  
  74    /**
  75     * Cache a follow graph query result
  76     */
  77    async cacheFollowGraph(
  78      pubkey: string,
  79      depth: number,
  80      result: GraphResponse
  81    ): Promise<void> {
  82      try {
  83        const db = await this.getDB()
  84        const key = `${pubkey}:${depth}`
  85        const entry: CachedEntry<GraphResponse> = {
  86          data: result,
  87          timestamp: Date.now()
  88        }
  89  
  90        return new Promise((resolve, reject) => {
  91          const tx = db.transaction(STORES.FOLLOW_GRAPH, 'readwrite')
  92          const store = tx.objectStore(STORES.FOLLOW_GRAPH)
  93          const request = store.put(entry, key)
  94  
  95          request.onsuccess = () => resolve()
  96          request.onerror = () => reject(request.error)
  97        })
  98      } catch (error) {
  99        console.error('Failed to cache follow graph:', error)
 100      }
 101    }
 102  
 103    /**
 104     * Get cached follow graph result
 105     */
 106    async getCachedFollowGraph(
 107      pubkey: string,
 108      depth: number
 109    ): Promise<GraphResponse | null> {
 110      try {
 111        const db = await this.getDB()
 112        const key = `${pubkey}:${depth}`
 113  
 114        return new Promise((resolve, reject) => {
 115          const tx = db.transaction(STORES.FOLLOW_GRAPH, 'readonly')
 116          const store = tx.objectStore(STORES.FOLLOW_GRAPH)
 117          const request = store.get(key)
 118  
 119          request.onsuccess = () => {
 120            const entry = request.result as CachedEntry<GraphResponse> | undefined
 121            if (!entry) {
 122              resolve(null)
 123              return
 124            }
 125  
 126            // Check if cache is expired
 127            if (Date.now() - entry.timestamp > CACHE_EXPIRY.FOLLOW_GRAPH) {
 128              resolve(null)
 129              return
 130            }
 131  
 132            resolve(entry.data)
 133          }
 134          request.onerror = () => reject(request.error)
 135        })
 136      } catch (error) {
 137        console.error('Failed to get cached follow graph:', error)
 138        return null
 139      }
 140    }
 141  
 142    /**
 143     * Cache a thread query result
 144     */
 145    async cacheThread(eventId: string, result: GraphResponse): Promise<void> {
 146      try {
 147        const db = await this.getDB()
 148        const entry: CachedEntry<GraphResponse> = {
 149          data: result,
 150          timestamp: Date.now()
 151        }
 152  
 153        return new Promise((resolve, reject) => {
 154          const tx = db.transaction(STORES.THREAD, 'readwrite')
 155          const store = tx.objectStore(STORES.THREAD)
 156          const request = store.put(entry, eventId)
 157  
 158          request.onsuccess = () => resolve()
 159          request.onerror = () => reject(request.error)
 160        })
 161      } catch (error) {
 162        console.error('Failed to cache thread:', error)
 163      }
 164    }
 165  
 166    /**
 167     * Get cached thread result
 168     */
 169    async getCachedThread(eventId: string): Promise<GraphResponse | null> {
 170      try {
 171        const db = await this.getDB()
 172  
 173        return new Promise((resolve, reject) => {
 174          const tx = db.transaction(STORES.THREAD, 'readonly')
 175          const store = tx.objectStore(STORES.THREAD)
 176          const request = store.get(eventId)
 177  
 178          request.onsuccess = () => {
 179            const entry = request.result as CachedEntry<GraphResponse> | undefined
 180            if (!entry) {
 181              resolve(null)
 182              return
 183            }
 184  
 185            if (Date.now() - entry.timestamp > CACHE_EXPIRY.THREAD) {
 186              resolve(null)
 187              return
 188            }
 189  
 190            resolve(entry.data)
 191          }
 192          request.onerror = () => reject(request.error)
 193        })
 194      } catch (error) {
 195        console.error('Failed to get cached thread:', error)
 196        return null
 197      }
 198    }
 199  
 200    /**
 201     * Cache relay graph capability
 202     */
 203    async cacheRelayCapability(
 204      url: string,
 205      capability: TGraphQueryCapability | null
 206    ): Promise<void> {
 207      try {
 208        const db = await this.getDB()
 209        const entry: CachedEntry<TGraphQueryCapability | null> = {
 210          data: capability,
 211          timestamp: Date.now()
 212        }
 213  
 214        return new Promise((resolve, reject) => {
 215          const tx = db.transaction(STORES.RELAY_CAPABILITIES, 'readwrite')
 216          const store = tx.objectStore(STORES.RELAY_CAPABILITIES)
 217          const request = store.put(entry, url)
 218  
 219          request.onsuccess = () => resolve()
 220          request.onerror = () => reject(request.error)
 221        })
 222      } catch (error) {
 223        console.error('Failed to cache relay capability:', error)
 224      }
 225    }
 226  
 227    /**
 228     * Get cached relay capability
 229     */
 230    async getCachedRelayCapability(
 231      url: string
 232    ): Promise<TGraphQueryCapability | null | undefined> {
 233      try {
 234        const db = await this.getDB()
 235  
 236        return new Promise((resolve, reject) => {
 237          const tx = db.transaction(STORES.RELAY_CAPABILITIES, 'readonly')
 238          const store = tx.objectStore(STORES.RELAY_CAPABILITIES)
 239          const request = store.get(url)
 240  
 241          request.onsuccess = () => {
 242            const entry = request.result as
 243              | CachedEntry<TGraphQueryCapability | null>
 244              | undefined
 245            if (!entry) {
 246              resolve(undefined) // Not in cache
 247              return
 248            }
 249  
 250            if (Date.now() - entry.timestamp > CACHE_EXPIRY.RELAY_CAPABILITY) {
 251              resolve(undefined) // Expired
 252              return
 253            }
 254  
 255            resolve(entry.data)
 256          }
 257          request.onerror = () => reject(request.error)
 258        })
 259      } catch (error) {
 260        console.error('Failed to get cached relay capability:', error)
 261        return undefined
 262      }
 263    }
 264  
 265    /**
 266     * Invalidate follow graph cache for a pubkey
 267     */
 268    async invalidateFollowGraph(pubkey: string): Promise<void> {
 269      try {
 270        const db = await this.getDB()
 271  
 272        return new Promise((resolve, reject) => {
 273          const tx = db.transaction(STORES.FOLLOW_GRAPH, 'readwrite')
 274          const store = tx.objectStore(STORES.FOLLOW_GRAPH)
 275  
 276          // Delete entries for all depths
 277          for (let depth = 1; depth <= 16; depth++) {
 278            store.delete(`${pubkey}:${depth}`)
 279          }
 280  
 281          tx.oncomplete = () => resolve()
 282          tx.onerror = () => reject(tx.error)
 283        })
 284      } catch (error) {
 285        console.error('Failed to invalidate follow graph cache:', error)
 286      }
 287    }
 288  
 289    /**
 290     * Clear all caches
 291     */
 292    async clearAll(): Promise<void> {
 293      try {
 294        const db = await this.getDB()
 295  
 296        return new Promise((resolve, reject) => {
 297          const tx = db.transaction(
 298            [STORES.FOLLOW_GRAPH, STORES.THREAD, STORES.RELAY_CAPABILITIES],
 299            'readwrite'
 300          )
 301  
 302          tx.objectStore(STORES.FOLLOW_GRAPH).clear()
 303          tx.objectStore(STORES.THREAD).clear()
 304          tx.objectStore(STORES.RELAY_CAPABILITIES).clear()
 305  
 306          tx.oncomplete = () => resolve()
 307          tx.onerror = () => reject(tx.error)
 308        })
 309      } catch (error) {
 310        console.error('Failed to clear graph cache:', error)
 311      }
 312    }
 313  }
 314  
 315  const instance = GraphCacheService.getInstance()
 316  export default instance
 317