/**
* Device-local room history (IndexedDB). Each device remembers the rooms it
* was in — messages plus recent image blobs — so rejoining alone restores
* your view, and you become a history source for everyone who joins later.
* Nothing here ever leaves the device except through the normal encrypted
* peer-to-peer history sync.
*
* Note: like the room list, this is stored readable-on-device — the room
* links in localStorage already carry the secrets, so at-rest encryption here
* would add nothing against someone who has the browser profile.
*/
export interface PersistMsg {
id: string;
from: string;
name: string;
ts: number;
seq: number;
text?: string;
thread?: string;
img?: { w: number; h: number; bytes: number };
rx?: [string, string[]][];
blob?: Blob; // present for the most recent images
}
export interface StoredRoom {
msgs: PersistMsg[];
mineIds: string[]; // messages authored on this device (for "mine" styling)
savedAt: number;
}
const DB_NAME = "sueta-history";
const STORE = "rooms";
const MAX_ROOMS = 50;
function openDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, 1);
req.onupgradeneeded = () => req.result.createObjectStore(STORE);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
export async function loadHistory(roomId: string): Promise<StoredRoom | null> {
try {
const db = await openDB();
return await new Promise((resolve) => {
const req = db.transaction(STORE).objectStore(STORE).get(roomId);
req.onsuccess = () => resolve((req.result as StoredRoom) ?? null);
req.onerror = () => resolve(null);
});
} catch {
return null; // no IDB (private mode) — history is session-only, app still works
}
}
export async function saveHistory(roomId: string, rec: StoredRoom): Promise<void> {
try {
const db = await openDB();
await new Promise<void>((resolve) => {
const tx = db.transaction(STORE, "readwrite");
tx.objectStore(STORE).put(rec, roomId);
tx.oncomplete = () => resolve();
tx.onerror = () => resolve();
});
void pruneOldRooms(db);
} catch {}
}
/** Keep the store bounded: drop the oldest rooms beyond MAX_ROOMS. */
async function pruneOldRooms(db: IDBDatabase): Promise<void> {
try {
const keys: string[] = [];
const stamps = new Map<string, number>();
await new Promise<void>((resolve) => {
const cur = db.transaction(STORE).objectStore(STORE).openCursor();
cur.onsuccess = () => {
const c = cur.result;
if (!c) return resolve();
keys.push(String(c.key));
stamps.set(String(c.key), (c.value as StoredRoom)?.savedAt ?? 0);
c.continue();
};
cur.onerror = () => resolve();
});
if (keys.length <= MAX_ROOMS) return;
keys.sort((a, b) => (stamps.get(a) ?? 0) - (stamps.get(b) ?? 0));
const tx = db.transaction(STORE, "readwrite");
for (const k of keys.slice(0, keys.length - MAX_ROOMS)) tx.objectStore(STORE).delete(k);
} catch {}
}