From 86589b3306821dfe84e029dd7db5ebe687bd805e Mon Sep 17 00:00:00 2001 From: Silberengel Date: Thu, 30 Oct 2025 00:00:29 +0100 Subject: [PATCH] publication caching --- .../PublicationIndex/PublicationIndex.tsx | 119 +++++++++++++----- src/services/indexed-db.service.ts | 60 ++++++++- 2 files changed, 147 insertions(+), 32 deletions(-) diff --git a/src/components/Note/PublicationIndex/PublicationIndex.tsx b/src/components/Note/PublicationIndex/PublicationIndex.tsx index 54a71a9..dc706a8 100644 --- a/src/components/Note/PublicationIndex/PublicationIndex.tsx +++ b/src/components/Note/PublicationIndex/PublicationIndex.tsx @@ -8,6 +8,7 @@ import client from '@/services/client.service' import logger from '@/lib/logger' import { Button } from '@/components/ui/button' import { MoreVertical } from 'lucide-react' +import indexedDb from '@/services/indexed-db.service' interface PublicationReference { coordinate: string @@ -208,10 +209,17 @@ export default function PublicationIndex({ useEffect(() => { setVisitedIndices(prev => new Set([...prev, currentCoordinate])) - }, [currentCoordinate]) + + // Cache the current publication index event using its actual event ID + indexedDb.putPublicationEvent(event).catch(err => { + logger.error('[PublicationIndex] Error caching publication event:', err) + }) + }, [currentCoordinate, event]) // Fetch referenced events useEffect(() => { + let isMounted = true + const fetchReferences = async () => { setIsLoading(true) const fetchedRefs: PublicationReference[] = [] @@ -219,41 +227,78 @@ export default function PublicationIndex({ // Capture current visitedIndices at the start of the fetch const currentVisited = visitedIndices - for (const ref of referencesData) { - // Skip if this is a 30040 event we've already visited (prevent circular references) - if (ref.kind === ExtendedKind.PUBLICATION) { - if (currentVisited.has(ref.coordinate)) { - logger.debug('[PublicationIndex] Skipping visited 30040 index:', ref.coordinate) - fetchedRefs.push({ ...ref, event: undefined }) - continue - } + // Add a timeout to prevent infinite loading on mobile + const timeout = setTimeout(() => { + if (isMounted) { + logger.warn('[PublicationIndex] Fetch timeout reached, setting loaded state') + setIsLoading(false) } - - try { - // Generate bech32 ID from the 'a' tag - const aTag = ['a', ref.coordinate, ref.relay || '', ref.eventId || ''] - const bech32Id = generateBech32IdFromATag(aTag) + }, 30000) // 30 second timeout + + try { + for (const ref of referencesData) { + if (!isMounted) break - if (bech32Id) { - const fetchedEvent = await client.fetchEvent(bech32Id) - if (fetchedEvent) { - fetchedRefs.push({ ...ref, event: fetchedEvent }) - } else { - logger.warn('[PublicationIndex] Could not fetch event for:', ref.coordinate) + // Skip if this is a 30040 event we've already visited (prevent circular references) + if (ref.kind === ExtendedKind.PUBLICATION) { + if (currentVisited.has(ref.coordinate)) { + logger.debug('[PublicationIndex] Skipping visited 30040 index:', ref.coordinate) + fetchedRefs.push({ ...ref, event: undefined }) + continue + } + } + + try { + // Generate bech32 ID from the 'a' tag + const aTag = ['a', ref.coordinate, ref.relay || '', ref.eventId || ''] + const bech32Id = generateBech32IdFromATag(aTag) + + if (bech32Id) { + // First, check if we have this event by its eventId in the ref + let fetchedEvent: Event | undefined = undefined + + if (ref.eventId) { + // Try to get by event ID first + fetchedEvent = await indexedDb.getPublicationEvent(ref.eventId) + } + + // If not found by event ID, try to fetch from relay + if (!fetchedEvent) { + fetchedEvent = await client.fetchEvent(bech32Id) + // Save to cache using the fetched event's ID as the key + if (fetchedEvent) { + await indexedDb.putPublicationEvent(fetchedEvent) + logger.debug('[PublicationIndex] Cached event with ID:', fetchedEvent.id) + } + } else { + logger.debug('[PublicationIndex] Loaded from cache by event ID:', ref.eventId) + } + + if (fetchedEvent && isMounted) { + fetchedRefs.push({ ...ref, event: fetchedEvent }) + } else if (isMounted) { + logger.warn('[PublicationIndex] Could not fetch event for:', ref.coordinate) + fetchedRefs.push({ ...ref, event: undefined }) + } + } else if (isMounted) { + logger.warn('[PublicationIndex] Could not generate bech32 ID for:', ref.coordinate) + fetchedRefs.push({ ...ref, event: undefined }) + } + } catch (error) { + logger.error('[PublicationIndex] Error fetching reference:', error) + if (isMounted) { fetchedRefs.push({ ...ref, event: undefined }) } - } else { - logger.warn('[PublicationIndex] Could not generate bech32 ID for:', ref.coordinate) - fetchedRefs.push({ ...ref, event: undefined }) } - } catch (error) { - logger.error('[PublicationIndex] Error fetching reference:', error) - fetchedRefs.push({ ...ref, event: undefined }) } + + if (isMounted) { + setReferences(fetchedRefs) + setIsLoading(false) + } + } finally { + clearTimeout(timeout) } - - setReferences(fetchedRefs) - setIsLoading(false) } if (referencesData.length > 0) { @@ -261,6 +306,10 @@ export default function PublicationIndex({ } else { setIsLoading(false) } + + return () => { + isMounted = false + } }, [referencesData, visitedIndices]) // Now include visitedIndices but capture it inside return ( @@ -326,7 +375,17 @@ export default function PublicationIndex({ {/* Content - render referenced events */} {isLoading ? ( -
Loading publication content...
+
+
Loading publication content...
+
If this takes too long, the content may not be available.
+
+ ) : references.length === 0 ? ( +
+
No content loaded
+
+ Unable to load publication content. The referenced events may not be available on the current relays. +
+
) : (
{references.map((ref, index) => { diff --git a/src/services/indexed-db.service.ts b/src/services/indexed-db.service.ts index 4c75b91..8173663 100644 --- a/src/services/indexed-db.service.ts +++ b/src/services/indexed-db.service.ts @@ -26,7 +26,8 @@ const StoreNames = { RELAY_SETS: 'relaySets', FOLLOWING_FAVORITE_RELAYS: 'followingFavoriteRelays', RELAY_INFOS: 'relayInfos', - RELAY_INFO_EVENTS: 'relayInfoEvents' // deprecated + RELAY_INFO_EVENTS: 'relayInfoEvents', // deprecated + PUBLICATION_EVENTS: 'publicationEvents' } class IndexedDbService { @@ -45,7 +46,7 @@ class IndexedDbService { init(): Promise { if (!this.initPromise) { this.initPromise = new Promise((resolve, reject) => { - const request = window.indexedDB.open('jumble', 11) + const request = window.indexedDB.open('jumble', 12) request.onerror = (event) => { reject(event) @@ -109,6 +110,9 @@ class IndexedDbService { if (db.objectStoreNames.contains(StoreNames.RELAY_INFO_EVENTS)) { db.deleteObjectStore(StoreNames.RELAY_INFO_EVENTS) } + if (!db.objectStoreNames.contains(StoreNames.PUBLICATION_EVENTS)) { + db.createObjectStore(StoreNames.PUBLICATION_EVENTS, { keyPath: 'key' }) + } this.db = db } }) @@ -477,11 +481,63 @@ class IndexedDbService { return StoreNames.USER_EMOJI_LIST_EVENTS case kinds.Emojisets: return StoreNames.EMOJI_SET_EVENTS + case ExtendedKind.PUBLICATION: + case ExtendedKind.PUBLICATION_CONTENT: + case ExtendedKind.WIKI_ARTICLE: + case kinds.LongFormArticle: + return StoreNames.PUBLICATION_EVENTS default: return undefined } } + async putPublicationEvent(event: Event): Promise { + await this.initPromise + return new Promise((resolve, reject) => { + if (!this.db) { + return reject('database not initialized') + } + const transaction = this.db.transaction(StoreNames.PUBLICATION_EVENTS, 'readwrite') + const store = transaction.objectStore(StoreNames.PUBLICATION_EVENTS) + + const key = event.id + // Always update, as these are not replaceable events + const putRequest = store.put(this.formatValue(key, event)) + putRequest.onsuccess = () => { + transaction.commit() + resolve(event) + } + + putRequest.onerror = (event) => { + transaction.commit() + reject(event) + } + }) + } + + async getPublicationEvent(eventId: string): Promise { + await this.initPromise + return new Promise((resolve, reject) => { + if (!this.db) { + return reject('database not initialized') + } + const transaction = this.db.transaction(StoreNames.PUBLICATION_EVENTS, 'readonly') + const store = transaction.objectStore(StoreNames.PUBLICATION_EVENTS) + const request = store.get(eventId) + + request.onsuccess = () => { + transaction.commit() + const cachedValue = (request.result as TValue)?.value + resolve(cachedValue || undefined) + } + + request.onerror = (event) => { + transaction.commit() + reject(event) + } + }) + } + private formatValue(key: string, value: T): TValue { return { key,