SW cache-first rewrite, React.lazy code splitting, PWA types/test stubs, install prompt, offline banner, offline queue, ChatPanel wiring. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
96 lines
2.3 KiB
TypeScript
96 lines
2.3 KiB
TypeScript
import { openDB } from "idb";
|
|
import { useCallback, useEffect, useState } from "react";
|
|
import { chatApi } from "../api/chat";
|
|
|
|
const DB_NAME = "nexus-offline";
|
|
const STORE = "message_queue";
|
|
|
|
interface QueueEntry {
|
|
conversationId: string;
|
|
content: string;
|
|
queuedAt: number;
|
|
}
|
|
|
|
async function getDb() {
|
|
return openDB(DB_NAME, 1, {
|
|
upgrade(db) {
|
|
db.createObjectStore(STORE, { autoIncrement: true });
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Offline message queue backed by IndexedDB.
|
|
* Enqueues messages when offline; auto-flushes when the online event fires.
|
|
*/
|
|
export function useOfflineQueue(): {
|
|
enqueue: (conversationId: string, content: string) => Promise<void>;
|
|
flush: () => Promise<void>;
|
|
queuedCount: number;
|
|
} {
|
|
const [queuedCount, setQueuedCount] = useState(0);
|
|
|
|
// On mount, read current queue count from IndexedDB
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
getDb()
|
|
.then((db) => db.count(STORE))
|
|
.then((count) => {
|
|
if (!cancelled) setQueuedCount(count);
|
|
})
|
|
.catch(() => {
|
|
// IndexedDB unavailable (SSR or private browsing edge case) — ignore
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
const flush = useCallback(async () => {
|
|
let db;
|
|
try {
|
|
db = await getDb();
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
const allKeys = await db.getAllKeys(STORE);
|
|
for (const key of allKeys) {
|
|
const entry = (await db.get(STORE, key)) as QueueEntry | undefined;
|
|
if (!entry) continue;
|
|
try {
|
|
await chatApi.postMessage(entry.conversationId, {
|
|
role: "user",
|
|
content: entry.content,
|
|
});
|
|
await db.delete(STORE, key);
|
|
setQueuedCount((c) => Math.max(0, c - 1));
|
|
} catch {
|
|
// Stop flushing on first failure — retry next time the online event fires
|
|
break;
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
const enqueue = useCallback(
|
|
async (conversationId: string, content: string) => {
|
|
let db;
|
|
try {
|
|
db = await getDb();
|
|
} catch {
|
|
return;
|
|
}
|
|
await db.add(STORE, { conversationId, content, queuedAt: Date.now() });
|
|
setQueuedCount((c) => c + 1);
|
|
},
|
|
[],
|
|
);
|
|
|
|
// Auto-flush when reconnected
|
|
useEffect(() => {
|
|
window.addEventListener("online", flush);
|
|
return () => window.removeEventListener("online", flush);
|
|
}, [flush]);
|
|
|
|
return { enqueue, flush, queuedCount };
|
|
}
|