Some checks failed
E2E / Playwright e2e (push) Has been cancelled
Move mail, compose, contacts, and accounts off mocks onto REST + WS. Add client, auth store, IDB-backed query cache, offline queue, and sync bar; hybrid Zustand for UI-only state. Settings still local until backend has preferences API.
107 lines
3.0 KiB
TypeScript
107 lines
3.0 KiB
TypeScript
import { openDB, type IDBPDatabase } from "idb"
|
|
import { apiClient } from "./client"
|
|
|
|
export interface PendingMutation {
|
|
id: string
|
|
timestamp: number
|
|
type:
|
|
| "send_message"
|
|
| "update_flags"
|
|
| "update_labels"
|
|
| "delete_message"
|
|
| "create_draft"
|
|
| "update_draft"
|
|
| "schedule_send"
|
|
| "create_contact"
|
|
| "update_contact"
|
|
| "delete_draft"
|
|
| "delete_contact"
|
|
payload: unknown
|
|
retries: number
|
|
}
|
|
|
|
const DB_NAME = "ultimail-offline-queue"
|
|
const STORE_NAME = "mutations"
|
|
|
|
let dbPromise: Promise<IDBPDatabase> | null = null
|
|
|
|
function getDb() {
|
|
if (!dbPromise) {
|
|
dbPromise = openDB(DB_NAME, 1, {
|
|
upgrade(db) {
|
|
db.createObjectStore(STORE_NAME, { keyPath: "id" })
|
|
},
|
|
})
|
|
}
|
|
return dbPromise
|
|
}
|
|
|
|
export async function enqueue(mutation: PendingMutation): Promise<void> {
|
|
const db = await getDb()
|
|
await db.put(STORE_NAME, mutation)
|
|
}
|
|
|
|
export async function getAll(): Promise<PendingMutation[]> {
|
|
const db = await getDb()
|
|
return db.getAll(STORE_NAME)
|
|
}
|
|
|
|
export async function remove(id: string): Promise<void> {
|
|
const db = await getDb()
|
|
await db.delete(STORE_NAME, id)
|
|
}
|
|
|
|
export async function getPendingCount(): Promise<number> {
|
|
const db = await getDb()
|
|
return db.count(STORE_NAME)
|
|
}
|
|
|
|
const MUTATION_ENDPOINTS: Record<PendingMutation["type"], { method: "post" | "put" | "delete"; path: (p: any) => string }> = {
|
|
send_message: { method: "post", path: () => "/outbox" },
|
|
update_flags: { method: "put", path: (p) => `/messages/${p.message_id}/flags` },
|
|
update_labels: { method: "put", path: (p) => `/messages/${p.message_id}/labels` },
|
|
delete_message: { method: "delete", path: (p) => `/messages/${p.message_id}` },
|
|
create_draft: { method: "post", path: () => "/drafts" },
|
|
update_draft: { method: "put", path: (p) => `/drafts/${p.draft_id}` },
|
|
schedule_send: { method: "post", path: () => "/outbox/schedule" },
|
|
delete_draft: { method: "delete", path: (p) => `/drafts/${p.draft_id}` },
|
|
create_contact: { method: "post", path: () => "/contacts" },
|
|
update_contact: { method: "put", path: (p) => `/contacts/${p.uid}` },
|
|
delete_contact: { method: "delete", path: (p) => `/contacts/${p.uid}` },
|
|
}
|
|
|
|
export async function flush(): Promise<void> {
|
|
const mutations = await getAll()
|
|
const sorted = mutations.sort((a, b) => a.timestamp - b.timestamp)
|
|
|
|
for (const mutation of sorted) {
|
|
try {
|
|
const endpoint = MUTATION_ENDPOINTS[mutation.type]
|
|
const path = endpoint.path(mutation.payload)
|
|
|
|
switch (endpoint.method) {
|
|
case "post":
|
|
await apiClient.post(path, mutation.payload)
|
|
break
|
|
case "put":
|
|
await apiClient.put(path, mutation.payload)
|
|
break
|
|
case "delete":
|
|
await apiClient.delete(path)
|
|
break
|
|
}
|
|
|
|
await remove(mutation.id)
|
|
} catch {
|
|
const db = await getDb()
|
|
await db.put(STORE_NAME, { ...mutation, retries: mutation.retries + 1 })
|
|
}
|
|
}
|
|
}
|
|
|
|
if (typeof window !== "undefined") {
|
|
window.addEventListener("online", () => {
|
|
flush()
|
|
})
|
|
}
|