nexus/ui/src/hooks/useOfflineQueue.ts
Nexus Dev 77117d9fc0 feat(26): merge worktree code from plans 26-00, 26-01, 26-03
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>
2026-04-02 15:08:51 +00:00

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 };
}