diff --git a/__tests__/lib/highlight-sync-manager.test.ts b/__tests__/lib/highlight-sync-manager.test.ts new file mode 100644 index 0000000..a5093eb --- /dev/null +++ b/__tests__/lib/highlight-sync-manager.test.ts @@ -0,0 +1,79 @@ +import { HighlightSyncManager } from '@/lib/highlight-sync-manager' +import { BibleHighlight } from '@/types' + +describe('HighlightSyncManager', () => { + let manager: HighlightSyncManager + + beforeEach(() => { + manager = new HighlightSyncManager() + }) + + it('should add highlight to sync queue', async () => { + const highlight: BibleHighlight = { + id: 'h-1', + verseId: 'v-1', + color: 'yellow', + createdAt: Date.now(), + updatedAt: Date.now(), + syncStatus: 'pending' + } + + await manager.queueHighlight(highlight) + const pending = await manager.getPendingSyncItems() + + expect(pending.length).toBe(1) + expect(pending[0].id).toBe('h-1') + }) + + it('should mark highlight as syncing', async () => { + const highlight: BibleHighlight = { + id: 'h-1', + verseId: 'v-1', + color: 'yellow', + createdAt: Date.now(), + updatedAt: Date.now(), + syncStatus: 'pending' + } + + await manager.queueHighlight(highlight) + await manager.markSyncing(['h-1']) + + const syncing = await manager.getSyncingItems() + expect(syncing.length).toBe(1) + }) + + it('should mark highlight as synced', async () => { + const highlight: BibleHighlight = { + id: 'h-1', + verseId: 'v-1', + color: 'yellow', + createdAt: Date.now(), + updatedAt: Date.now(), + syncStatus: 'pending' + } + + await manager.queueHighlight(highlight) + await manager.markSynced(['h-1']) + + const pending = await manager.getPendingSyncItems() + expect(pending.length).toBe(0) + }) + + it('should retry sync on error', async () => { + const highlight: BibleHighlight = { + id: 'h-1', + verseId: 'v-1', + color: 'yellow', + createdAt: Date.now(), + updatedAt: Date.now(), + syncStatus: 'pending' + } + + await manager.queueHighlight(highlight) + await manager.markError(['h-1'], 'Network error') + await manager.markSyncing(['h-1']) + + const syncing = await manager.getSyncingItems() + expect(syncing.length).toBe(1) + }) +}) diff --git a/lib/highlight-sync-manager.ts b/lib/highlight-sync-manager.ts new file mode 100644 index 0000000..7f628e0 --- /dev/null +++ b/lib/highlight-sync-manager.ts @@ -0,0 +1,126 @@ +import { BibleHighlight, HighlightSyncQueueItem } from '@/types' +import { + initHighlightsDatabase, + updateHighlight, + getHighlight +} from './highlight-manager' + +const SYNC_QUEUE_STORE = 'highlight_sync_queue' + +export class HighlightSyncManager { + private db: IDBDatabase | null = null + private syncInterval: NodeJS.Timeout | null = null + + async init() { + this.db = await initHighlightsDatabase() + + // Create sync queue store if it doesn't exist + if (!this.db.objectStoreNames.contains(SYNC_QUEUE_STORE)) { + // Note: In real app, this would be done in onupgradeneeded + // For this implementation, assume schema is managed separately + } + } + + async queueHighlight(highlight: BibleHighlight): Promise { + if (!this.db) await this.init() + + const queueItem: HighlightSyncQueueItem = { + highlightId: highlight.id, + action: highlight.syncStatus === 'synced' ? 'update' : 'create', + highlight, + retryCount: 0 + } + + await updateHighlight({ + ...highlight, + syncStatus: 'pending' + }) + } + + async getPendingSyncItems(): Promise { + if (!this.db) await this.init() + + return new Promise((resolve, reject) => { + const tx = this.db!.transaction('highlights', 'readonly') + const store = tx.objectStore('highlights') + const index = store.index('syncStatus') + const request = index.getAll('pending') + + request.onsuccess = () => resolve(request.result || []) + request.onerror = () => reject(new Error('Failed to get pending items')) + }) + } + + async getSyncingItems(): Promise { + if (!this.db) await this.init() + + return new Promise((resolve, reject) => { + const tx = this.db!.transaction('highlights', 'readonly') + const store = tx.objectStore('highlights') + const index = store.index('syncStatus') + const request = index.getAll('syncing') + + request.onsuccess = () => resolve(request.result || []) + request.onerror = () => reject(new Error('Failed to get syncing items')) + }) + } + + async markSyncing(highlightIds: string[]): Promise { + if (!this.db) await this.init() + + for (const id of highlightIds) { + const highlight = await getHighlight(id) + if (highlight) { + await updateHighlight({ + ...highlight, + syncStatus: 'syncing' + }) + } + } + } + + async markSynced(highlightIds: string[]): Promise { + if (!this.db) await this.init() + + for (const id of highlightIds) { + const highlight = await getHighlight(id) + if (highlight) { + await updateHighlight({ + ...highlight, + syncStatus: 'synced' + }) + } + } + } + + async markError(highlightIds: string[], errorMsg: string): Promise { + if (!this.db) await this.init() + + for (const id of highlightIds) { + const highlight = await getHighlight(id) + if (highlight) { + await updateHighlight({ + ...highlight, + syncStatus: 'error', + syncErrorMsg: errorMsg + }) + } + } + } + + startAutoSync(intervalMs: number = 30000, onSyncNeeded?: () => void) { + this.syncInterval = setInterval(async () => { + const pending = await this.getPendingSyncItems() + if (pending.length > 0 && onSyncNeeded) { + onSyncNeeded() + } + }, intervalMs) + } + + stopAutoSync() { + if (this.syncInterval) { + clearInterval(this.syncInterval) + this.syncInterval = null + } + } +}