File size: 5,351 Bytes
0ed8124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c62075
0ed8124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c62075
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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();
}