From c858f4e25211a2ef6968c526b399f6c77c426d4c Mon Sep 17 00:00:00 2001 From: Admin Date: Wed, 23 Sep 2026 09:44:08 +0000 Subject: [PATCH] fix: sync scans immediately and mark acknowledgements --- apps/web/src/sync.test.ts | 8 ++++++ apps/web/src/sync.ts | 12 ++++++++- .../fieldlens-core/src/lib/database.test.ts | 3 ++- packages/fieldlens-core/src/lib/database.ts | 27 +++++++++++++++---- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/apps/web/src/sync.test.ts b/apps/web/src/sync.test.ts index f5294db..f9df88b 100644 --- a/apps/web/src/sync.test.ts +++ b/apps/web/src/sync.test.ts @@ -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)') + }) }) diff --git a/apps/web/src/sync.ts b/apps/web/src/sync.ts index 5b1fc4c..d376eaa 100644 --- a/apps/web/src/sync.ts +++ b/apps/web/src/sync.ts @@ -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 } let syncInFlight: Promise | 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) } diff --git a/packages/fieldlens-core/src/lib/database.test.ts b/packages/fieldlens-core/src/lib/database.test.ts index 7cb2dd9..f6d8e3c 100644 --- a/packages/fieldlens-core/src/lib/database.test.ts +++ b/packages/fieldlens-core/src/lib/database.test.ts @@ -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 () => { diff --git a/packages/fieldlens-core/src/lib/database.ts b/packages/fieldlens-core/src/lib/database.ts index 6f8cf9b..1487ba4 100644 --- a/packages/fieldlens-core/src/lib/database.ts +++ b/packages/fieldlens-core/src/lib/database.ts @@ -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' }) + } }) }