|
| 1 | +const ALL_TABLES = ['stat'] as const |
| 2 | + |
| 3 | +export type Table = typeof ALL_TABLES[number] |
| 4 | + |
| 5 | +export type Key<T = Record<string, number>> = keyof T & string |
| 6 | + |
| 7 | +type IndexConfig<T = Record<string, unknown>> = { |
| 8 | + key: Key<T> | Key<T>[] |
| 9 | + unique?: boolean |
| 10 | +} |
| 11 | + |
| 12 | +export type Index<T = Record<string, unknown>> = Key<T> | Key<T>[] | IndexConfig<T> |
| 13 | + |
| 14 | +const DB_NAME = `tt4b_${chrome.runtime.id}` |
| 15 | + |
| 16 | +function normalizeIndex<T = Record<string, number>>(index: Index<T>): IndexConfig<T> { |
| 17 | + return typeof index === 'string' || Array.isArray(index) ? { key: index } : index |
| 18 | +} |
| 19 | + |
| 20 | +function formatIdxName<T = Record<string, number>>(key: IndexConfig<T>['key']): string { |
| 21 | + const keyStr = Array.isArray(key) ? key.sort().join('_') : key |
| 22 | + return `idx_${keyStr}` |
| 23 | +} |
| 24 | + |
| 25 | +export function req2Promise<T = unknown>(req: IDBRequest<T>): Promise<T | undefined> { |
| 26 | + return new Promise((resolve, reject) => { |
| 27 | + req.onsuccess = () => resolve(req.result) |
| 28 | + req.onerror = (ev) => { |
| 29 | + console.error("Failed to request indexed-db", ev, req.error) |
| 30 | + reject(req.error) |
| 31 | + } |
| 32 | + }) |
| 33 | +} |
| 34 | + |
| 35 | +export async function iterateCursor<T = unknown>( |
| 36 | + req: IDBRequest<IDBCursorWithValue | null> |
| 37 | +): Promise<readonly T[]> |
| 38 | +export async function iterateCursor<T = unknown>( |
| 39 | + req: IDBRequest<IDBCursorWithValue | null>, |
| 40 | + processor: (cursor: IDBCursorWithValue) => void | Promise<void> |
| 41 | +): Promise<void> |
| 42 | + |
| 43 | +export async function iterateCursor<T = unknown>( |
| 44 | + req: IDBRequest<IDBCursorWithValue | null>, |
| 45 | + processor?: (cursor: IDBCursorWithValue) => void | Promise<void> |
| 46 | +): Promise<readonly T[] | void> { |
| 47 | + const collectResults = !processor |
| 48 | + const results: T[] = [] |
| 49 | + |
| 50 | + return new Promise((resolve, reject) => { |
| 51 | + req.onsuccess = async () => { |
| 52 | + const cursor = req.result |
| 53 | + if (!cursor) return resolve(collectResults ? results : undefined) |
| 54 | + |
| 55 | + try { |
| 56 | + collectResults && results.push(cursor.value as T) |
| 57 | + await processor?.(cursor) |
| 58 | + cursor.continue() |
| 59 | + } catch (error) { |
| 60 | + reject(error) |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + req.onerror = () => reject(req.error) |
| 65 | + }) |
| 66 | +} |
| 67 | + |
| 68 | +export abstract class BaseIDBStorage<T = Record<string, unknown>> { |
| 69 | + private db: IDBDatabase | undefined |
| 70 | + abstract indexes: Index<T>[] |
| 71 | + abstract key: Key<T> | Key<T>[] |
| 72 | + abstract table: Table |
| 73 | + |
| 74 | + protected async initDb(): Promise<IDBDatabase> { |
| 75 | + if (this.db) return this.db |
| 76 | + |
| 77 | + const factory = typeof window === 'undefined' ? self.indexedDB : window.indexedDB |
| 78 | + const checkRequest = factory.open(DB_NAME) |
| 79 | + |
| 80 | + return new Promise((resolve, reject) => { |
| 81 | + checkRequest.onsuccess = () => { |
| 82 | + const db = checkRequest.result |
| 83 | + const storeExisted = db.objectStoreNames.contains(this.table) |
| 84 | + const needUpgrade = !storeExisted || this.needUpgradeIndexes(db) |
| 85 | + |
| 86 | + if (!needUpgrade) { |
| 87 | + this.db = db |
| 88 | + return resolve(db) |
| 89 | + } |
| 90 | + |
| 91 | + const currentVersion = db.version |
| 92 | + db.close() |
| 93 | + |
| 94 | + const upgradeRequest = factory.open(DB_NAME, currentVersion + 1) |
| 95 | + |
| 96 | + upgradeRequest.onupgradeneeded = () => { |
| 97 | + const upgradeDb = upgradeRequest.result |
| 98 | + const transaction = upgradeRequest.transaction |
| 99 | + if (!transaction) return reject("Failed to get transaction of upgrading request") |
| 100 | + |
| 101 | + let store = upgradeDb.objectStoreNames.contains(this.table) |
| 102 | + ? transaction.objectStore(this.table) |
| 103 | + : upgradeDb.createObjectStore(this.table, { keyPath: this.key }) |
| 104 | + this.createIndexes(store) |
| 105 | + } |
| 106 | + |
| 107 | + upgradeRequest.onsuccess = () => { |
| 108 | + console.log("IndexedDB upgraded") |
| 109 | + this.db = upgradeRequest.result |
| 110 | + resolve(upgradeRequest.result) |
| 111 | + } |
| 112 | + |
| 113 | + upgradeRequest.onerror = () => reject(upgradeRequest.error) |
| 114 | + } |
| 115 | + |
| 116 | + checkRequest.onerror = () => reject(checkRequest.error) |
| 117 | + }) |
| 118 | + } |
| 119 | + |
| 120 | + private needUpgradeIndexes(db: IDBDatabase): boolean { |
| 121 | + try { |
| 122 | + const transaction = db.transaction(this.table, 'readonly') |
| 123 | + const store = transaction.objectStore(this.table) |
| 124 | + const indexNames = store.indexNames |
| 125 | + |
| 126 | + for (const index of this.indexes) { |
| 127 | + const { key } = normalizeIndex(index) |
| 128 | + const idxName = formatIdxName(key) |
| 129 | + if (!indexNames.contains(idxName)) { |
| 130 | + return true |
| 131 | + } |
| 132 | + } |
| 133 | + return false |
| 134 | + } catch (e) { |
| 135 | + console.error("Failed to check indexes", e) |
| 136 | + return true |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | + private createIndexes(store: IDBObjectStore) { |
| 141 | + const existingIndexes = store.indexNames |
| 142 | + |
| 143 | + for (const index of this.indexes) { |
| 144 | + const { key, unique } = normalizeIndex(index) |
| 145 | + const idxName = formatIdxName(key) |
| 146 | + if (!existingIndexes.contains(idxName)) { |
| 147 | + store.createIndex(idxName, key, { unique }) |
| 148 | + } |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + protected async withStore<T = unknown>(operation: (store: IDBObjectStore) => T | Promise<T>, mode?: IDBTransactionMode): Promise<T> { |
| 153 | + const db = await this.initDb() |
| 154 | + const trans = db.transaction(this.table, mode ?? 'readwrite') |
| 155 | + try { |
| 156 | + const store = trans.objectStore(this.table) |
| 157 | + const result = await operation(store) |
| 158 | + // Waiting for transaction completed |
| 159 | + await new Promise<void>((resolve, reject) => { |
| 160 | + trans.oncomplete = () => resolve() |
| 161 | + trans.onerror = () => reject(trans.error) |
| 162 | + trans.onabort = () => reject(new Error('Transaction aborted')) |
| 163 | + }) |
| 164 | + return result |
| 165 | + } catch (e) { |
| 166 | + console.error("Failed to process with transaction", e) |
| 167 | + if (!trans.error && trans.mode !== 'readonly') { |
| 168 | + try { |
| 169 | + trans.abort() |
| 170 | + } catch (ignored) { } |
| 171 | + } |
| 172 | + throw e |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + protected assertIndex(store: IDBObjectStore, key: Key<T> | Key<T>[]): IDBIndex { |
| 177 | + const idxName = formatIdxName(key) |
| 178 | + try { |
| 179 | + return store.index(idxName) |
| 180 | + } catch (err) { |
| 181 | + console.error(`Failed to query index: table=${this.table}`, err) |
| 182 | + throw err |
| 183 | + } |
| 184 | + } |
| 185 | +} |
0 commit comments