import { getDb, type QueueError, type StoredVoiceNote } from "@/lib/offline/db"; import { newIdempotencyKey } from "@/lib/offline/idempotency"; export type { StoredVoiceNote } from "@/lib/offline/db"; /* * Voice notes recorded on this device. The audio stays here until the sync * uploads it and is deleted on success, on discard and when the account signs * out (db.clearQueueOf). It is never logged and never leaves the device * before the user's upload. */ /** The device had no room left for another recording. */ export class VoiceStorageFullError extends Error { constructor() { super("Auf dem Gerät ist kein Platz mehr für die Sprachnotiz."); this.name = "VoiceStorageFullError"; } } export type NewVoiceNote = Omit< StoredVoiceNote, "id" | "createdAt" | "uploadIdempotencyKey" | "state" >; let persistAsked = false; /** Asks once per page for storage the browser will not clear on its own. Best effort. */ async function askForPersistentStorage(): Promise { if (persistAsked) { return; } persistAsked = true; try { await navigator.storage?.persist?.(); } catch { // Without persistent storage the note is still saved; the browser may clear it under pressure. } } function isQuotaError(error: unknown): boolean { return error instanceof DOMException && error.name === "QuotaExceededError"; } export async function queueVoiceNote(input: NewVoiceNote): Promise { await askForPersistentStorage(); const note: StoredVoiceNote = { ...input, id: newIdempotencyKey(), createdAt: new Date().toISOString(), uploadIdempotencyKey: newIdempotencyKey(), state: "recorded", }; const db = await getDb(); try { await db.add("pending-voice", note); } catch (error) { if (isQuotaError(error)) { throw new VoiceStorageFullError(); } throw error; } return note; } /** The signed-in user's notes, oldest first. Another account's notes never appear. */ export async function listPendingVoice(currentUserId: string): Promise { const db = await getDb(); const notes = await db.getAllFromIndex("pending-voice", "ownerUserId", currentUserId); return notes.sort((a, b) => a.createdAt.localeCompare(b.createdAt)); } /** How many notes of other accounts wait on this device - shown as a number only. */ export async function countForeignVoice(currentUserId: string): Promise { const db = await getDb(); const all = await db.getAllKeys("pending-voice"); const own = await db.countFromIndex("pending-voice", "ownerUserId", currentUserId); return all.length - own; } /** Deletes one note, but only if it belongs to `ownerUserId`. */ export async function discardPendingVoice(id: string, ownerUserId: string): Promise { const db = await getDb(); const tx = db.transaction("pending-voice", "readwrite"); const note = await tx.store.get(id); if (note?.ownerUserId === ownerUserId) { await tx.store.delete(id); } await tx.done; } /** Deletes every note one account recorded on this device; returns how many. */ export async function discardAllPendingVoice(ownerUserId: string): Promise { const db = await getDb(); const tx = db.transaction("pending-voice", "readwrite"); const ids = await tx.store.index("ownerUserId").getAllKeys(ownerUserId); await Promise.all(ids.map((id) => tx.store.delete(id))); await tx.done; return ids.length; } /** One note by id, or undefined when it is gone (discarded meanwhile). */ export async function getPendingVoice(id: string): Promise { const db = await getDb(); return db.get("pending-voice", id); } /** Books the server's refusal on a note that still exists; the audio stays for the user to see. */ export async function markVoiceFailed(id: string, error: QueueError): Promise { const db = await getDb(); const tx = db.transaction("pending-voice", "readwrite"); const note = await tx.store.get(id); if (note) { const rejected: StoredVoiceNote = { ...note, lastError: error, state: "recorded" }; delete rejected.blockedBy; await tx.store.put(rejected); } await tx.done; } /** Notes that the server could take later (too many open): the sheet says so. */ export async function markVoiceBlocked(id: string, blockedBy: "TOO_MANY_OPEN"): Promise { const db = await getDb(); const tx = db.transaction("pending-voice", "readwrite"); const note = await tx.store.get(id); if (note) { await tx.store.put({ ...note, blockedBy }); } await tx.done; }