revert #957
This commit is contained in:
parent
eba9ee446a
commit
5e4a310c4e
4 changed files with 267 additions and 364 deletions
|
|
@ -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<string, Map<string, Map<IDBValidKey, unknown>>>();
|
||||
|
||||
interface MemoryStore {
|
||||
__memoryStore: true;
|
||||
get<T>(key: IDBValidKey): T | undefined;
|
||||
put(value: unknown, key: IDBValidKey): void;
|
||||
delete(key: IDBValidKey): void;
|
||||
clear(): void;
|
||||
getAllKeys<KeyType extends IDBValidKey>(): KeyType[];
|
||||
getAll<T>(): T[];
|
||||
entries<KeyType extends IDBValidKey, ValueType>(): [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<T>(promise: Promise<T>, fallback: T): Promise<T> {
|
||||
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<IDBValidKey, unknown>();
|
||||
database.set(storeName, data);
|
||||
}
|
||||
|
||||
return {
|
||||
__memoryStore: true,
|
||||
get: <T>(key: IDBValidKey) => data.get(key) as T | undefined,
|
||||
put: (value, key) => { data.set(key, value); },
|
||||
delete: key => { data.delete(key); },
|
||||
clear: () => { data.clear(); },
|
||||
getAllKeys: <KeyType extends IDBValidKey>() => Array.from(data.keys()) as KeyType[],
|
||||
getAll: <T>() => Array.from(data.values()) as T[],
|
||||
entries: <KeyType extends IDBValidKey, ValueType>() => Array.from(data.entries()) as [KeyType, ValueType][]
|
||||
};
|
||||
}
|
||||
|
||||
function isMemoryStore(store: StoreLike): store is MemoryStore {
|
||||
return "__memoryStore" in store;
|
||||
}
|
||||
|
||||
export function promisifyRequest<T = undefined>(
|
||||
request: IDBRequest<T> | IDBTransaction,
|
||||
): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const target = request as IDBRequest<T> & 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 = <T>(
|
||||
txMode: IDBTransactionMode,
|
||||
callback: (store: StoreLike) => T | PromiseLike<T>,
|
||||
callback: (store: IDBObjectStore) => T | PromiseLike<T>,
|
||||
) => Promise<T>;
|
||||
|
||||
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<T = any>(
|
||||
key: IDBValidKey,
|
||||
customStore = defaultGetStore(),
|
||||
): Promise<T | undefined> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<T = any>(
|
||||
keys: IDBValidKey[],
|
||||
customStore = defaultGetStore(),
|
||||
): Promise<T[]> {
|
||||
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<T = any>(
|
||||
key: IDBValidKey,
|
||||
updater: (oldValue: T | undefined) => T,
|
||||
customStore = defaultGetStore(),
|
||||
): Promise<void> {
|
||||
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<T = any>(
|
|||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<KeyType extends IDBValidKey>(
|
||||
customStore = defaultGetStore(),
|
||||
): Promise<KeyType[]> {
|
||||
return withFailureFallback(customStore("readonly", store => {
|
||||
if (isMemoryStore(store)) return store.getAllKeys<KeyType>();
|
||||
|
||||
return customStore("readonly", store => {
|
||||
// Fast path for modern browsers
|
||||
if (store.getAllKeys) {
|
||||
return promisifyRequest(
|
||||
store.getAllKeys() as unknown as IDBRequest<KeyType[]>,
|
||||
|
|
@ -253,45 +221,59 @@ export function keys<KeyType extends IDBValidKey>(
|
|||
}
|
||||
|
||||
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<T = any>(customStore = defaultGetStore()): Promise<T[]> {
|
||||
return withFailureFallback(customStore("readonly", store => {
|
||||
if (isMemoryStore(store)) return store.getAll<T>();
|
||||
|
||||
return customStore("readonly", store => {
|
||||
// Fast path for modern browsers
|
||||
if (store.getAll) {
|
||||
return promisifyRequest(store.getAll() as IDBRequest<T[]>);
|
||||
}
|
||||
|
||||
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<KeyType extends IDBValidKey, ValueType = any>(
|
||||
customStore = defaultGetStore(),
|
||||
): Promise<[KeyType, ValueType][]> {
|
||||
return withFailureFallback(customStore("readonly", store => {
|
||||
if (isMemoryStore(store)) return store.entries<KeyType, ValueType>();
|
||||
|
||||
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<KeyType[]>,
|
||||
),
|
||||
promisifyRequest(store.getAll() as IDBRequest<ValueType[]>),
|
||||
]).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),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<MLIDB> | null = null;
|
||||
let dbPromise: Promise<IDBPDatabase<MLIDB> | null> | null = null;
|
||||
let loggedFailure = false;
|
||||
|
||||
export let db: IDBPDatabase<MLIDB>;
|
||||
export const cachedMessages = new Map<string, LoggedMessageJSON>();
|
||||
|
||||
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<MLIDB>(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<T>(fallback: T, callback: (database: IDBPDatabase<MLIDB>) => Promise<T>): Promise<T> {
|
||||
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<MLIDB>(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<DBMessageRecord | null>(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.",
|
||||
|
|
|
|||
|
|
@ -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<IDBDatabase | null> | undefined;
|
||||
let loggedFailure = false;
|
||||
function openDB(): Promise<IDBDatabase> {
|
||||
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<IDBDatabase | null> {
|
||||
if (!openDbPromise) {
|
||||
openDbPromise = new Promise<IDBDatabase>((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<T>(fallback: T, callback: (db: IDBDatabase) => Promise<T>): Promise<T> {
|
||||
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<void> {
|
||||
|
|
@ -309,17 +279,19 @@ export async function clearTtsCache(): Promise<void> {
|
|||
}
|
||||
ttsCache.clear();
|
||||
|
||||
await withDb<void>(void 0, db => new Promise<void>((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<void> {
|
||||
await withDb<void>(void 0, db => new Promise<void>((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<Blob | null> {
|
||||
return withDb<Blob | null>(null, db => new Promise<Blob | null>((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<Blob | nul
|
|||
resolve(blob);
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
export async function setCachedVoiceInDB(cacheKey: string, blob: Blob): Promise<void> {
|
||||
await withDb<void>(void 0, db => new Promise<void>((resolve, reject) => {
|
||||
const db = await openDB();
|
||||
await new Promise<void>((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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue