"use client"; // Thin promise wrapper around the browser's native IndexedDB — no added // dependency. The service worker (public/sw.js) opens the same database by // name/version/store names using the raw API directly (it can't import this // module, since it's registered as a classic, non-module script), so any // change here must be mirrored there. const DB_NAME = "epicure-offline"; const DB_VERSION = 1; export const SAVED_RECIPES_STORE = "savedRecipes"; export const PENDING_ACTIONS_STORE = "pendingActions"; export type SavedRecipe = { id: string; title: string; savedAt: string }; export type PendingAction = { id: string; method: string; url: string; body: unknown; description: string; createdAt: string; }; function openDb(): Promise { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = () => { const db = req.result; if (!db.objectStoreNames.contains(SAVED_RECIPES_STORE)) { db.createObjectStore(SAVED_RECIPES_STORE, { keyPath: "id" }); } if (!db.objectStoreNames.contains(PENDING_ACTIONS_STORE)) { db.createObjectStore(PENDING_ACTIONS_STORE, { keyPath: "id" }); } }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function withStore(store: string, mode: IDBTransactionMode, fn: (s: IDBObjectStore) => T): Promise { const db = await openDb(); return new Promise((resolve, reject) => { const tx = db.transaction(store, mode); const result = fn(tx.objectStore(store)); tx.oncomplete = () => resolve(result); tx.onerror = () => reject(tx.error); }); } export async function saveRecipeOffline(id: string, title: string): Promise { await withStore(SAVED_RECIPES_STORE, "readwrite", (s) => { s.put({ id, title, savedAt: new Date().toISOString() }); }); } export async function unsaveRecipeOffline(id: string): Promise { await withStore(SAVED_RECIPES_STORE, "readwrite", (s) => { s.delete(id); }); } export async function isRecipeSavedOffline(id: string): Promise { const record = await withStore(SAVED_RECIPES_STORE, "readonly", (s) => { let result: SavedRecipe | undefined; s.get(id).onsuccess = (e) => { result = (e.target as IDBRequest).result; }; return result; }); // The transaction's oncomplete fires after the get's onsuccess callback // runs, so `record` is populated by the time withStore resolves. return record !== undefined; } export async function getSavedRecipesOffline(): Promise { return withStore(SAVED_RECIPES_STORE, "readonly", (s) => { let result: SavedRecipe[] = []; s.getAll().onsuccess = (e) => { result = (e.target as IDBRequest).result; }; return result; }); } export async function enqueuePendingAction(action: Omit): Promise { await withStore(PENDING_ACTIONS_STORE, "readwrite", (s) => { s.put({ ...action, id: crypto.randomUUID(), createdAt: new Date().toISOString() }); }); } export async function getPendingActions(): Promise { const actions = await withStore(PENDING_ACTIONS_STORE, "readonly", (s) => { let result: PendingAction[] = []; s.getAll().onsuccess = (e) => { result = (e.target as IDBRequest).result; }; return result; }); return actions.sort((a, b) => a.createdAt.localeCompare(b.createdAt)); } export async function deletePendingAction(id: string): Promise { await withStore(PENDING_ACTIONS_STORE, "readwrite", (s) => { s.delete(id); }); }