feat: implement client-side sync with bulk API
Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -76,4 +76,31 @@ describe('HighlightSyncManager', () => {
|
|||||||
const syncing = await manager.getSyncingItems()
|
const syncing = await manager.getSyncingItems()
|
||||||
expect(syncing.length).toBe(1)
|
expect(syncing.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should perform sync and mark items 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.init()
|
||||||
|
|
||||||
|
// Mock fetch
|
||||||
|
global.fetch = jest.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ synced: 1, errors: [] })
|
||||||
|
})
|
||||||
|
) as jest.Mock
|
||||||
|
|
||||||
|
const result = await manager.performSync()
|
||||||
|
|
||||||
|
expect(result.synced).toBe(1)
|
||||||
|
expect(result.errors).toBe(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -108,11 +108,69 @@ export class HighlightSyncManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startAutoSync(intervalMs: number = 30000, onSyncNeeded?: () => void) {
|
async performSync(): Promise<{ synced: number; errors: number }> {
|
||||||
this.syncInterval = setInterval(async () => {
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
|
try {
|
||||||
const pending = await this.getPendingSyncItems()
|
const pending = await this.getPendingSyncItems()
|
||||||
if (pending.length > 0 && onSyncNeeded) {
|
if (pending.length === 0) return { synced: 0, errors: 0 }
|
||||||
onSyncNeeded()
|
|
||||||
|
// 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)
|
}, intervalMs)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user