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