import { api } from './pocketbase.svelte' import type { TimesStatusOptions } from '$lib/types' const KEY = 'timeOutbox' /** * Ein Start oder ein Stopp, der noch nicht beim Server ist. * * `at` ist die abgeglichene Zeit im Moment des Drückens — nicht die des * Übertragens. Darin liegt der ganze Sinn: Wann die Nachricht rausgeht, darf * die gemessene Zeit nicht berühren. * * Ein Stopp trägt kein `time`, wenn die Zeit selbst noch im Ausgang liegt * (offline gestartet und offline gestoppt). Er zeigt dann über `localId` auf * den Start und wird erst beim Übertragen aufgelöst. */ export type OutboxEntry = | { kind: 'start' localId: string at: string stage: string rider: string team: string startedBy?: string } | { kind: 'stop' localId: string at: string status: TimesStatusOptions /** ID der Zeit auf dem Server — fehlt, wenn der Start noch aussteht. */ time?: string /** Start aus diesem Ausgang, auf den sich der Stopp bezieht. */ startLocalId?: string stoppedBy?: string } /** * Was an der Strecke gedrückt wurde und noch nicht beim Server ankam. * * Eine Stage lässt sich nicht wiederholen. Ein Funkloch im falschen Moment * darf deshalb nicht bedeuten, dass eine Zeit verloren ist — gedrückt wird * ins localStorage, übertragen wird, sobald es geht. Die Reihenfolge bleibt * erhalten: Ein Stopp kann auf einen Start warten, der selbst noch aussteht. * * Bewusst kein Kontext-Store: Der Ausgang gehört dem Gerät und überlebt * Seitenwechsel, Neustart und Abmeldung. */ export class TimeOutbox { entries = $state([]) /** Läuft gerade eine Übertragung? */ flushing = $state(false) /** Was beim letzten Versuch schiefging — bleibt stehen, bis es klappt. */ error = $state(null) private online = $state(true) constructor() { if (typeof localStorage !== 'undefined') { try { const raw = localStorage.getItem(KEY) if (raw) this.entries = JSON.parse(raw) as OutboxEntry[] } catch { // Kaputter Ausgang: lieber leer weiter als beim Start scheitern. } } if (typeof window !== 'undefined') { this.online = navigator.onLine window.addEventListener('online', () => { this.online = true void this.flush() }) window.addEventListener('offline', () => (this.online = false)) } } get pending(): number { return this.entries.length } get isOnline(): boolean { return this.online } private persist() { if (typeof localStorage === 'undefined') return try { localStorage.setItem(KEY, JSON.stringify(this.entries)) } catch { // Ohne Speicher bleibt der Ausgang in dieser Sitzung — dann muss // die Übertragung klappen, bevor jemand die Seite schliesst. } } private add(entry: OutboxEntry) { this.entries = [...this.entries, entry] this.persist() } /** Start vormerken. Gibt die lokale ID zurück, damit ein Stopp sie findet. */ queueStart(entry: Omit, 'kind' | 'localId'>): string { const localId = crypto.randomUUID() this.add({ kind: 'start', localId, ...entry }) return localId } queueStop(entry: Omit, 'kind' | 'localId'>): string { const localId = crypto.randomUUID() this.add({ kind: 'stop', localId, ...entry }) return localId } /** * Alles übertragen, was geht — der Reihe nach, denn ein Stopp kann auf * seinen Start warten. Der erste Fehler bricht ab: Ist das Netz weg, ist * es auch für den nächsten Eintrag weg, und die Reihenfolge muss halten. */ async flush(): Promise { if (this.flushing || this.entries.length === 0) return this.flushing = true this.error = null // Zeit-IDs, die in diesem Durchlauf entstanden sind — ein Stopp aus // dem Ausgang kennt nur die lokale ID seines Starts. const resolved = new Map() try { while (this.entries.length > 0) { const entry = this.entries[0] if (entry.kind === 'start') { const record = await api.collection('times').create({ stage: entry.stage, rider: entry.rider, team: entry.team, start: entry.at, status: 'active', startedBy: entry.startedBy, correction: 0, }) resolved.set(entry.localId, record.id) } else { const timeId = entry.time ?? (entry.startLocalId ? resolved.get(entry.startLocalId) : undefined) if (!timeId) { // Der zugehörige Start ist weder auf dem Server noch in // diesem Durchlauf entstanden. Weiterreichen wäre ein // stiller Verlust, also stehen lassen und melden. throw new Error( 'Zu einem gestoppten Lauf fehlt der Start. Die Zeit bleibt im Ausgang.', ) } await api.collection('times').update(timeId, { stop: entry.at, status: entry.status, stoppedBy: entry.stoppedBy, }) } this.entries = this.entries.slice(1) this.persist() } } catch (e: any) { this.error = e?.message ?? 'Die Übertragung ist fehlgeschlagen.' } finally { this.flushing = false } } } export const timeOutbox = new TimeOutbox()