feat: persist highlight metadata and offline events to localStorage

- Add localStorage persistence for highlightMetadataCache Map
- Add localStorage persistence for offlineCreatedEvents Set
- Load both caches from localStorage on module initialization
- Save to localStorage whenever caches are updated
- Update metadata cache during sync operations (isSyncing changes)
- Ensures airplane icon displays correctly after page reloads
- Gracefully handles localStorage errors and corrupted data
This commit is contained in:
Gigi
2025-10-31 00:17:13 +01:00
parent 5013ccc552
commit 19dc2f70f2
2 changed files with 101 additions and 17 deletions

View File

@@ -21,38 +21,63 @@ const {
getHighlightAttributions getHighlightAttributions
} = Helpers } = Helpers
const METADATA_CACHE_KEY = 'highlightMetadataCache'
type HighlightMetadata = {
publishedRelays?: string[]
isLocalOnly?: boolean
isSyncing?: boolean
}
/**
* Load highlight metadata from localStorage
*/
function loadHighlightMetadataFromStorage(): Map<string, HighlightMetadata> {
try {
const raw = localStorage.getItem(METADATA_CACHE_KEY)
if (!raw) return new Map()
const parsed = JSON.parse(raw) as Record<string, HighlightMetadata>
return new Map(Object.entries(parsed))
} catch {
// Silently fail on parse errors or if storage is unavailable
return new Map()
}
}
/**
* Save highlight metadata to localStorage
*/
function saveHighlightMetadataToStorage(cache: Map<string, HighlightMetadata>): void {
try {
const record = Object.fromEntries(cache.entries())
localStorage.setItem(METADATA_CACHE_KEY, JSON.stringify(record))
} catch {
// Silently fail if storage is full or unavailable
}
}
/** /**
* Cache for highlight metadata that persists across EventStore serialization * Cache for highlight metadata that persists across EventStore serialization
* Key: event ID, Value: { publishedRelays, isLocalOnly, isSyncing } * Key: event ID, Value: { publishedRelays, isLocalOnly, isSyncing }
*/ */
const highlightMetadataCache = new Map<string, { const highlightMetadataCache = loadHighlightMetadataFromStorage()
publishedRelays?: string[]
isLocalOnly?: boolean
isSyncing?: boolean
}>()
/** /**
* Store highlight metadata for an event ID * Store highlight metadata for an event ID
*/ */
export function setHighlightMetadata( export function setHighlightMetadata(
eventId: string, eventId: string,
metadata: { metadata: HighlightMetadata
publishedRelays?: string[]
isLocalOnly?: boolean
isSyncing?: boolean
}
): void { ): void {
highlightMetadataCache.set(eventId, metadata) highlightMetadataCache.set(eventId, metadata)
saveHighlightMetadataToStorage(highlightMetadataCache)
} }
/** /**
* Get highlight metadata for an event ID * Get highlight metadata for an event ID
*/ */
export function getHighlightMetadata(eventId: string): { export function getHighlightMetadata(eventId: string): HighlightMetadata | undefined {
publishedRelays?: string[]
isLocalOnly?: boolean
isSyncing?: boolean
} | undefined {
return highlightMetadataCache.get(eventId) return highlightMetadataCache.get(eventId)
} }

View File

@@ -3,11 +3,42 @@ import { NostrEvent } from 'nostr-tools'
import { IEventStore } from 'applesauce-core' import { IEventStore } from 'applesauce-core'
import { RELAYS } from '../config/relays' import { RELAYS } from '../config/relays'
import { isLocalRelay } from '../utils/helpers' import { isLocalRelay } from '../utils/helpers'
import { setHighlightMetadata, getHighlightMetadata } from './highlightEventProcessor'
const OFFLINE_EVENTS_KEY = 'offlineCreatedEvents'
let isSyncing = false let isSyncing = false
/**
* Load offline events from localStorage
*/
function loadOfflineEventsFromStorage(): Set<string> {
try {
const raw = localStorage.getItem(OFFLINE_EVENTS_KEY)
if (!raw) return new Set()
const parsed = JSON.parse(raw) as string[]
return new Set(parsed)
} catch {
// Silently fail on parse errors or if storage is unavailable
return new Set()
}
}
/**
* Save offline events to localStorage
*/
function saveOfflineEventsToStorage(events: Set<string>): void {
try {
const array = Array.from(events)
localStorage.setItem(OFFLINE_EVENTS_KEY, JSON.stringify(array))
} catch {
// Silently fail if storage is full or unavailable
}
}
// Track events created during offline period // Track events created during offline period
const offlineCreatedEvents = new Set<string>() const offlineCreatedEvents = loadOfflineEventsFromStorage()
// Track events currently being synced // Track events currently being synced
const syncingEvents = new Set<string>() const syncingEvents = new Set<string>()
@@ -20,6 +51,7 @@ const syncStateListeners: Array<(eventId: string, isSyncing: boolean) => void> =
*/ */
export function markEventAsOfflineCreated(eventId: string): void { export function markEventAsOfflineCreated(eventId: string): void {
offlineCreatedEvents.add(eventId) offlineCreatedEvents.add(eventId)
saveOfflineEventsToStorage(offlineCreatedEvents)
} }
/** /**
@@ -94,6 +126,7 @@ export async function syncLocalEventsToRemote(
if (eventsToSync.length === 0) { if (eventsToSync.length === 0) {
isSyncing = false isSyncing = false
offlineCreatedEvents.clear() offlineCreatedEvents.clear()
saveOfflineEventsToStorage(offlineCreatedEvents)
return return
} }
@@ -102,10 +135,17 @@ export async function syncLocalEventsToRemote(
new Map(eventsToSync.map(e => [e.id, e])).values() new Map(eventsToSync.map(e => [e.id, e])).values()
) )
// Mark all events as syncing // Mark all events as syncing and update metadata
uniqueEvents.forEach(event => { uniqueEvents.forEach(event => {
syncingEvents.add(event.id) syncingEvents.add(event.id)
notifySyncStateChange(event.id, true) notifySyncStateChange(event.id, true)
// Update metadata cache to reflect syncing state
const existingMetadata = getHighlightMetadata(event.id)
setHighlightMetadata(event.id, {
...existingMetadata,
isSyncing: true
})
}) })
// Publish to remote relays // Publish to remote relays
@@ -125,13 +165,32 @@ export async function syncLocalEventsToRemote(
syncingEvents.delete(eventId) syncingEvents.delete(eventId)
offlineCreatedEvents.delete(eventId) offlineCreatedEvents.delete(eventId)
notifySyncStateChange(eventId, false) notifySyncStateChange(eventId, false)
// Update metadata cache: sync complete, no longer local-only
const existingMetadata = getHighlightMetadata(eventId)
setHighlightMetadata(eventId, {
...existingMetadata,
isSyncing: false,
isLocalOnly: false
}) })
})
// Save updated offline events set to localStorage
saveOfflineEventsToStorage(offlineCreatedEvents)
// Clear syncing state for failed events // Clear syncing state for failed events
uniqueEvents.forEach(event => { uniqueEvents.forEach(event => {
if (!successfulIds.includes(event.id)) { if (!successfulIds.includes(event.id)) {
syncingEvents.delete(event.id) syncingEvents.delete(event.id)
notifySyncStateChange(event.id, false) notifySyncStateChange(event.id, false)
// Update metadata cache: sync failed, still local-only
const existingMetadata = getHighlightMetadata(event.id)
setHighlightMetadata(event.id, {
...existingMetadata,
isSyncing: false
// Keep isLocalOnly as true (sync failed)
})
} }
}) })
} catch (error) { } catch (error) {