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("window.addEventListener('online'")
|
||||||
expect(source).toContain("document.addEventListener('visibilitychange'")
|
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,
|
type SyncResponse,
|
||||||
} from '@fieldlens/contracts'
|
} from '@fieldlens/contracts'
|
||||||
import {
|
import {
|
||||||
|
FIELDLENS_SYNC_REQUEST_EVENT,
|
||||||
acknowledgeOutboxEvent,
|
acknowledgeOutboxEvent,
|
||||||
failOutboxEvent,
|
failOutboxEvent,
|
||||||
getInstallationId,
|
getInstallationId,
|
||||||
@ -57,10 +58,17 @@ export async function ensureDeviceRegistered(): Promise<RegisteredDevice | null>
|
|||||||
}
|
}
|
||||||
|
|
||||||
let syncInFlight: Promise<void> | null = null
|
let syncInFlight: Promise<void> | null = null
|
||||||
|
let syncRequested = false
|
||||||
|
|
||||||
export function syncNow() {
|
export function syncNow() {
|
||||||
|
syncRequested = true
|
||||||
if (syncInFlight) return syncInFlight
|
if (syncInFlight) return syncInFlight
|
||||||
syncInFlight = runSync().finally(() => { syncInFlight = null })
|
syncInFlight = (async () => {
|
||||||
|
do {
|
||||||
|
syncRequested = false
|
||||||
|
await runSync()
|
||||||
|
} while (syncRequested)
|
||||||
|
})().finally(() => { syncInFlight = null })
|
||||||
return syncInFlight
|
return syncInFlight
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,12 +111,14 @@ export function installSyncTriggers() {
|
|||||||
if (document.visibilityState === 'visible') attempt()
|
if (document.visibilityState === 'visible') attempt()
|
||||||
}
|
}
|
||||||
window.addEventListener('online', attempt)
|
window.addEventListener('online', attempt)
|
||||||
|
window.addEventListener(FIELDLENS_SYNC_REQUEST_EVENT, attempt)
|
||||||
document.addEventListener('visibilitychange', onVisible)
|
document.addEventListener('visibilitychange', onVisible)
|
||||||
const interval = window.setInterval(attempt, 30_000)
|
const interval = window.setInterval(attempt, 30_000)
|
||||||
attempt()
|
attempt()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('online', attempt)
|
window.removeEventListener('online', attempt)
|
||||||
|
window.removeEventListener(FIELDLENS_SYNC_REQUEST_EVENT, attempt)
|
||||||
document.removeEventListener('visibilitychange', onVisible)
|
document.removeEventListener('visibilitychange', onVisible)
|
||||||
window.clearInterval(interval)
|
window.clearInterval(interval)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,13 +65,14 @@ describe('scan persistence', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('marks only explicitly acknowledged events synced', async () => {
|
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')
|
await recordScan('TWO', 'Code 128')
|
||||||
const events = await database.outbox.toArray()
|
const events = await database.outbox.toArray()
|
||||||
await acknowledgeOutboxEvent(events[0]!.id)
|
await acknowledgeOutboxEvent(events[0]!.id)
|
||||||
const stats = await getOutboxStats()
|
const stats = await getOutboxStats()
|
||||||
expect(stats.synced).toBe(1)
|
expect(stats.synced).toBe(1)
|
||||||
expect(stats.pending).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 () => {
|
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'
|
import type { LabelFields } from './labelOcr'
|
||||||
|
|
||||||
export const FIELDLENS_SCHEMA_VERSION = 1 as const
|
export const FIELDLENS_SCHEMA_VERSION = 1 as const
|
||||||
|
export const FIELDLENS_SYNC_REQUEST_EVENT = 'fieldlens:sync-request' as const
|
||||||
|
|
||||||
export type ScanRecord = {
|
export type ScanRecord = {
|
||||||
id: string
|
id: string
|
||||||
@ -101,6 +102,11 @@ class FieldLensDatabase extends Dexie {
|
|||||||
|
|
||||||
export const database = new FieldLensDatabase()
|
export const database = new FieldLensDatabase()
|
||||||
|
|
||||||
|
function requestBackgroundSync() {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
window.dispatchEvent(new Event(FIELDLENS_SYNC_REQUEST_EVENT))
|
||||||
|
}
|
||||||
|
|
||||||
function outboxEvent(
|
function outboxEvent(
|
||||||
aggregateId: string,
|
aggregateId: string,
|
||||||
eventType: OutboxRecord['eventType'],
|
eventType: OutboxRecord['eventType'],
|
||||||
@ -119,7 +125,7 @@ function outboxEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function recordScan(rawValue: string, format: string) {
|
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 existing = await database.scans.where('rawValue').equals(rawValue).first()
|
||||||
const nextRecord: ScanRecord = {
|
const nextRecord: ScanRecord = {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
@ -141,6 +147,8 @@ export async function recordScan(rawValue: string, format: string) {
|
|||||||
}, nextRecord.scannedAt))
|
}, nextRecord.scannedAt))
|
||||||
return nextRecord
|
return nextRecord
|
||||||
})
|
})
|
||||||
|
requestBackgroundSync()
|
||||||
|
return record
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startEanPalletSession(targetCount: number) {
|
export async function startEanPalletSession(targetCount: number) {
|
||||||
@ -346,10 +354,19 @@ export async function getOutboxStats() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function acknowledgeOutboxEvent(id: string) {
|
export async function acknowledgeOutboxEvent(id: string) {
|
||||||
await database.outbox.update(id, {
|
await database.transaction('rw', database.outbox, database.scans, async () => {
|
||||||
syncedAt: new Date().toISOString(),
|
const event = await database.outbox.get(id)
|
||||||
lastError: undefined,
|
if (!event) return
|
||||||
nextAttemptAt: undefined,
|
|
||||||
|
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