diff --git a/frontend-v2/src/app/event/activist-registry.ts b/frontend-v2/src/app/event/activist-registry.ts index e950a8f2..bc82fa0b 100644 --- a/frontend-v2/src/app/event/activist-registry.ts +++ b/frontend-v2/src/app/event/activist-registry.ts @@ -15,9 +15,7 @@ export type ActivistRecord = { * Reads are synchronous (from memory) for fast autocomplete/filtering. * Writes are async and automatically persist to IndexedDB when storage is configured. * - * @param storage - Optional IndexedDB storage for persistence. - * When provided, enables automatic write-through caching. - * When omitted, registry operates in memory-only mode. + * When storage is not configured, registry operates in memory-only mode. */ export class ActivistRegistry { private activists: ActivistRecord[] @@ -25,11 +23,10 @@ export class ActivistRegistry { private activistsById: Map private storage?: ActivistStorage - constructor(storage?: ActivistStorage) { + constructor() { this.activists = [] this.activistsByName = new Map() this.activistsById = new Map() - this.storage = storage } /** @@ -48,24 +45,18 @@ export class ActivistRegistry { /** * Loads activists from IndexedDB storage into memory. * Call once after construction, before first use of the registry. - * @throws Error if storage is not configured */ - async loadFromStorage(): Promise { - if (!this.storage) { - throw new Error( - 'Cannot load activists from storage: storage not configured.', - ) - } + async loadFromStorage(storage: ActivistStorage): Promise { + this.storage = storage - const stored = await this.storage.getAllActivists() - this.activists = stored + this.activists = await this.storage.getAllActivists() this.sortActivists() - this.activistsByName = new Map(stored.map((a) => [a.name, a])) - this.activistsById = new Map(stored.map((a) => [a.id, a])) + this.activistsByName = new Map(this.activists.map((a) => [a.name, a])) + this.activistsById = new Map(this.activists.map((a) => [a.id, a])) } /** - * Merges new activists with existing data, replacing duplicates by id. + * Merges new activists with existing data. * If storage is configured, persists updates to IndexedDB. */ async mergeActivists(newActivists: ActivistRecord[]): Promise { @@ -79,27 +70,17 @@ export class ActivistRegistry { const existingIndex = indexById.get(activist.id) ?? -1 if (existingIndex >= 0) { - // Update existing activist (handles renames properly) - const oldActivist = this.activists[existingIndex] this.activists[existingIndex] = activist - - // Remove old name from index if name changed - if (oldActivist.name !== activist.name) { - this.activistsByName.delete(oldActivist.name) - } } else { - // Add new activist this.activists.push(activist) indexById.set(activist.id, this.activists.length - 1) } - // Update indexes - this.activistsByName.set(activist.name, activist) this.activistsById.set(activist.id, activist) } - // Re-sort after batch updates to maintain sort order this.sortActivists() + this.activistsByName = new Map(this.activists.map((a) => [a.name, a])) // Write through to storage if configured await this.storage?.saveActivists(newActivists) @@ -116,14 +97,16 @@ export class ActivistRegistry { const idsToRemove = new Set(ids) - this.activists = this.activists.filter((activist) => { + const remainingActivists: ActivistRecord[] = [] + for (const activist of this.activists) { if (idsToRemove.has(activist.id)) { this.activistsByName.delete(activist.name) this.activistsById.delete(activist.id) - return false + continue } - return true - }) + remainingActivists.push(activist) + } + this.activists = remainingActivists // Write through to storage if configured await this.storage?.deleteActivistsByIds(ids) diff --git a/frontend-v2/src/app/event/activist-storage.ts b/frontend-v2/src/app/event/activist-storage.ts index 1322cb74..2b4fb43a 100644 --- a/frontend-v2/src/app/event/activist-storage.ts +++ b/frontend-v2/src/app/event/activist-storage.ts @@ -3,7 +3,7 @@ * Provides caching and incremental sync capabilities. * * Why IndexedDB instead of localStorage: - * - Data size: ~3MB currently, approaching localStorage's 5-10MB limit + * - Data size: ~3MB as of January 2026, approaching localStorage's 5-10MB limit * - Async operations: Avoid blocking main thread with large JSON parse/stringify * - Structured storage: Store by ID with built-in indexing */ @@ -121,7 +121,10 @@ export class ActivistStorage { const request = timestamp === null ? store.delete('lastSync') - : store.put({ lastSyncTime: timestamp }, 'lastSync') + : store.put( + { lastSyncTime: timestamp } satisfies SyncMetadata, + 'lastSync', + ) request.onsuccess = () => resolve() request.onerror = () => reject(request.error) diff --git a/frontend-v2/src/app/event/useActivistRegistry.ts b/frontend-v2/src/app/event/useActivistRegistry.ts index 07a3482a..bc8b6afa 100644 --- a/frontend-v2/src/app/event/useActivistRegistry.ts +++ b/frontend-v2/src/app/event/useActivistRegistry.ts @@ -15,7 +15,8 @@ import toast from 'react-hot-toast' */ export function useActivistRegistry() { // Single registry instance with write-through storage to IndexedDB (if available) - const registryRef = useRef(new ActivistRegistry(activistStorage)) + const registry = new ActivistRegistry() + const registryRef = useRef(registry) const [isStorageLoaded, setIsStorageLoaded] = useState(false) const [isServerLoaded, setIsServerLoaded] = useState(false) @@ -33,7 +34,7 @@ export function useActivistRegistry() { } registryRef.current - .loadFromStorage() + .loadFromStorage(activistStorage) .then(() => { if (mounted) setIsStorageLoaded(true) }) @@ -131,9 +132,7 @@ export function useActivistRegistry() { } // Merge newer activists (registry handles both memory and storage) - if (activistsToUpdate.length > 0) { - await registryRef.current.mergeActivists(activistsToUpdate) - } + await registryRef.current.mergeActivists(activistsToUpdate) // Update last sync timestamp await registryRef.current.setLastSyncTime(new Date().toISOString())