diff --git a/src/api/DataStore/index.ts b/src/api/DataStore/index.ts index 3cdb50c7e..80ebb065f 100644 --- a/src/api/DataStore/index.ts +++ b/src/api/DataStore/index.ts @@ -18,97 +18,31 @@ * limitations under the License. */ -import { Logger } from "@utils/Logger"; - -const logger = new Logger("DataStore"); -let loggedFailure = false; -const memoryDatabases = new Map>>(); - -interface MemoryStore { - __memoryStore: true; - get(key: IDBValidKey): T | undefined; - put(value: unknown, key: IDBValidKey): void; - delete(key: IDBValidKey): void; - clear(): void; - getAllKeys(): KeyType[]; - getAll(): T[]; - entries(): [KeyType, ValueType][]; -} - -type StoreLike = IDBObjectStore | MemoryStore; - -function handleFailure(error: unknown) { - if (loggedFailure) return; - loggedFailure = true; - logger.error("IndexedDB unavailable, using empty storage.", error); -} - -function withFailureFallback(promise: Promise, fallback: T): Promise { - return promise.catch(error => { - handleFailure(error); - return fallback; - }); -} - -function getMemoryStore(dbName: string, storeName: string): MemoryStore { - let database = memoryDatabases.get(dbName); - if (!database) { - database = new Map(); - memoryDatabases.set(dbName, database); - } - - let data = database.get(storeName); - if (!data) { - data = new Map(); - database.set(storeName, data); - } - - return { - __memoryStore: true, - get: (key: IDBValidKey) => data.get(key) as T | undefined, - put: (value, key) => { data.set(key, value); }, - delete: key => { data.delete(key); }, - clear: () => { data.clear(); }, - getAllKeys: () => Array.from(data.keys()) as KeyType[], - getAll: () => Array.from(data.values()) as T[], - entries: () => Array.from(data.entries()) as [KeyType, ValueType][] - }; -} - -function isMemoryStore(store: StoreLike): store is MemoryStore { - return "__memoryStore" in store; -} - export function promisifyRequest( request: IDBRequest | IDBTransaction, ): Promise { return new Promise((resolve, reject) => { - const target = request as IDBRequest & IDBTransaction; - target.oncomplete = target.onsuccess = () => resolve(target.result); - target.onabort = target.onerror = () => reject(target.error); + // @ts-expect-error - file size hacks + request.oncomplete = request.onsuccess = () => resolve(request.result); + // @ts-expect-error - file size hacks + request.onabort = request.onerror = () => reject(request.error); }); } export function createStore(dbName: string, storeName: string): UseStore { const request = indexedDB.open(dbName); request.onupgradeneeded = () => request.result.createObjectStore(storeName); - const dbp = promisifyRequest(request).catch(error => { - handleFailure(error); - return null; - }); - const memoryStore = getMemoryStore(dbName, storeName); + const dbp = promisifyRequest(request); - return async (txMode, callback) => { - const db = await dbp; - if (!db) return callback(memoryStore); - - return callback(db.transaction(storeName, txMode).objectStore(storeName)); - }; + return (txMode, callback) => + dbp.then(db => + callback(db.transaction(storeName, txMode).objectStore(storeName)), + ); } export type UseStore = ( txMode: IDBTransactionMode, - callback: (store: StoreLike) => T | PromiseLike, + callback: (store: IDBObjectStore) => T | PromiseLike, ) => Promise; let defaultGetStoreFunc: UseStore | undefined; @@ -120,68 +54,88 @@ function defaultGetStore() { return defaultGetStoreFunc; } +/** + * Get a value by its key. + * + * @param key + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function get( key: IDBValidKey, customStore = defaultGetStore(), ): Promise { - return withFailureFallback(customStore("readonly", store => { - if (isMemoryStore(store)) return store.get(key) as T | undefined; - return promisifyRequest(store.get(key)); - }), undefined); + return customStore("readonly", store => promisifyRequest(store.get(key))); } +/** + * Set a value with a key. + * + * @param key + * @param value + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function set( key: IDBValidKey, value: any, customStore = defaultGetStore(), ): Promise { - return withFailureFallback(customStore("readwrite", store => { + return customStore("readwrite", store => { store.put(value, key); - if (isMemoryStore(store)) return; - return promisifyRequest(store.transaction); - }), undefined); + }); } +/** + * Set multiple values at once. This is faster than calling set() multiple times. + * It's also atomic – if one of the pairs can't be added, none will be added. + * + * @param entries Array of entries, where each entry is an array of `[key, value]`. + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function setMany( entries: [IDBValidKey, any][], customStore = defaultGetStore(), ): Promise { - return withFailureFallback(customStore("readwrite", store => { + return customStore("readwrite", store => { entries.forEach(entry => store.put(entry[1], entry[0])); - if (isMemoryStore(store)) return; - return promisifyRequest(store.transaction); - }), undefined); + }); } +/** + * Get multiple values by their keys + * + * @param keys + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function getMany( keys: IDBValidKey[], customStore = defaultGetStore(), ): Promise { - return withFailureFallback(customStore("readonly", store => { - if (isMemoryStore(store)) { - return keys.map(key => store.get(key) as T | undefined); - } - - return Promise.all(keys.map(key => promisifyRequest(store.get(key)))); - }), []); + return customStore("readonly", store => + Promise.all(keys.map(key => promisifyRequest(store.get(key)))), + ); } +/** + * Update a value. This lets you see the old value and update it as an atomic operation. + * + * @param key + * @param updater A callback that takes the old value and returns a new value. + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function update( key: IDBValidKey, updater: (oldValue: T | undefined) => T, customStore = defaultGetStore(), ): Promise { - return withFailureFallback(customStore( + return customStore( "readwrite", - store => { - if (isMemoryStore(store)) { - store.put(updater(store.get(key) as T | undefined), key); - return; - } - - return new Promise((resolve, reject) => { + store => + // Need to create the promise manually. + // If I try to chain promises, the transaction closes in browsers + // that use a promise polyfill (IE10/11). + new Promise((resolve, reject) => { store.get(key).onsuccess = function () { try { store.put(updater(this.result), key); @@ -190,42 +144,52 @@ export function update( reject(err); } }; - }); - }, - ), undefined); + }), + ); } +/** + * Delete a particular key from the store. + * + * @param key + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function del( key: IDBValidKey, customStore = defaultGetStore(), ): Promise { - return withFailureFallback(customStore("readwrite", store => { + return customStore("readwrite", store => { store.delete(key); - if (isMemoryStore(store)) return; - return promisifyRequest(store.transaction); - }), undefined); + }); } +/** + * Delete multiple keys at once. + * + * @param keys List of keys to delete. + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function delMany( keys: IDBValidKey[], customStore = defaultGetStore(), ): Promise { - return withFailureFallback(customStore("readwrite", store => { - keys.forEach(key => store.delete(key)); - if (isMemoryStore(store)) return; - + return customStore("readwrite", (store: IDBObjectStore) => { + keys.forEach((key: IDBValidKey) => store.delete(key)); return promisifyRequest(store.transaction); - }), undefined); + }); } +/** + * Clear all values in the store. + * + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function clear(customStore = defaultGetStore()): Promise { - return withFailureFallback(customStore("readwrite", store => { + return customStore("readwrite", store => { store.clear(); - if (isMemoryStore(store)) return; - return promisifyRequest(store.transaction); - }), undefined); + }); } function eachCursor( @@ -240,12 +204,16 @@ function eachCursor( return promisifyRequest(store.transaction); } +/** + * Get all keys in the store. + * + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function keys( customStore = defaultGetStore(), ): Promise { - return withFailureFallback(customStore("readonly", store => { - if (isMemoryStore(store)) return store.getAllKeys(); - + return customStore("readonly", store => { + // Fast path for modern browsers if (store.getAllKeys) { return promisifyRequest( store.getAllKeys() as unknown as IDBRequest, @@ -253,45 +221,59 @@ export function keys( } const items: KeyType[] = []; + return eachCursor(store, cursor => items.push(cursor.key as KeyType), ).then(() => items); - }), [] as KeyType[]); + }); } +/** + * Get all values in the store. + * + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function values(customStore = defaultGetStore()): Promise { - return withFailureFallback(customStore("readonly", store => { - if (isMemoryStore(store)) return store.getAll(); - + return customStore("readonly", store => { + // Fast path for modern browsers if (store.getAll) { return promisifyRequest(store.getAll() as IDBRequest); } const items: T[] = []; + return eachCursor(store, cursor => items.push(cursor.value as T)).then( () => items, ); - }), [] as T[]); + }); } +/** + * Get all entries in the store. Each entry is an array of `[key, value]`. + * + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function entries( customStore = defaultGetStore(), ): Promise<[KeyType, ValueType][]> { - return withFailureFallback(customStore("readonly", store => { - if (isMemoryStore(store)) return store.entries(); - + return customStore("readonly", store => { + // Fast path for modern browsers + // (although, hopefully we'll get a simpler path some day) if (store.getAll && store.getAllKeys) { return Promise.all([ promisifyRequest( store.getAllKeys() as unknown as IDBRequest, ), promisifyRequest(store.getAll() as IDBRequest), - ]).then(([keys, values]) => keys.map((key, i) => [key, values[i]] as [KeyType, ValueType])); + ]).then(([keys, values]) => keys.map((key, i) => [key, values[i]])); } const items: [KeyType, ValueType][] = []; - return eachCursor(store, cursor => - items.push([cursor.key as KeyType, cursor.value]), - ).then(() => items); - }), [] as [KeyType, ValueType][]); + + return customStore("readonly", store => + eachCursor(store, cursor => + items.push([cursor.key as KeyType, cursor.value]), + ).then(() => items), + ); + }); } diff --git a/src/equicordplugins/messageLoggerEnhanced/LoggedMessageManager.ts b/src/equicordplugins/messageLoggerEnhanced/LoggedMessageManager.ts index 7f51572af..5408349e9 100644 --- a/src/equicordplugins/messageLoggerEnhanced/LoggedMessageManager.ts +++ b/src/equicordplugins/messageLoggerEnhanced/LoggedMessageManager.ts @@ -17,7 +17,7 @@ */ import { Flogger, settings } from "."; -import { addMessageIDB, countMessagesIDB, DBMessageStatus, deleteMessagesBulkIDB, getOlderThanTimestampForGuildsIDB, getOldestMessagesIDB } from "./db"; +import { addMessageIDB, db, DBMessageStatus, deleteMessagesBulkIDB, getOlderThanTimestampForGuildsIDB, getOldestMessagesIDB } from "./db"; import { LoggedMessage, LoggedMessageJSON } from "./types"; import { cleanupMessage } from "./utils"; import { cacheMessageImages } from "./utils/saveImage"; @@ -47,7 +47,7 @@ export const addMessage = async (message: LoggedMessage | LoggedMessageJSON, sta } if (settings.store.messageLimit > 0) { - const currentMessageCount = await countMessagesIDB(); + const currentMessageCount = await db.count("messages"); if (currentMessageCount > settings.store.messageLimit) { const messagesToDelete = currentMessageCount - settings.store.messageLimit; if (messagesToDelete <= 0 || messagesToDelete >= settings.store.messageLimit) return; diff --git a/src/equicordplugins/messageLoggerEnhanced/db.ts b/src/equicordplugins/messageLoggerEnhanced/db.ts index 4fae28348..8d80e0213 100644 --- a/src/equicordplugins/messageLoggerEnhanced/db.ts +++ b/src/equicordplugins/messageLoggerEnhanced/db.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { Logger } from "@utils/Logger"; import { ChannelStore, Toasts } from "@webpack/common"; import { DBSchema, IDBPDatabase, openDB } from "idb"; @@ -38,63 +37,17 @@ export interface MLIDB extends DBSchema { by_timestamp_and_message_id: [string, string]; }; }; + } -const logger = new Logger("MessageLoggerEnhanced"); -let db: IDBPDatabase | null = null; -let dbPromise: Promise | null> | null = null; -let loggedFailure = false; - +export let db: IDBPDatabase; export const cachedMessages = new Map(); -function handleDbFailure(error: unknown) { - if (loggedFailure) return; - loggedFailure = true; - logger.error("Message storage unavailable, using memory only.", error); -} - -async function ensureDb() { - if (db) return db; - if (!dbPromise) { - dbPromise = openDB(DB_NAME, DB_VERSION, { - upgrade(database) { - const messageStore = database.createObjectStore("messages", { keyPath: "message_id" }); - messageStore.createIndex("by_channel_id", "channel_id"); - messageStore.createIndex("by_status", "status"); - messageStore.createIndex("by_timestamp", "message.timestamp"); - messageStore.createIndex("by_timestamp_and_message_id", ["channel_id", "message.timestamp"]); - } - }).then(database => { - db = database; - return database; - }).catch(error => { - handleDbFailure(error); - return null; - }); - } - - db = await dbPromise; - return db; -} - -async function withDb(fallback: T, callback: (database: IDBPDatabase) => Promise): Promise { - const database = await ensureDb(); - if (!database) return fallback; - - try { - return await callback(database); - } catch (error) { - handleDbFailure(error); - return fallback; - } -} - -async function cacheRecords(records: DBMessageRecord[], cacheAttachmentBlobs = true) { +// this is probably not the best way to do this +async function cacheRecords(records: DBMessageRecord[]) { for (const r of records) { cacheRecord(r); - if (!cacheAttachmentBlobs) continue; - for (const att of r.message.attachments) { const blobUrl = await getAttachmentBlobUrl(att); if (blobUrl) { @@ -103,7 +56,6 @@ async function cacheRecords(records: DBMessageRecord[], cacheAttachmentBlobs = t } } } - return records; } @@ -116,56 +68,56 @@ async function cacheRecord(record?: DBMessageRecord | null) { } export async function initIDB() { - await ensureDb(); + db = await openDB(DB_NAME, DB_VERSION, { + upgrade(db) { + const messageStore = db.createObjectStore("messages", { keyPath: "message_id" }); + messageStore.createIndex("by_channel_id", "channel_id"); + messageStore.createIndex("by_status", "status"); + messageStore.createIndex("by_timestamp", "message.timestamp"); + messageStore.createIndex("by_timestamp_and_message_id", ["channel_id", "message.timestamp"]); + } + }); } -void initIDB(); +initIDB(); export async function hasMessageIDB(message_id: string) { - if (cachedMessages.has(message_id)) return true; - return withDb(false, database => database.count("messages", message_id).then(count => count > 0)); + return cachedMessages.has(message_id) || (await db.count("messages", message_id)) > 0; } export async function countMessagesIDB() { - if (!db) return cachedMessages.size; return db.count("messages"); } export async function countMessagesByStatusIDB(status: DBMessageStatus) { - return withDb(0, database => database.countFromIndex("messages", "by_status", status)); + return db.countFromIndex("messages", "by_status", status); } export async function getAllMessagesIDB() { - return withDb([], async database => cacheRecords(await database.getAll("messages"))); + return cacheRecords(await db.getAll("messages")); } export async function getMessagesForChannelIDB(channel_id: string) { - return withDb([], async database => cacheRecords(await database.getAllFromIndex("messages", "by_channel_id", channel_id))); + return cacheRecords(await db.getAllFromIndex("messages", "by_channel_id", channel_id)); } export async function getMessageIDB(message_id: string) { - return withDb(null, async database => { - const record = await database.get("messages", message_id); - await cacheRecord(record); - return record ?? null; - }); + return cacheRecord(await db.get("messages", message_id)); } export async function getMessagesByStatusIDB(status: DBMessageStatus) { - return withDb([], async database => cacheRecords(await database.getAllFromIndex("messages", "by_status", status))); + return cacheRecords(await db.getAllFromIndex("messages", "by_status", status)); } export async function getOldestMessagesIDB(limit: number) { - return withDb([], async database => cacheRecords(await database.getAllFromIndex("messages", "by_timestamp", undefined, limit))); + return cacheRecords(await db.getAllFromIndex("messages", "by_timestamp", undefined, limit)); } export async function* iterateAllMessagesIDB(batchSize = 100) { - if (!db && !(await ensureDb())) return; - let lastId: string | undefined; while (true) { const batch: DBMessageRecord[] = []; - - const tx = db!.transaction("messages"); + // new transaction for each batch to avoid timeouts during yield + const tx = db.transaction("messages"); const range = lastId ? IDBKeyRange.lowerBound(lastId, true) : undefined; let cursor = await tx.store.openCursor(range); @@ -177,6 +129,7 @@ export async function* iterateAllMessagesIDB(batchSize = 100) { if (batch.length === 0) break; lastId = batch[batch.length - 1].message_id; + yield await cacheRecords(batch); if (batch.length < batchSize) break; @@ -184,20 +137,22 @@ export async function* iterateAllMessagesIDB(batchSize = 100) { } export async function getOlderThanTimestampIDB(timestamp: string) { - return withDb([], async database => { - const tx = database.transaction("messages", "readonly"); - const index = tx.store.index("by_timestamp"); - const cursor = await index.openCursor(IDBKeyRange.upperBound(timestamp)); + const tx = db.transaction("messages", "readonly"); + const { store } = tx; + const index = store.index("by_timestamp"); - if (!cursor) return []; + const cursor = await index.openCursor(IDBKeyRange.upperBound(timestamp)); - const messages: DBMessageRecord[] = []; - for await (const c of cursor) { - messages.push(c.value); - } + if (!cursor) { + return []; + } - return cacheRecords(messages, false); - }); + const messages: DBMessageRecord[] = []; + for await (const c of cursor) { + messages.push(c.value); + } + + return cacheRecords(messages); } export async function getOlderThanTimestampForGuildsIDB(timestamp: string, currentChannelId?: string, preserveCurrentChannel?: boolean) { @@ -212,104 +167,96 @@ export async function getOlderThanTimestampForGuildsIDB(timestamp: string, curre } export async function getDateStortedMessagesByStatusIDB(newest: boolean, limit: number, status: DBMessageStatus) { - return withDb([], async database => { - const tx = database.transaction("messages", "readonly"); - const index = tx.store.index("by_status"); - const direction = newest ? "prev" : "next"; - const cursor = await index.openCursor(IDBKeyRange.only(status), direction); + const tx = db.transaction("messages", "readonly"); + const { store } = tx; + const index = store.index("by_status"); - if (!cursor) { - return []; - } + const direction = newest ? "prev" : "next"; + const cursor = await index.openCursor(IDBKeyRange.only(status), direction); - const messages: DBMessageRecord[] = []; - for await (const c of cursor) { - messages.push(c.value); - if (messages.length >= limit) break; - } + if (!cursor) { + console.log("No messages found"); + return []; + } - return cacheRecords(messages); - }); + const messages: DBMessageRecord[] = []; + for await (const c of cursor) { + messages.push(c.value); + if (messages.length >= limit) break; + } + + return cacheRecords(messages); } export async function getMessagesByChannelAndAfterTimestampIDB(channel_id: string, start: string) { - return withDb([], async database => { - const tx = database.transaction("messages", "readonly"); - const index = tx.store.index("by_timestamp_and_message_id"); - const cursor = await index.openCursor(IDBKeyRange.bound([channel_id, start], [channel_id, "\uffff"])); + const tx = db.transaction("messages", "readonly"); + const { store } = tx; + const index = store.index("by_timestamp_and_message_id"); - if (!cursor) { - return []; - } + const cursor = await index.openCursor(IDBKeyRange.bound([channel_id, start], [channel_id, "\uffff"])); - const messages: DBMessageRecord[] = []; - for await (const c of cursor) { - messages.push(c.value); - } + if (!cursor) { + console.log("No messages found in range"); + return []; + } - return cacheRecords(messages, false); - }); + const messages: DBMessageRecord[] = []; + for await (const c of cursor) { + messages.push(c.value); + } + + return cacheRecords(messages); } export async function addMessageIDB(message: LoggedMessageJSON, status: DBMessageStatus) { stripTransientRenderState(message); - cachedMessages.set(message.id, message); - return withDb(void 0, async database => { - await database.put("messages", { - channel_id: message.channel_id, - message_id: message.id, - status, - message, - }); + await db.put("messages", { + channel_id: message.channel_id, + message_id: message.id, + status, + message, }); + + cachedMessages.set(message.id, message); } export async function addMessagesBulkIDB(messages: LoggedMessageJSON[], status?: DBMessageStatus) { messages.forEach(stripTransientRenderState); + + const tx = db.transaction("messages", "readwrite"); + const { store } = tx; + + await Promise.all([ + ...messages.map(message => store.add({ + channel_id: message.channel_id, + message_id: message.id, + status: status ?? getMessageStatus(message), + message, + })), + tx.done + ]); + messages.forEach(message => cachedMessages.set(message.id, message)); - - return withDb(void 0, async database => { - const tx = database.transaction("messages", "readwrite"); - const { store } = tx; - - await Promise.all([ - ...messages.map(message => store.add({ - channel_id: message.channel_id, - message_id: message.id, - status: status ?? getMessageStatus(message), - message, - })), - tx.done - ]); - }); } export async function deleteMessageIDB(message_id: string) { - cachedMessages.delete(message_id); + await db.delete("messages", message_id); - return withDb(void 0, database => database.delete("messages", message_id)); + cachedMessages.delete(message_id); } export async function deleteMessagesBulkIDB(message_ids: string[]) { - message_ids.forEach(id => cachedMessages.delete(id)); + const tx = db.transaction("messages", "readwrite"); + const { store } = tx; - return withDb(void 0, async database => { - const tx = database.transaction("messages", "readwrite"); - const { store } = tx; - await Promise.all([ - ...message_ids.map(id => store.delete(id)), - tx.done - ]); - }); + await Promise.all([...message_ids.map(id => store.delete(id)), tx.done]); + message_ids.forEach(id => cachedMessages.delete(id)); } -export async function clearMessagesIDB(showToast = true) { +export async function clearMessagesIDB() { + await db.clear("messages"); cachedMessages.clear(); - await withDb(void 0, database => database.clear("messages")); - - if (!showToast) return; - Toasts.show({ type: Toasts.Type.MESSAGE, message: "Cleared message log database and cache.", diff --git a/src/equicordplugins/vcNarratorCustom/util.ts b/src/equicordplugins/vcNarratorCustom/util.ts index 476d2d2e0..5d27f5e3e 100644 --- a/src/equicordplugins/vcNarratorCustom/util.ts +++ b/src/equicordplugins/vcNarratorCustom/util.ts @@ -198,9 +198,6 @@ const DB_VERSION = 2; const VOICES_STORE = "voices"; -import { Logger } from "@utils/Logger"; - -const logger = new Logger("VcNarratorCustom"); const META_STORE = "voices_meta"; type VoiceMeta = { @@ -209,76 +206,49 @@ type VoiceMeta = { lastAccess: number; }; -let openDbPromise: Promise | undefined; -let loggedFailure = false; +function openDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + request.onupgradeneeded = event => { + const db = request.result; + const upgradeTx = request.transaction; + if (!db.objectStoreNames.contains(VOICES_STORE)) { + db.createObjectStore(VOICES_STORE); + } + if (!db.objectStoreNames.contains(META_STORE)) { + const meta = db.createObjectStore(META_STORE); + meta.createIndex("by_lastAccess", "lastAccess"); + } -function handleDbFailure(error: unknown) { - if (loggedFailure) return; - loggedFailure = true; - logger.error("Voice cache storage unavailable, falling back to memory.", error); -} - -function openDB(): Promise { - if (!openDbPromise) { - openDbPromise = new Promise((resolve, reject) => { - const request = indexedDB.open(DB_NAME, DB_VERSION); - request.onupgradeneeded = event => { - const db = request.result; - const upgradeTx = request.transaction; - if (!db.objectStoreNames.contains(VOICES_STORE)) { - db.createObjectStore(VOICES_STORE); - } - if (!db.objectStoreNames.contains(META_STORE)) { - const meta = db.createObjectStore(META_STORE); - meta.createIndex("by_lastAccess", "lastAccess"); - } - - if (upgradeTx && (event?.oldVersion ?? 0) < 2) { - const now = Date.now(); - const voices = upgradeTx.objectStore(VOICES_STORE); - const meta = upgradeTx.objectStore(META_STORE); - const cursorReq = voices.openCursor(); - cursorReq.onsuccess = () => { - const cursor = cursorReq.result; - if (!cursor) return; - const blob = cursor.value as Blob | undefined; - meta.put( - { - size: blob?.size ?? 0, - createdAt: now, - lastAccess: now, - } satisfies VoiceMeta, - String(cursor.key) - ); - cursor.continue(); - }; - } - }; - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(request.error); - }).then(db => db).catch(error => { - handleDbFailure(error); - return null; - }); - } - - return openDbPromise; -} - -async function withDb(fallback: T, callback: (db: IDBDatabase) => Promise): Promise { - const db = await openDB(); - if (!db) return fallback; - - try { - return await callback(db); - } catch (error) { - handleDbFailure(error); - return fallback; - } + if (upgradeTx && (event?.oldVersion ?? 0) < 2) { + const now = Date.now(); + const voices = upgradeTx.objectStore(VOICES_STORE); + const meta = upgradeTx.objectStore(META_STORE); + const cursorReq = voices.openCursor(); + cursorReq.onsuccess = () => { + const cursor = cursorReq.result; + if (!cursor) return; + const blob = cursor.value as Blob | undefined; + meta.put( + { + size: blob?.size ?? 0, + createdAt: now, + lastAccess: now, + } satisfies VoiceMeta, + String(cursor.key) + ); + cursor.continue(); + }; + } + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); } export async function getPersistentTtsCacheStats(): Promise<{ bytes: number; entries: number; }> { - return withDb<{ bytes: number; entries: number; }>({ bytes: 0, entries: 0 }, db => new Promise<{ bytes: number; entries: number; }>((resolve, reject) => { + const db = await openDB(); + return new Promise((resolve, reject) => { const tx = db.transaction(META_STORE, "readonly"); const store = tx.objectStore(META_STORE); @@ -298,7 +268,7 @@ export async function getPersistentTtsCacheStats(): Promise<{ bytes: number; ent cursor.continue(); }; cursorReq.onerror = () => reject(cursorReq.error); - })); + }); } export async function clearTtsCache(): Promise { @@ -309,17 +279,19 @@ export async function clearTtsCache(): Promise { } ttsCache.clear(); - await withDb(void 0, db => new Promise((resolve, reject) => { + const db = await openDB(); + return new Promise((resolve, reject) => { const tx = db.transaction([VOICES_STORE, META_STORE], "readwrite"); tx.objectStore(VOICES_STORE).clear(); tx.objectStore(META_STORE).clear(); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); - })); + }); } async function trimPersistentCacheToMaxBytes(maxBytes = PERSISTENT_TTS_CACHE_MAX_BYTES): Promise { - await withDb(void 0, db => new Promise((resolve, reject) => { + const db = await openDB(); + return new Promise((resolve, reject) => { const tx = db.transaction([VOICES_STORE, META_STORE], "readwrite"); const voicesStore = tx.objectStore(VOICES_STORE); const metaStore = tx.objectStore(META_STORE); @@ -372,11 +344,12 @@ async function trimPersistentCacheToMaxBytes(maxBytes = PERSISTENT_TTS_CACHE_MAX tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); - })); + }); } export async function getCachedVoiceFromDB(cacheKey: string): Promise { - return withDb(null, db => new Promise((resolve, reject) => { + const db = await openDB(); + return new Promise((resolve, reject) => { const tx = db.transaction([VOICES_STORE, META_STORE], "readwrite"); const voicesStore = tx.objectStore(VOICES_STORE); const metaStore = tx.objectStore(META_STORE); @@ -400,11 +373,12 @@ export async function getCachedVoiceFromDB(cacheKey: string): Promise reject(request.error); - })); + }); } export async function setCachedVoiceInDB(cacheKey: string, blob: Blob): Promise { - await withDb(void 0, db => new Promise((resolve, reject) => { + const db = await openDB(); + await new Promise((resolve, reject) => { const tx = db.transaction([VOICES_STORE, META_STORE], "readwrite"); const voicesStore = tx.objectStore(VOICES_STORE); const metaStore = tx.objectStore(META_STORE); @@ -418,7 +392,7 @@ export async function setCachedVoiceInDB(cacheKey: string, blob: Blob): Promise< tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); - })); + }); await trimPersistentCacheToMaxBytes(); }