summaryrefslogtreecommitdiffhomepage
path: root/src/adapters/idb/index.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/adapters/idb/index.ts')
-rw-r--r--src/adapters/idb/index.ts292
1 files changed, 146 insertions, 146 deletions
diff --git a/src/adapters/idb/index.ts b/src/adapters/idb/index.ts
index 302edb5..96b2cbc 100644
--- a/src/adapters/idb/index.ts
+++ b/src/adapters/idb/index.ts
@@ -1,7 +1,7 @@
import type { StoredChunk } from "@dispatch/wire";
import type {
- ConversationCacheIndexEntry,
- ConversationChunkStore,
+ ConversationCacheIndexEntry,
+ ConversationChunkStore,
} from "../../features/conversation-cache";
const DEFAULT_DB_NAME = "dispatch-chunk-cache";
@@ -10,172 +10,172 @@ const CHUNKS_STORE = "chunks";
const META_STORE = "meta";
interface ChunkRecord {
- conversationId: string;
- seq: number;
- role: StoredChunk["role"];
- chunk: StoredChunk["chunk"];
+ conversationId: string;
+ seq: number;
+ role: StoredChunk["role"];
+ chunk: StoredChunk["chunk"];
}
interface MetaRecord {
- conversationId: string;
- lastAccess: number;
+ conversationId: string;
+ lastAccess: number;
}
export interface CreateIdbChunkStoreOptions {
- indexedDB?: IDBFactory;
- dbName?: string;
+ indexedDB?: IDBFactory;
+ dbName?: string;
}
function requestToPromise<T>(req: IDBRequest<T>): Promise<T> {
- return new Promise<T>((resolve, reject) => {
- req.onsuccess = () => resolve(req.result);
- req.onerror = () => reject(req.error);
- });
+ return new Promise<T>((resolve, reject) => {
+ req.onsuccess = () => resolve(req.result);
+ req.onerror = () => reject(req.error);
+ });
}
function txComplete(tx: IDBTransaction): Promise<void> {
- return new Promise<void>((resolve, reject) => {
- tx.oncomplete = () => resolve();
- tx.onerror = () => reject(tx.error);
- tx.onabort = () => reject(tx.error);
- });
+ return new Promise<void>((resolve, reject) => {
+ tx.oncomplete = () => resolve();
+ tx.onerror = () => reject(tx.error);
+ tx.onabort = () => reject(tx.error);
+ });
}
function openDb(idb: IDBFactory, dbName: string): Promise<IDBDatabase> {
- return new Promise<IDBDatabase>((resolve, reject) => {
- const req = idb.open(dbName, DB_VERSION);
-
- req.onupgradeneeded = () => {
- const db = req.result;
- if (!db.objectStoreNames.contains(CHUNKS_STORE)) {
- const store = db.createObjectStore(CHUNKS_STORE, {
- keyPath: ["conversationId", "seq"],
- });
- store.createIndex("byConversation", "conversationId");
- }
- if (!db.objectStoreNames.contains(META_STORE)) {
- db.createObjectStore(META_STORE, { keyPath: "conversationId" });
- }
- };
-
- req.onsuccess = () => resolve(req.result);
- req.onerror = () => reject(req.error);
- });
+ return new Promise<IDBDatabase>((resolve, reject) => {
+ const req = idb.open(dbName, DB_VERSION);
+
+ req.onupgradeneeded = () => {
+ const db = req.result;
+ if (!db.objectStoreNames.contains(CHUNKS_STORE)) {
+ const store = db.createObjectStore(CHUNKS_STORE, {
+ keyPath: ["conversationId", "seq"],
+ });
+ store.createIndex("byConversation", "conversationId");
+ }
+ if (!db.objectStoreNames.contains(META_STORE)) {
+ db.createObjectStore(META_STORE, { keyPath: "conversationId" });
+ }
+ };
+
+ req.onsuccess = () => resolve(req.result);
+ req.onerror = () => reject(req.error);
+ });
}
function keyRangeFor(conversationId: string): IDBKeyRange {
- const lower: [string, number] = [conversationId, 0];
- const upper: [string, number] = [conversationId, Number.POSITIVE_INFINITY];
- return IDBKeyRange.bound(lower, upper);
+ const lower: [string, number] = [conversationId, 0];
+ const upper: [string, number] = [conversationId, Number.POSITIVE_INFINITY];
+ return IDBKeyRange.bound(lower, upper);
}
function chunksToStoredChunks(records: ChunkRecord[]): StoredChunk[] {
- return records.map((r) => ({ seq: r.seq, role: r.role, chunk: r.chunk }));
+ return records.map((r) => ({ seq: r.seq, role: r.role, chunk: r.chunk }));
}
export function createIdbChunkStore(opts?: CreateIdbChunkStoreOptions): ConversationChunkStore {
- const idb = opts?.indexedDB ?? globalThis.indexedDB;
- const dbName = opts?.dbName ?? DEFAULT_DB_NAME;
-
- let dbPromise: Promise<IDBDatabase> | null = null;
-
- function getDb(): Promise<IDBDatabase> {
- if (dbPromise === null) {
- dbPromise = openDb(idb, dbName);
- }
- return dbPromise;
- }
-
- return {
- async load(conversationId: string): Promise<readonly StoredChunk[]> {
- const db = await getDb();
- const tx = db.transaction(CHUNKS_STORE, "readonly");
- const store = tx.objectStore(CHUNKS_STORE);
- const range = keyRangeFor(conversationId);
- const records = await requestToPromise<ChunkRecord[]>(store.getAll(range));
- await txComplete(tx);
-
- records.sort((a, b) => a.seq - b.seq);
- return chunksToStoredChunks(records);
- },
-
- async append(conversationId: string, chunks: readonly StoredChunk[]): Promise<void> {
- if (chunks.length === 0) return;
-
- const db = await getDb();
- const tx = db.transaction([CHUNKS_STORE, META_STORE], "readwrite");
- const chunkStore = tx.objectStore(CHUNKS_STORE);
- const metaStore = tx.objectStore(META_STORE);
-
- for (const c of chunks) {
- chunkStore.put({
- conversationId,
- seq: c.seq,
- role: c.role,
- chunk: c.chunk,
- } satisfies ChunkRecord);
- }
-
- metaStore.put({
- conversationId,
- lastAccess: Date.now(),
- } satisfies MetaRecord);
-
- await txComplete(tx);
- },
-
- async delete(conversationId: string): Promise<void> {
- const db = await getDb();
- const tx = db.transaction([CHUNKS_STORE, META_STORE], "readwrite");
- const chunkStore = tx.objectStore(CHUNKS_STORE);
- const metaStore = tx.objectStore(META_STORE);
-
- chunkStore.delete(keyRangeFor(conversationId));
- metaStore.delete(conversationId);
-
- await txComplete(tx);
- },
-
- async index(): Promise<readonly ConversationCacheIndexEntry[]> {
- const db = await getDb();
- const tx = db.transaction([CHUNKS_STORE, META_STORE], "readonly");
- const chunkStore = tx.objectStore(CHUNKS_STORE);
- const metaStore = tx.objectStore(META_STORE);
-
- const allChunks = await requestToPromise<ChunkRecord[]>(chunkStore.getAll());
- const allMeta = await requestToPromise<MetaRecord[]>(metaStore.getAll());
- await txComplete(tx);
-
- const metaMap = new Map<string, number>();
- for (const m of allMeta) {
- metaMap.set(m.conversationId, m.lastAccess);
- }
-
- const grouped = new Map<string, { chunkCount: number; maxSeq: number }>();
- for (const r of allChunks) {
- const existing = grouped.get(r.conversationId);
- if (existing === undefined) {
- grouped.set(r.conversationId, { chunkCount: 1, maxSeq: r.seq });
- } else {
- existing.chunkCount++;
- if (r.seq > existing.maxSeq) {
- existing.maxSeq = r.seq;
- }
- }
- }
-
- const result: ConversationCacheIndexEntry[] = [];
- for (const [conversationId, stats] of grouped) {
- const lastAccess = metaMap.get(conversationId);
- result.push({
- conversationId,
- chunkCount: stats.chunkCount,
- maxSeq: stats.maxSeq,
- ...(lastAccess !== undefined ? { lastAccess } : {}),
- });
- }
-
- return result;
- },
- };
+ const idb = opts?.indexedDB ?? globalThis.indexedDB;
+ const dbName = opts?.dbName ?? DEFAULT_DB_NAME;
+
+ let dbPromise: Promise<IDBDatabase> | null = null;
+
+ function getDb(): Promise<IDBDatabase> {
+ if (dbPromise === null) {
+ dbPromise = openDb(idb, dbName);
+ }
+ return dbPromise;
+ }
+
+ return {
+ async load(conversationId: string): Promise<readonly StoredChunk[]> {
+ const db = await getDb();
+ const tx = db.transaction(CHUNKS_STORE, "readonly");
+ const store = tx.objectStore(CHUNKS_STORE);
+ const range = keyRangeFor(conversationId);
+ const records = await requestToPromise<ChunkRecord[]>(store.getAll(range));
+ await txComplete(tx);
+
+ records.sort((a, b) => a.seq - b.seq);
+ return chunksToStoredChunks(records);
+ },
+
+ async append(conversationId: string, chunks: readonly StoredChunk[]): Promise<void> {
+ if (chunks.length === 0) return;
+
+ const db = await getDb();
+ const tx = db.transaction([CHUNKS_STORE, META_STORE], "readwrite");
+ const chunkStore = tx.objectStore(CHUNKS_STORE);
+ const metaStore = tx.objectStore(META_STORE);
+
+ for (const c of chunks) {
+ chunkStore.put({
+ conversationId,
+ seq: c.seq,
+ role: c.role,
+ chunk: c.chunk,
+ } satisfies ChunkRecord);
+ }
+
+ metaStore.put({
+ conversationId,
+ lastAccess: Date.now(),
+ } satisfies MetaRecord);
+
+ await txComplete(tx);
+ },
+
+ async delete(conversationId: string): Promise<void> {
+ const db = await getDb();
+ const tx = db.transaction([CHUNKS_STORE, META_STORE], "readwrite");
+ const chunkStore = tx.objectStore(CHUNKS_STORE);
+ const metaStore = tx.objectStore(META_STORE);
+
+ chunkStore.delete(keyRangeFor(conversationId));
+ metaStore.delete(conversationId);
+
+ await txComplete(tx);
+ },
+
+ async index(): Promise<readonly ConversationCacheIndexEntry[]> {
+ const db = await getDb();
+ const tx = db.transaction([CHUNKS_STORE, META_STORE], "readonly");
+ const chunkStore = tx.objectStore(CHUNKS_STORE);
+ const metaStore = tx.objectStore(META_STORE);
+
+ const allChunks = await requestToPromise<ChunkRecord[]>(chunkStore.getAll());
+ const allMeta = await requestToPromise<MetaRecord[]>(metaStore.getAll());
+ await txComplete(tx);
+
+ const metaMap = new Map<string, number>();
+ for (const m of allMeta) {
+ metaMap.set(m.conversationId, m.lastAccess);
+ }
+
+ const grouped = new Map<string, { chunkCount: number; maxSeq: number }>();
+ for (const r of allChunks) {
+ const existing = grouped.get(r.conversationId);
+ if (existing === undefined) {
+ grouped.set(r.conversationId, { chunkCount: 1, maxSeq: r.seq });
+ } else {
+ existing.chunkCount++;
+ if (r.seq > existing.maxSeq) {
+ existing.maxSeq = r.seq;
+ }
+ }
+ }
+
+ const result: ConversationCacheIndexEntry[] = [];
+ for (const [conversationId, stats] of grouped) {
+ const lastAccess = metaMap.get(conversationId);
+ result.push({
+ conversationId,
+ chunkCount: stats.chunkCount,
+ maxSeq: stats.maxSeq,
+ ...(lastAccess !== undefined ? { lastAccess } : {}),
+ });
+ }
+
+ return result;
+ },
+ };
}