Skip to content

Commit a11593f

Browse files
committed
fix: upgrade idb
1 parent ef989a3 commit a11593f

File tree

4 files changed

+150
-45
lines changed

4 files changed

+150
-45
lines changed

src/background/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import badgeTextManager from "./badge-manager"
1515
import initBrowserAction from "./browser-action-manager"
1616
import initCsHandler from "./content-script-handler"
1717
import initDataCleaner from "./data-cleaner"
18-
import handleInstall from "./install-handler"
18+
import handleInstall from './install-handler'
1919
import initLimitProcessor from "./limit-processor"
2020
import MessageDispatcher from "./message-dispatcher"
2121
import VersionMigrator from "./migrator"

src/background/migrator/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class VersionManager {
4242
}
4343
}
4444

45-
init() {
45+
async init() {
4646
onInstalled(reason => this.onChromeInstalled(reason))
4747
}
4848
}

src/background/migrator/indexed-migrator.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,28 @@
1+
import { BaseIDBStorage } from '@db/common/indexed-storage'
2+
import { IDBStatDatabase } from '@db/stat-database/idb'
13
import timelineDatabase from '@db/timeline-database'
4+
import IDBTimelineDatabase from '@db/timeline-database/idb'
25
import type { Migrator } from './types'
36

7+
async function upgradeIndexedDB() {
8+
try {
9+
const storages: BaseIDBStorage<any>[] = [new IDBStatDatabase(), new IDBTimelineDatabase()]
10+
for (const storage of storages) {
11+
await storage.upgrade()
12+
}
13+
console.log('IndexedDB upgraded successfully')
14+
} catch (error) {
15+
console.error('Failed to upgrade IndexedDB', error)
16+
}
17+
}
18+
419
class IndexedMigrator implements Migrator {
520
onInstall(): void {
621
}
7-
onUpdate(_version: string): void {
22+
23+
async onUpdate(_version: string): Promise<void> {
24+
await upgradeIndexedDB()
25+
826
timelineDatabase.migrateFromClassic()
927
.then(() => console.log('Timeline data migrated to IndexedDB'))
1028
.catch(e => console.error('Failed to migrate timeline data to IndexedDB', e))

src/database/common/indexed-storage.ts

Lines changed: 129 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -87,53 +87,126 @@ export abstract class BaseIDBStorage<T = Record<string, unknown>> {
8787
private DB_NAME = `tt4b_${chrome.runtime.id}` as const
8888

8989
private db: IDBDatabase | undefined
90+
private static initPromises = new Map<string, Promise<IDBDatabase>>()
91+
9092
abstract indexes: Index<T>[]
9193
abstract key: Key<T> | Key<T>[]
9294
abstract table: Table
9395

9496
protected async initDb(): Promise<IDBDatabase> {
9597
if (this.db) return this.db
9698

99+
let initPromise = BaseIDBStorage.initPromises.get(this.table)
100+
if (!initPromise) {
101+
initPromise = this.doInitDb()
102+
BaseIDBStorage.initPromises.set(this.table, initPromise)
103+
}
104+
105+
try {
106+
this.db = await initPromise
107+
this.setupDbCloseHandler(this.db)
108+
return this.db
109+
} catch (error) {
110+
BaseIDBStorage.initPromises.delete(this.table)
111+
throw error
112+
}
113+
}
114+
115+
private setupDbCloseHandler(db: IDBDatabase): void {
116+
db.onclose = () => {
117+
if (this.db !== db) return
118+
119+
this.db = undefined
120+
BaseIDBStorage.initPromises.delete(this.table)
121+
}
122+
}
123+
124+
private async doInitDb(): Promise<IDBDatabase> {
97125
const factory = typeof window !== 'undefined' ? window.indexedDB : globalThis.indexedDB
98-
const checkRequest = factory.open(this.DB_NAME)
99126

100-
return new Promise((resolve, reject) => {
101-
checkRequest.onsuccess = () => {
102-
const db = checkRequest.result
103-
const storeExisted = db.objectStoreNames.contains(this.table)
104-
const needUpgrade = !storeExisted || this.needUpgradeIndexes(db)
127+
const checkDb = await new Promise<IDBDatabase>((resolve, reject) => {
128+
const checkRequest = factory.open(this.DB_NAME)
129+
checkRequest.onsuccess = () => resolve(checkRequest.result)
130+
checkRequest.onerror = () => reject(checkRequest.error || new Error("Failed to open database"))
131+
})
105132

106-
if (!needUpgrade) {
107-
this.db = db
108-
return resolve(db)
109-
}
133+
return checkDb
134+
}
110135

111-
const currentVersion = db.version
112-
db.close()
136+
async upgrade(): Promise<void> {
137+
const factory = typeof window !== 'undefined' ? window.indexedDB : globalThis.indexedDB
138+
139+
const checkDb = await new Promise<IDBDatabase>((resolve, reject) => {
140+
const checkRequest = factory.open(this.DB_NAME)
141+
checkRequest.onsuccess = () => resolve(checkRequest.result)
142+
checkRequest.onerror = () => reject(checkRequest.error || new Error("Failed to open database"))
143+
checkRequest.onblocked = () => {
144+
console.warn(`Database check blocked for "${this.table}" (DB: ${this.DB_NAME}), waiting for other connections to close`)
145+
}
146+
})
113147

114-
const upgradeRequest = factory.open(this.DB_NAME, currentVersion + 1)
148+
const storeExisted = checkDb.objectStoreNames.contains(this.table)
149+
const needUpgrade = !storeExisted || this.needUpgradeIndexes(checkDb)
115150

116-
upgradeRequest.onupgradeneeded = () => {
151+
if (!needUpgrade) {
152+
checkDb.close()
153+
return
154+
}
155+
156+
const currentVersion = checkDb.version
157+
checkDb.close()
158+
159+
return new Promise<void>((resolve, reject) => {
160+
const upgradeRequest = factory.open(this.DB_NAME, currentVersion + 1)
161+
162+
upgradeRequest.onupgradeneeded = () => {
163+
try {
117164
const upgradeDb = upgradeRequest.result
118165
const transaction = upgradeRequest.transaction
119-
if (!transaction) return reject("Failed to get transaction of upgrading request")
166+
if (!transaction) {
167+
reject(new Error("Failed to get transaction of upgrading request"))
168+
return
169+
}
170+
171+
transaction.onerror = () => {
172+
reject(transaction.error || new Error("Transaction failed"))
173+
}
174+
175+
transaction.onabort = () => {
176+
reject(new Error("Upgrade transaction was aborted"))
177+
}
120178

121179
let store = upgradeDb.objectStoreNames.contains(this.table)
122180
? transaction.objectStore(this.table)
123-
: upgradeDb.createObjectStore(this.table, { keyPath: this.key })
181+
: upgradeDb.createObjectStore(this.table, { keyPath: this.key as string | string[] })
124182
this.createIndexes(store)
183+
} catch (error) {
184+
console.error("Failed to upgrade database in onupgradeneeded", error)
185+
upgradeRequest.transaction?.abort()
186+
reject(error instanceof Error ? error : new Error(String(error)))
125187
}
188+
}
126189

127-
upgradeRequest.onsuccess = () => {
128-
console.log("IndexedDB upgraded")
129-
this.db = upgradeRequest.result
130-
resolve(upgradeRequest.result)
131-
}
190+
upgradeRequest.onsuccess = () => {
191+
console.log(`IndexedDB upgraded for table "${this.table}"`)
192+
upgradeRequest.result.close()
193+
resolve()
194+
}
132195

133-
upgradeRequest.onerror = () => reject(upgradeRequest.error)
196+
upgradeRequest.onerror = (event) => {
197+
console.error("Failed to upgrade database", event, upgradeRequest.error)
198+
reject(upgradeRequest.error || new Error("Failed to upgrade database"))
134199
}
135200

136-
checkRequest.onerror = () => reject(checkRequest.error)
201+
upgradeRequest.onblocked = () => {
202+
const blockingTables = Array.from(BaseIDBStorage.initPromises.keys())
203+
.filter(table => table !== this.table)
204+
console.warn(
205+
`Database upgrade blocked for table "${this.table}" (DB: ${this.DB_NAME}), ` +
206+
`waiting for other connections to close. ` +
207+
`Other tables with active connections: ${blockingTables.length > 0 ? blockingTables.join(', ') : 'none'}`
208+
)
209+
}
137210
})
138211
}
139212

@@ -170,27 +243,41 @@ export abstract class BaseIDBStorage<T = Record<string, unknown>> {
170243
}
171244

172245
protected async withStore<T = unknown>(operation: (store: IDBObjectStore) => T | Promise<T>, mode?: IDBTransactionMode): Promise<T> {
173-
const db = await this.initDb()
174-
const trans = db.transaction(this.table, mode ?? 'readwrite')
175-
try {
176-
const store = trans.objectStore(this.table)
177-
const result = await operation(store)
178-
// Waiting for transaction completed
179-
await new Promise<void>((resolve, reject) => {
180-
trans.oncomplete = () => resolve()
181-
trans.onerror = () => reject(trans.error)
182-
trans.onabort = () => reject(new Error('Transaction aborted'))
183-
})
184-
return result
185-
} catch (e) {
186-
console.error("Failed to process with transaction", e)
187-
if (!trans.error && trans.mode !== 'readonly') {
188-
try {
189-
trans.abort()
190-
} catch (ignored) { }
246+
let db = await this.initDb()
247+
248+
for (let retryCount = 0; retryCount < 2; retryCount++) {
249+
let trans: IDBTransaction | undefined
250+
try {
251+
trans = db.transaction(this.table, mode ?? 'readwrite')
252+
const store = trans.objectStore(this.table)
253+
const result = await operation(store)
254+
const transaction = trans
255+
await new Promise<void>((resolve, reject) => {
256+
transaction.oncomplete = () => resolve()
257+
transaction.onerror = () => reject(transaction.error)
258+
transaction.onabort = () => reject(new Error('Transaction aborted'))
259+
})
260+
return result
261+
} catch (e) {
262+
const isConnectionError = e instanceof DOMException &&
263+
(e.name === 'InvalidStateError' || e.name === 'AbortError')
264+
265+
if (isConnectionError) {
266+
this.db = undefined
267+
BaseIDBStorage.initPromises.delete(this.table)
268+
db = await this.initDb()
269+
} else {
270+
console.error("Failed to process with transaction", e)
271+
if (trans && !trans.error && trans.mode !== 'readonly') {
272+
try {
273+
trans.abort()
274+
} catch (ignored) { }
275+
}
276+
throw e
277+
}
191278
}
192-
throw e
193279
}
280+
throw new Error("Max retries exceeded")
194281
}
195282

196283
protected assertIndex(store: IDBObjectStore, key: Key<T> | Key<T>[]): IDBIndex {

0 commit comments

Comments
 (0)