From 19dc2f70f29ce09c7a3690f38dcd4fe9b9016de5 Mon Sep 17 00:00:00 2001 From: Gigi Date: Fri, 31 Oct 2025 00:17:13 +0100 Subject: [PATCH] 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 --- src/services/highlightEventProcessor.ts | 55 +++++++++++++++------ src/services/offlineSyncService.ts | 63 ++++++++++++++++++++++++- 2 files changed, 101 insertions(+), 17 deletions(-) diff --git a/src/services/highlightEventProcessor.ts b/src/services/highlightEventProcessor.ts index 7e99b203..f20d780a 100644 --- a/src/services/highlightEventProcessor.ts +++ b/src/services/highlightEventProcessor.ts @@ -21,38 +21,63 @@ const { getHighlightAttributions } = Helpers +const METADATA_CACHE_KEY = 'highlightMetadataCache' + +type HighlightMetadata = { + publishedRelays?: string[] + isLocalOnly?: boolean + isSyncing?: boolean +} + +/** + * Load highlight metadata from localStorage + */ +function loadHighlightMetadataFromStorage(): Map { + try { + const raw = localStorage.getItem(METADATA_CACHE_KEY) + if (!raw) return new Map() + + const parsed = JSON.parse(raw) as Record + 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): 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 * Key: event ID, Value: { publishedRelays, isLocalOnly, isSyncing } */ -const highlightMetadataCache = new Map() +const highlightMetadataCache = loadHighlightMetadataFromStorage() /** * Store highlight metadata for an event ID */ export function setHighlightMetadata( eventId: string, - metadata: { - publishedRelays?: string[] - isLocalOnly?: boolean - isSyncing?: boolean - } + metadata: HighlightMetadata ): void { highlightMetadataCache.set(eventId, metadata) + saveHighlightMetadataToStorage(highlightMetadataCache) } /** * Get highlight metadata for an event ID */ -export function getHighlightMetadata(eventId: string): { - publishedRelays?: string[] - isLocalOnly?: boolean - isSyncing?: boolean -} | undefined { +export function getHighlightMetadata(eventId: string): HighlightMetadata | undefined { return highlightMetadataCache.get(eventId) } diff --git a/src/services/offlineSyncService.ts b/src/services/offlineSyncService.ts index 901ea936..a410a64c 100644 --- a/src/services/offlineSyncService.ts +++ b/src/services/offlineSyncService.ts @@ -3,11 +3,42 @@ import { NostrEvent } from 'nostr-tools' import { IEventStore } from 'applesauce-core' import { RELAYS } from '../config/relays' import { isLocalRelay } from '../utils/helpers' +import { setHighlightMetadata, getHighlightMetadata } from './highlightEventProcessor' + +const OFFLINE_EVENTS_KEY = 'offlineCreatedEvents' let isSyncing = false +/** + * Load offline events from localStorage + */ +function loadOfflineEventsFromStorage(): Set { + 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): 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 -const offlineCreatedEvents = new Set() +const offlineCreatedEvents = loadOfflineEventsFromStorage() // Track events currently being synced const syncingEvents = new Set() @@ -20,6 +51,7 @@ const syncStateListeners: Array<(eventId: string, isSyncing: boolean) => void> = */ export function markEventAsOfflineCreated(eventId: string): void { offlineCreatedEvents.add(eventId) + saveOfflineEventsToStorage(offlineCreatedEvents) } /** @@ -94,6 +126,7 @@ export async function syncLocalEventsToRemote( if (eventsToSync.length === 0) { isSyncing = false offlineCreatedEvents.clear() + saveOfflineEventsToStorage(offlineCreatedEvents) return } @@ -102,10 +135,17 @@ export async function syncLocalEventsToRemote( 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 => { syncingEvents.add(event.id) 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 @@ -125,13 +165,32 @@ export async function syncLocalEventsToRemote( syncingEvents.delete(eventId) offlineCreatedEvents.delete(eventId) 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 uniqueEvents.forEach(event => { if (!successfulIds.includes(event.id)) { syncingEvents.delete(event.id) 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) {