Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
185 lines
5.2 KiB
TypeScript
185 lines
5.2 KiB
TypeScript
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<void> {
|
|
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<BibleHighlight[]> {
|
|
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<BibleHighlight[]> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
async performSync(): Promise<{ synced: number; errors: number }> {
|
|
if (!this.db) await this.init()
|
|
|
|
try {
|
|
const pending = await this.getPendingSyncItems()
|
|
if (pending.length === 0) return { synced: 0, errors: 0 }
|
|
|
|
// Mark as syncing
|
|
await this.markSyncing(pending.map(h => h.id))
|
|
|
|
// POST to backend
|
|
const response = await fetch('/api/highlights/bulk', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ highlights: pending })
|
|
})
|
|
|
|
if (!response.ok) {
|
|
// Mark all as error
|
|
const errorIds = pending.map(h => h.id)
|
|
await this.markError(errorIds, `HTTP ${response.status}`)
|
|
return { synced: 0, errors: pending.length }
|
|
}
|
|
|
|
const result = await response.json()
|
|
|
|
// Mark successfully synced items
|
|
if (result.synced > 0) {
|
|
const syncedIds = pending
|
|
.filter(h => !result.errors.some((e: any) => e.verseId === h.verseId))
|
|
.map(h => h.id)
|
|
await this.markSynced(syncedIds)
|
|
}
|
|
|
|
// Mark errored items
|
|
if (result.errors && result.errors.length > 0) {
|
|
for (const error of result.errors) {
|
|
const h = pending.find(item => item.verseId === error.verseId)
|
|
if (h) {
|
|
await this.markError([h.id], error.error)
|
|
}
|
|
}
|
|
}
|
|
|
|
return { synced: result.synced, errors: result.errors?.length || 0 }
|
|
} catch (error) {
|
|
console.error('Sync failed:', error)
|
|
const pending = await this.getPendingSyncItems()
|
|
if (pending.length > 0) {
|
|
await this.markError(
|
|
pending.map(h => h.id),
|
|
'Network error'
|
|
)
|
|
}
|
|
return { synced: 0, errors: pending.length }
|
|
}
|
|
}
|
|
|
|
startAutoSync(intervalMs: number = 30000, onSyncNeeded?: (result: { synced: number; errors: number }) => void) {
|
|
this.syncInterval = setInterval(async () => {
|
|
const result = await this.performSync()
|
|
if (result.synced > 0 || result.errors > 0) {
|
|
onSyncNeeded?.(result)
|
|
}
|
|
}, intervalMs)
|
|
}
|
|
|
|
stopAutoSync() {
|
|
if (this.syncInterval) {
|
|
clearInterval(this.syncInterval)
|
|
this.syncInterval = null
|
|
}
|
|
}
|
|
}
|