Repository navigation
Expand file tree
/
Copy pathkv.ts
More file actions
68 lines (61 loc) · 3.42 KB
/
Copy pathkv.ts
File metadata and controls
68 lines (61 loc) · 3.42 KB
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
// Mock — small key/value stores on IndexedDB: `snapshots` (Parquet per table), `blobs`
// (recordings, clips, uploaded originals) and `secrets` (the Gemini key; never in snapshots or
// backups). `MemoryKv` stands in when IndexedDB is unavailable and in unit tests.
export interface Kv<T = unknown> {
get(key: string): Promise<T | null>;
put(key: string, value: T): Promise<void>;
delete(key: string): Promise<void>;
keys(prefix?: string): Promise<string[]>;
clear(): Promise<void>;
}
export class MemoryKv<T = unknown> implements Kv<T> {
private m = new Map<string, T>();
async get(key: string): Promise<T | null> { return this.m.has(key) ? this.m.get(key)! : null; }
async put(key: string, value: T): Promise<void> { this.m.set(key, value); }
async delete(key: string): Promise<void> { this.m.delete(key); }
async keys(prefix = ''): Promise<string[]> { return [...this.m.keys()].filter((k) => k.startsWith(prefix)).sort(); }
async clear(): Promise<void> { this.m.clear(); }
}
const DB_NAME = 'mock';
const DB_VERSION = 1;
export const STORES = ['snapshots', 'blobs', 'secrets'] as const;
export type StoreName = (typeof STORES)[number];
const req = <T,>(r: IDBRequest<T>): Promise<T> => new Promise((res, rej) => { r. => res(r.result); r. => rej(r.error); });
let opening: Promise<IDBDatabase> | null = null;
export function openIdb(): Promise<IDBDatabase> {
if (opening) return opening;
opening = new Promise<IDBDatabase>((res, rej) => {
if (typeof indexedDB === 'undefined') { rej(new Error('IndexedDB is not available')); return; }
const r = indexedDB.open(DB_NAME, DB_VERSION);
r. => { for (const s of STORES) if (!r.result.objectStoreNames.contains(s)) r.result.createObjectStore(s); };
r. => res(r.result);
r. => rej(r.error);
r. => rej(new Error('IndexedDB is blocked by another tab'));
});
opening.catch(() => { opening = null; });
return opening;
}
export class IdbKv<T = unknown> implements Kv<T> {
constructor(private db: IDBDatabase, private store: StoreName) {}
private tx(mode: IDBTransactionMode): IDBObjectStore { return this.db.transaction(this.store, mode).objectStore(this.store); }
async get(key: string): Promise<T | null> { const v = await req(this.tx('readonly').get(key)); return v === undefined ? null : (v as T); }
async put(key: string, value: T): Promise<void> { await req(this.tx('readwrite').put(value, key)); }
async delete(key: string): Promise<void> { await req(this.tx('readwrite').delete(key)); }
async keys(prefix = ''): Promise<string[]> {
const range = prefix ? IDBKeyRange.bound(prefix, prefix + '') : undefined;
return ((await req(this.tx('readonly').getAllKeys(range))) as IDBValidKey[]).map(String).sort();
}
async clear(): Promise<void> { await req(this.tx('readwrite').clear()); }
}
/** Writes several snapshot entries in one transaction, so a snapshot is never half-written. */
export async function putMany(db: IDBDatabase, store: StoreName, entries: [string, unknown][], remove: string[] = []): Promise<void> {
await new Promise<void>((res, rej) => {
const t = db.transaction(store, 'readwrite');
const s = t.objectStore(store);
for (const k of remove) s.delete(k);
for (const [k, v] of entries) s.put(v, k);
t. => res();
t. => rej(t.error);
t. => rej(t.error ?? new Error('snapshot write aborted'));
});
}