Browse Source

publication caching

imwald
Silberengel 5 months ago
parent
commit
86589b3306
  1. 69
      src/components/Note/PublicationIndex/PublicationIndex.tsx
  2. 60
      src/services/indexed-db.service.ts

69
src/components/Note/PublicationIndex/PublicationIndex.tsx

@ -8,6 +8,7 @@ import client from '@/services/client.service'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { MoreVertical } from 'lucide-react' import { MoreVertical } from 'lucide-react'
import indexedDb from '@/services/indexed-db.service'
interface PublicationReference { interface PublicationReference {
coordinate: string coordinate: string
@ -208,10 +209,17 @@ export default function PublicationIndex({
useEffect(() => { useEffect(() => {
setVisitedIndices(prev => new Set([...prev, currentCoordinate])) 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 // Fetch referenced events
useEffect(() => { useEffect(() => {
let isMounted = true
const fetchReferences = async () => { const fetchReferences = async () => {
setIsLoading(true) setIsLoading(true)
const fetchedRefs: PublicationReference[] = [] const fetchedRefs: PublicationReference[] = []
@ -219,7 +227,18 @@ export default function PublicationIndex({
// Capture current visitedIndices at the start of the fetch // Capture current visitedIndices at the start of the fetch
const currentVisited = visitedIndices const currentVisited = visitedIndices
// 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)
}
}, 30000) // 30 second timeout
try {
for (const ref of referencesData) { for (const ref of referencesData) {
if (!isMounted) break
// Skip if this is a 30040 event we've already visited (prevent circular references) // Skip if this is a 30040 event we've already visited (prevent circular references)
if (ref.kind === ExtendedKind.PUBLICATION) { if (ref.kind === ExtendedKind.PUBLICATION) {
if (currentVisited.has(ref.coordinate)) { if (currentVisited.has(ref.coordinate)) {
@ -235,32 +254,62 @@ export default function PublicationIndex({
const bech32Id = generateBech32IdFromATag(aTag) const bech32Id = generateBech32IdFromATag(aTag)
if (bech32Id) { if (bech32Id) {
const fetchedEvent = await client.fetchEvent(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) { if (fetchedEvent) {
fetchedRefs.push({ ...ref, event: fetchedEvent }) await indexedDb.putPublicationEvent(fetchedEvent)
logger.debug('[PublicationIndex] Cached event with ID:', fetchedEvent.id)
}
} else { } 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) logger.warn('[PublicationIndex] Could not fetch event for:', ref.coordinate)
fetchedRefs.push({ ...ref, event: undefined }) fetchedRefs.push({ ...ref, event: undefined })
} }
} else { } else if (isMounted) {
logger.warn('[PublicationIndex] Could not generate bech32 ID for:', ref.coordinate) logger.warn('[PublicationIndex] Could not generate bech32 ID for:', ref.coordinate)
fetchedRefs.push({ ...ref, event: undefined }) fetchedRefs.push({ ...ref, event: undefined })
} }
} catch (error) { } catch (error) {
logger.error('[PublicationIndex] Error fetching reference:', error) logger.error('[PublicationIndex] Error fetching reference:', error)
if (isMounted) {
fetchedRefs.push({ ...ref, event: undefined }) fetchedRefs.push({ ...ref, event: undefined })
} }
} }
}
if (isMounted) {
setReferences(fetchedRefs) setReferences(fetchedRefs)
setIsLoading(false) setIsLoading(false)
} }
} finally {
clearTimeout(timeout)
}
}
if (referencesData.length > 0) { if (referencesData.length > 0) {
fetchReferences() fetchReferences()
} else { } else {
setIsLoading(false) setIsLoading(false)
} }
return () => {
isMounted = false
}
}, [referencesData, visitedIndices]) // Now include visitedIndices but capture it inside }, [referencesData, visitedIndices]) // Now include visitedIndices but capture it inside
return ( return (
@ -326,7 +375,17 @@ export default function PublicationIndex({
{/* Content - render referenced events */} {/* Content - render referenced events */}
{isLoading ? ( {isLoading ? (
<div className="text-muted-foreground">Loading publication content...</div> <div className="text-muted-foreground">
<div>Loading publication content...</div>
<div className="text-xs mt-2">If this takes too long, the content may not be available.</div>
</div>
) : references.length === 0 ? (
<div className="p-6 border rounded-lg bg-muted/30 text-center">
<div className="text-lg font-semibold mb-2">No content loaded</div>
<div className="text-sm text-muted-foreground">
Unable to load publication content. The referenced events may not be available on the current relays.
</div>
</div>
) : ( ) : (
<div className="space-y-8"> <div className="space-y-8">
{references.map((ref, index) => { {references.map((ref, index) => {

60
src/services/indexed-db.service.ts

@ -26,7 +26,8 @@ const StoreNames = {
RELAY_SETS: 'relaySets', RELAY_SETS: 'relaySets',
FOLLOWING_FAVORITE_RELAYS: 'followingFavoriteRelays', FOLLOWING_FAVORITE_RELAYS: 'followingFavoriteRelays',
RELAY_INFOS: 'relayInfos', RELAY_INFOS: 'relayInfos',
RELAY_INFO_EVENTS: 'relayInfoEvents' // deprecated RELAY_INFO_EVENTS: 'relayInfoEvents', // deprecated
PUBLICATION_EVENTS: 'publicationEvents'
} }
class IndexedDbService { class IndexedDbService {
@ -45,7 +46,7 @@ class IndexedDbService {
init(): Promise<void> { init(): Promise<void> {
if (!this.initPromise) { if (!this.initPromise) {
this.initPromise = new Promise((resolve, reject) => { this.initPromise = new Promise((resolve, reject) => {
const request = window.indexedDB.open('jumble', 11) const request = window.indexedDB.open('jumble', 12)
request.onerror = (event) => { request.onerror = (event) => {
reject(event) reject(event)
@ -109,6 +110,9 @@ class IndexedDbService {
if (db.objectStoreNames.contains(StoreNames.RELAY_INFO_EVENTS)) { if (db.objectStoreNames.contains(StoreNames.RELAY_INFO_EVENTS)) {
db.deleteObjectStore(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 this.db = db
} }
}) })
@ -477,11 +481,63 @@ class IndexedDbService {
return StoreNames.USER_EMOJI_LIST_EVENTS return StoreNames.USER_EMOJI_LIST_EVENTS
case kinds.Emojisets: case kinds.Emojisets:
return StoreNames.EMOJI_SET_EVENTS return StoreNames.EMOJI_SET_EVENTS
case ExtendedKind.PUBLICATION:
case ExtendedKind.PUBLICATION_CONTENT:
case ExtendedKind.WIKI_ARTICLE:
case kinds.LongFormArticle:
return StoreNames.PUBLICATION_EVENTS
default: default:
return undefined return undefined
} }
} }
async putPublicationEvent(event: Event): Promise<Event> {
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<Event | undefined> {
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<Event>)?.value
resolve(cachedValue || undefined)
}
request.onerror = (event) => {
transaction.commit()
reject(event)
}
})
}
private formatValue<T>(key: string, value: T): TValue<T> { private formatValue<T>(key: string, value: T): TValue<T> {
return { return {
key, key,

Loading…
Cancel
Save