Spaces:
Running
Running
| const DATABASE_NAME = 'bonsai-webgpu-chat'; | |
| const DATABASE_VERSION = 1; | |
| const MEMORY_STORE = 'memory'; | |
| const CONVERSATION_STORE = 'conversations'; | |
| export interface MemoryRecord { | |
| key: string; | |
| value: string; | |
| updatedAt: number; | |
| } | |
| export interface ConversationRecord<TMessage = unknown> { | |
| id: string; | |
| title: string; | |
| messages: TMessage[]; | |
| modelId: string; | |
| systemPrompt?: string; | |
| updatedAt: number; | |
| } | |
| function requestResult<T>(request: IDBRequest<T>): Promise<T> { | |
| return new Promise((resolve, reject) => { | |
| request.onsuccess = () => resolve(request.result); | |
| request.onerror = () => reject(request.error ?? new Error('IndexedDB request failed')); | |
| }); | |
| } | |
| function transactionDone(transaction: IDBTransaction): Promise<void> { | |
| return new Promise((resolve, reject) => { | |
| transaction.oncomplete = () => resolve(); | |
| transaction.onerror = () => reject(transaction.error ?? new Error('IndexedDB transaction failed')); | |
| transaction.onabort = () => reject(transaction.error ?? new Error('IndexedDB transaction aborted')); | |
| }); | |
| } | |
| async function openDatabase(): Promise<IDBDatabase> { | |
| if (!('indexedDB' in globalThis)) throw new Error('IndexedDB is unavailable in this browser'); | |
| const request = indexedDB.open(DATABASE_NAME, DATABASE_VERSION); | |
| request.onupgradeneeded = () => { | |
| const database = request.result; | |
| if (!database.objectStoreNames.contains(MEMORY_STORE)) { | |
| database.createObjectStore(MEMORY_STORE, { keyPath: 'key' }); | |
| } | |
| if (!database.objectStoreNames.contains(CONVERSATION_STORE)) { | |
| const store = database.createObjectStore(CONVERSATION_STORE, { keyPath: 'id' }); | |
| store.createIndex('updatedAt', 'updatedAt'); | |
| } | |
| }; | |
| return requestResult(request); | |
| } | |
| function validateIdentifier(value: string, label: string): string { | |
| const normalized = value.trim(); | |
| if (!normalized || normalized.length > 256) throw new Error(`${label} must contain 1–256 characters`); | |
| return normalized; | |
| } | |
| export async function putMemory(key: string, value: string): Promise<MemoryRecord> { | |
| const record = { key: validateIdentifier(key, 'memory key'), value: value.slice(0, 64 * 1024), updatedAt: Date.now() }; | |
| const database = await openDatabase(); | |
| const transaction = database.transaction(MEMORY_STORE, 'readwrite'); | |
| transaction.objectStore(MEMORY_STORE).put(record); | |
| await transactionDone(transaction); | |
| database.close(); | |
| return record; | |
| } | |
| export async function getMemory(key: string): Promise<MemoryRecord | undefined> { | |
| const database = await openDatabase(); | |
| const transaction = database.transaction(MEMORY_STORE, 'readonly'); | |
| const result = await requestResult(transaction.objectStore(MEMORY_STORE).get(validateIdentifier(key, 'memory key'))); | |
| await transactionDone(transaction); | |
| database.close(); | |
| return result as MemoryRecord | undefined; | |
| } | |
| export async function listMemory(): Promise<MemoryRecord[]> { | |
| const database = await openDatabase(); | |
| const transaction = database.transaction(MEMORY_STORE, 'readonly'); | |
| const result = await requestResult(transaction.objectStore(MEMORY_STORE).getAll()) as MemoryRecord[]; | |
| await transactionDone(transaction); | |
| database.close(); | |
| return result.sort((left, right) => right.updatedAt - left.updatedAt); | |
| } | |
| export async function deleteMemory(key: string): Promise<void> { | |
| const database = await openDatabase(); | |
| const transaction = database.transaction(MEMORY_STORE, 'readwrite'); | |
| transaction.objectStore(MEMORY_STORE).delete(validateIdentifier(key, 'memory key')); | |
| await transactionDone(transaction); | |
| database.close(); | |
| } | |
| export async function saveConversation<TMessage>(record: ConversationRecord<TMessage>): Promise<void> { | |
| const id = validateIdentifier(record.id, 'conversation id'); | |
| const database = await openDatabase(); | |
| const transaction = database.transaction(CONVERSATION_STORE, 'readwrite'); | |
| transaction.objectStore(CONVERSATION_STORE).put({ ...record, id, title: record.title.slice(0, 256), updatedAt: Date.now() }); | |
| await transactionDone(transaction); | |
| database.close(); | |
| } | |
| export async function loadConversation<TMessage>(id: string): Promise<ConversationRecord<TMessage> | undefined> { | |
| const database = await openDatabase(); | |
| const transaction = database.transaction(CONVERSATION_STORE, 'readonly'); | |
| const result = await requestResult(transaction.objectStore(CONVERSATION_STORE).get(validateIdentifier(id, 'conversation id'))); | |
| await transactionDone(transaction); | |
| database.close(); | |
| return result as ConversationRecord<TMessage> | undefined; | |
| } | |
| export async function listConversations<TMessage>(): Promise<ConversationRecord<TMessage>[]> { | |
| const database = await openDatabase(); | |
| const transaction = database.transaction(CONVERSATION_STORE, 'readonly'); | |
| const result = await requestResult(transaction.objectStore(CONVERSATION_STORE).getAll()) as ConversationRecord<TMessage>[]; | |
| await transactionDone(transaction); | |
| database.close(); | |
| return result.sort((left, right) => right.updatedAt - left.updatedAt); | |
| } | |
| export async function deleteConversation(id: string): Promise<void> { | |
| const database = await openDatabase(); | |
| const transaction = database.transaction(CONVERSATION_STORE, 'readwrite'); | |
| transaction.objectStore(CONVERSATION_STORE).delete(validateIdentifier(id, 'conversation id')); | |
| await transactionDone(transaction); | |
| database.close(); | |
| } | |