Admin da34221539
Some checks failed
CI / verify (push) Has been cancelled
mirror: fieldlens d38e0922441a (1/2)
2026-09-23 01:14:28 +00:00

118 lines
3.7 KiB
TypeScript

import {
FIELDLENS_SCHEMA_VERSION,
type DeviceRegistration,
type Station,
type SyncResponse,
} from '@fieldlens/contracts'
import {
acknowledgeOutboxEvent,
failOutboxEvent,
getInstallationId,
getLocalMetadata,
getPendingOutbox,
setLocalMetadata,
} from '../../../packages/fieldlens-core/src/lib/database'
import { getAccessToken } from './auth'
const apiBase = (import.meta.env.VITE_API_BASE_URL ?? '').replace(/\/$/, '')
export type RegisteredDevice = {
id: string
station: Station
}
async function authenticatedFetch(path: string, init: RequestInit = {}) {
const token = await getAccessToken()
if (!token) throw new Error('Authentication unavailable')
const headers = new Headers(init.headers)
headers.set('Authorization', `Bearer ${token}`)
if (init.body) headers.set('Content-Type', 'application/json')
const response = await fetch(`${apiBase}${path}`, { ...init, headers })
if (!response.ok) throw new Error(`API ${response.status}`)
return response
}
export async function ensureDeviceRegistered(): Promise<RegisteredDevice | null> {
const existing = await getLocalMetadata<RegisteredDevice>('registeredDevice')
const token = await getAccessToken()
if (!token) return existing ?? null
const installationId = await getInstallationId()
const registration: DeviceRegistration = {
installationId,
appVersion: import.meta.env.VITE_APP_VERSION ?? 'dev',
schemaVersion: FIELDLENS_SCHEMA_VERSION,
}
try {
const response = await authenticatedFetch('/api/v1/devices/register', {
method: 'POST',
body: JSON.stringify(registration),
})
const next = await response.json() as RegisteredDevice
await setLocalMetadata('registeredDevice', next)
return next
} catch {
return existing ?? null
}
}
let syncInFlight: Promise<void> | null = null
export function syncNow() {
if (syncInFlight) return syncInFlight
syncInFlight = runSync().finally(() => { syncInFlight = null })
return syncInFlight
}
async function runSync() {
const device = await ensureDeviceRegistered()
if (!device) return
const events = await getPendingOutbox(50)
if (events.length === 0) return
try {
const response = await authenticatedFetch('/api/v1/sync', {
method: 'POST',
body: JSON.stringify({
deviceId: device.id,
schemaVersion: FIELDLENS_SCHEMA_VERSION,
events: events.map(({ attempts: _attempts, nextAttemptAt: _next, syncedAt: _synced, lastError: _error, ...event }) => event),
}),
})
const body = await response.json() as SyncResponse
const acknowledgements = new Map(body.acknowledgements.map((ack) => [ack.eventId, ack]))
for (const event of events) {
const acknowledgement = acknowledgements.get(event.id)
if (acknowledgement?.status === 'accepted' || acknowledgement?.status === 'duplicate') {
await acknowledgeOutboxEvent(event.id)
} else {
await failOutboxEvent(event.id, acknowledgement?.error ?? 'Event was not acknowledged')
}
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Sync request failed'
await Promise.all(events.map((event) => failOutboxEvent(event.id, message)))
}
}
export function installSyncTriggers() {
const attempt = () => { void syncNow() }
const onVisible = () => {
if (document.visibilityState === 'visible') attempt()
}
window.addEventListener('online', attempt)
document.addEventListener('visibilitychange', onVisible)
const interval = window.setInterval(attempt, 30_000)
attempt()
return () => {
window.removeEventListener('online', attempt)
document.removeEventListener('visibilitychange', onVisible)
window.clearInterval(interval)
}
}
export { authenticatedFetch }