fix: sync scans immediately and mark acknowledgements
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
parent
6b0da0c396
commit
c858f4e252
@ -10,4 +10,12 @@ describe('sync contract', () => {
|
||||
expect(source).toContain("window.addEventListener('online'")
|
||||
expect(source).toContain("document.addEventListener('visibilitychange'")
|
||||
})
|
||||
|
||||
it('reacts immediately to local capture events and coalesces concurrent sync requests', async () => {
|
||||
const source = await readFile(fileURLToPath(new URL('./sync.ts', import.meta.url)), 'utf8')
|
||||
|
||||
expect(source).toContain('FIELDLENS_SYNC_REQUEST_EVENT')
|
||||
expect(source).toContain('syncRequested = true')
|
||||
expect(source).toContain('while (syncRequested)')
|
||||
})
|
||||
})
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
type SyncResponse,
|
||||
} from '@fieldlens/contracts'
|
||||
import {
|
||||
FIELDLENS_SYNC_REQUEST_EVENT,
|
||||
acknowledgeOutboxEvent,
|
||||
failOutboxEvent,
|
||||
getInstallationId,
|
||||
@ -57,10 +58,17 @@ export async function ensureDeviceRegistered(): Promise<RegisteredDevice | null>
|
||||
}
|
||||
|
||||
let syncInFlight: Promise<void> | null = null
|
||||
let syncRequested = false
|
||||
|
||||
export function syncNow() {
|
||||
syncRequested = true
|
||||
if (syncInFlight) return syncInFlight
|
||||
syncInFlight = runSync().finally(() => { syncInFlight = null })
|
||||
syncInFlight = (async () => {
|
||||
do {
|
||||
syncRequested = false
|
||||
await runSync()
|
||||
} while (syncRequested)
|
||||
})().finally(() => { syncInFlight = null })
|
||||
return syncInFlight
|
||||
}
|
||||
|
||||
@ -103,12 +111,14 @@ export function installSyncTriggers() {
|
||||
if (document.visibilityState === 'visible') attempt()
|
||||
}
|
||||
window.addEventListener('online', attempt)
|
||||
window.addEventListener(FIELDLENS_SYNC_REQUEST_EVENT, attempt)
|
||||
document.addEventListener('visibilitychange', onVisible)
|
||||
const interval = window.setInterval(attempt, 30_000)
|
||||
attempt()
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', attempt)
|
||||
window.removeEventListener(FIELDLENS_SYNC_REQUEST_EVENT, attempt)
|
||||
document.removeEventListener('visibilitychange', onVisible)
|
||||
window.clearInterval(interval)
|
||||
}
|
||||
|
||||
@ -65,13 +65,14 @@ describe('scan persistence', () => {
|
||||
})
|
||||
|
||||
it('marks only explicitly acknowledged events synced', async () => {
|
||||
await recordScan('ONE', 'Code 128')
|
||||
const first = await recordScan('ONE', 'Code 128')
|
||||
await recordScan('TWO', 'Code 128')
|
||||
const events = await database.outbox.toArray()
|
||||
await acknowledgeOutboxEvent(events[0]!.id)
|
||||
const stats = await getOutboxStats()
|
||||
expect(stats.synced).toBe(1)
|
||||
expect(stats.pending).toBe(1)
|
||||
await expect(database.scans.get(first.id)).resolves.toMatchObject({ syncStatus: 'synced' })
|
||||
})
|
||||
|
||||
it('queues a local pallet session without any remote dependency', async () => {
|
||||
|
||||
@ -3,6 +3,7 @@ import type { CartonLabelMajorityField, CartonLabelScan } from './cartonLabelCon
|
||||
import type { LabelFields } from './labelOcr'
|
||||
|
||||
export const FIELDLENS_SCHEMA_VERSION = 1 as const
|
||||
export const FIELDLENS_SYNC_REQUEST_EVENT = 'fieldlens:sync-request' as const
|
||||
|
||||
export type ScanRecord = {
|
||||
id: string
|
||||
@ -101,6 +102,11 @@ class FieldLensDatabase extends Dexie {
|
||||
|
||||
export const database = new FieldLensDatabase()
|
||||
|
||||
function requestBackgroundSync() {
|
||||
if (typeof window === 'undefined') return
|
||||
window.dispatchEvent(new Event(FIELDLENS_SYNC_REQUEST_EVENT))
|
||||
}
|
||||
|
||||
function outboxEvent(
|
||||
aggregateId: string,
|
||||
eventType: OutboxRecord['eventType'],
|
||||
@ -119,7 +125,7 @@ function outboxEvent(
|
||||
}
|
||||
|
||||
export async function recordScan(rawValue: string, format: string) {
|
||||
return database.transaction('rw', database.scans, database.outbox, async () => {
|
||||
const record = await database.transaction('rw', database.scans, database.outbox, async () => {
|
||||
const existing = await database.scans.where('rawValue').equals(rawValue).first()
|
||||
const nextRecord: ScanRecord = {
|
||||
id: crypto.randomUUID(),
|
||||
@ -141,6 +147,8 @@ export async function recordScan(rawValue: string, format: string) {
|
||||
}, nextRecord.scannedAt))
|
||||
return nextRecord
|
||||
})
|
||||
requestBackgroundSync()
|
||||
return record
|
||||
}
|
||||
|
||||
export async function startEanPalletSession(targetCount: number) {
|
||||
@ -346,10 +354,19 @@ export async function getOutboxStats() {
|
||||
}
|
||||
|
||||
export async function acknowledgeOutboxEvent(id: string) {
|
||||
await database.outbox.update(id, {
|
||||
syncedAt: new Date().toISOString(),
|
||||
lastError: undefined,
|
||||
nextAttemptAt: undefined,
|
||||
await database.transaction('rw', database.outbox, database.scans, async () => {
|
||||
const event = await database.outbox.get(id)
|
||||
if (!event) return
|
||||
|
||||
await database.outbox.update(id, {
|
||||
syncedAt: new Date().toISOString(),
|
||||
lastError: undefined,
|
||||
nextAttemptAt: undefined,
|
||||
})
|
||||
|
||||
if (event.eventType === 'scan.captured') {
|
||||
await database.scans.update(event.aggregateId, { syncStatus: 'synced' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user