feat: offline save-for-later + background sync for mark-cooked (v0.65.0)
Adds a "Save offline" button on the recipe page that force-refetches the current page so the service worker's cache picks up a fresh copy right now, plus a small IndexedDB-backed list (lib/offline-db.ts) of what's been saved. The /offline fallback page now reads that list and renders it instead of being a dead end with just a "go back" link. Also fixes the service worker's network-first fetch handler, which never wrote successful responses into its cache -- meaning the existing offline page's claim that "recently visited recipes are available" was never actually true. It populates the cache on every successful GET now. Background sync: marking a batch-cook dish as cooked while offline (the only existing mark-cooked call site in the app) now queues the request in IndexedDB instead of just failing, and registers a Background Sync (public/sw.js's "sync" listener replays the queue) for Chromium; lib/offline-queue.ts's online-event fallback covers Safari/Firefox, which never fire that event at all. Both replay paths read/write the same IndexedDB store so either one drains it. Also removes apps/web/public/manifest.json, a stale static manifest that layout.tsx used to link to before the previous commit pointed it at the real generated route (app/manifest.ts) -- it had gone unnoticed and unused since. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { enqueuePendingAction, getPendingActions, deletePendingAction, type PendingAction } from "@/lib/offline-db";
|
||||
|
||||
type ServiceWorkerRegistrationWithSync = ServiceWorkerRegistration & {
|
||||
sync?: { register: (tag: string) => Promise<void> };
|
||||
};
|
||||
|
||||
export const SYNC_TAG = "epicure-sync";
|
||||
|
||||
/** Queues a mutation that failed because the browser is offline. Tries to
|
||||
* register a Background Sync so the service worker replays it as soon as
|
||||
* connectivity returns (Chromium only); Safari/Firefox fall back to the
|
||||
* "online" event listener in OfflineSyncManager. */
|
||||
export async function queueOfflineAction(action: { method: string; url: string; body: unknown; description: string }): Promise<void> {
|
||||
await enqueuePendingAction(action);
|
||||
if ("serviceWorker" in navigator) {
|
||||
try {
|
||||
const registration = (await navigator.serviceWorker.ready) as ServiceWorkerRegistrationWithSync;
|
||||
await registration.sync?.register(SYNC_TAG);
|
||||
} catch {
|
||||
// Background Sync unsupported or registration failed — the
|
||||
// online-event fallback in OfflineSyncManager still covers this.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Replays queued actions in the order they were created, stopping at the
|
||||
* first failure so a dependent later action (e.g. add-items-to-a-just-
|
||||
* created-list) never runs ahead of the one it depends on. Returns how many
|
||||
* actions succeeded. Mirrors the service worker's own replay logic in
|
||||
* public/sw.js — kept for browsers with no Background Sync support. */
|
||||
export async function flushOfflineQueue(): Promise<number> {
|
||||
const pending = await getPendingActions();
|
||||
let synced = 0;
|
||||
for (const action of pending) {
|
||||
const ok = await replayAction(action);
|
||||
if (!ok) break;
|
||||
await deletePendingAction(action.id);
|
||||
synced += 1;
|
||||
}
|
||||
return synced;
|
||||
}
|
||||
|
||||
async function replayAction(action: PendingAction): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch(action.url, {
|
||||
method: action.method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(action.body),
|
||||
});
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user