@@ -63,6 +63,21 @@ export async function iterateCursor<T = unknown>(
6363 } )
6464}
6565
66+ type TransactionError = 'Connection' | 'StoreNotFound' | 'Unknown'
67+
68+ const detectTransactionError = ( err : unknown ) : TransactionError => {
69+ if ( ! ( err instanceof DOMException ) ) {
70+ return 'Unknown'
71+ }
72+ if ( err . name === 'InvalidStateError' || err . name === 'AbortError' ) {
73+ return 'Connection'
74+ }
75+ if ( err . name === 'NotFoundError' ) {
76+ return 'StoreNotFound'
77+ }
78+ return 'Unknown'
79+ }
80+
6681export function closedRangeKey ( lower : IDBValidKey | undefined , upper : IDBValidKey | undefined ) : IDBKeyRange | undefined {
6782 if ( lower !== undefined && upper !== undefined ) {
6883 if ( lower > upper ) {
@@ -87,53 +102,131 @@ export abstract class BaseIDBStorage<T = Record<string, unknown>> {
87102 private DB_NAME = `tt4b_${ chrome . runtime . id } ` as const
88103
89104 private db : IDBDatabase | undefined
105+ private static initPromises = new Map < string , Promise < IDBDatabase > > ( )
106+
90107 abstract indexes : Index < T > [ ]
91108 abstract key : Key < T > | Key < T > [ ]
92109 abstract table : Table
93110
94111 protected async initDb ( ) : Promise < IDBDatabase > {
95112 if ( this . db ) return this . db
96113
114+ let initPromise = BaseIDBStorage . initPromises . get ( this . table )
115+ if ( ! initPromise ) {
116+ initPromise = this . doInitDb ( )
117+ BaseIDBStorage . initPromises . set ( this . table , initPromise )
118+ }
119+
120+ try {
121+ this . db = await initPromise
122+ this . setupDbCloseHandler ( this . db )
123+ return this . db
124+ } catch ( error ) {
125+ BaseIDBStorage . initPromises . delete ( this . table )
126+ throw error
127+ }
128+ }
129+
130+ private setupDbCloseHandler ( db : IDBDatabase ) : void {
131+ db . onclose = ( ) => {
132+ if ( this . db !== db ) return
133+
134+ this . db = undefined
135+ BaseIDBStorage . initPromises . delete ( this . table )
136+ }
137+ }
138+
139+ private async doInitDb ( ) : Promise < IDBDatabase > {
97140 const factory = typeof window !== 'undefined' ? window . indexedDB : globalThis . indexedDB
98- const checkRequest = factory . open ( this . DB_NAME )
99141
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 )
142+ const checkDb = await new Promise < IDBDatabase > ( ( resolve , reject ) => {
143+ const checkRequest = factory . open ( this . DB_NAME )
144+ checkRequest . onsuccess = ( ) => resolve ( checkRequest . result )
145+ checkRequest . onerror = ( ) => reject ( checkRequest . error || new Error ( "Failed to open database" ) )
146+ } )
105147
106- if ( ! needUpgrade ) {
107- this . db = db
108- return resolve ( db )
109- }
148+ return checkDb
149+ }
150+
151+ // Only used for testing, be careful when using in production
152+ public async clear ( ) : Promise < void > {
153+ await this . withStore ( store => store . clear ( ) , 'readwrite' )
154+ }
155+
156+ async upgrade ( ) : Promise < void > {
157+ const factory = typeof window !== 'undefined' ? window . indexedDB : globalThis . indexedDB
158+
159+ const checkDb = await new Promise < IDBDatabase > ( ( resolve , reject ) => {
160+ const checkRequest = factory . open ( this . DB_NAME )
161+ checkRequest . onsuccess = ( ) => resolve ( checkRequest . result )
162+ checkRequest . onerror = ( ) => reject ( checkRequest . error || new Error ( "Failed to open database" ) )
163+ checkRequest . onblocked = ( ) => {
164+ console . warn ( `Database check blocked for "${ this . table } " (DB: ${ this . DB_NAME } ), waiting for other connections to close` )
165+ }
166+ } )
167+
168+ const storeExisted = checkDb . objectStoreNames . contains ( this . table )
169+ const needUpgrade = ! storeExisted || this . needUpgradeIndexes ( checkDb )
170+
171+ if ( ! needUpgrade ) {
172+ checkDb . close ( )
173+ return
174+ }
110175
111- const currentVersion = db . version
112- db . close ( )
176+ const currentVersion = checkDb . version
177+ checkDb . close ( )
113178
114- const upgradeRequest = factory . open ( this . DB_NAME , currentVersion + 1 )
179+ return new Promise < void > ( ( resolve , reject ) => {
180+ const upgradeRequest = factory . open ( this . DB_NAME , currentVersion + 1 )
115181
116- upgradeRequest . onupgradeneeded = ( ) => {
182+ upgradeRequest . onupgradeneeded = ( ) => {
183+ try {
117184 const upgradeDb = upgradeRequest . result
118185 const transaction = upgradeRequest . transaction
119- if ( ! transaction ) return reject ( "Failed to get transaction of upgrading request" )
186+ if ( ! transaction ) {
187+ reject ( new Error ( "Failed to get transaction of upgrading request" ) )
188+ return
189+ }
190+
191+ transaction . onerror = ( ) => {
192+ reject ( transaction . error || new Error ( "Transaction failed" ) )
193+ }
194+
195+ transaction . onabort = ( ) => {
196+ reject ( new Error ( "Upgrade transaction was aborted" ) )
197+ }
120198
121199 let store = upgradeDb . objectStoreNames . contains ( this . table )
122200 ? transaction . objectStore ( this . table )
123- : upgradeDb . createObjectStore ( this . table , { keyPath : this . key } )
201+ : upgradeDb . createObjectStore ( this . table , { keyPath : this . key as string | string [ ] } )
124202 this . createIndexes ( store )
203+ } catch ( error ) {
204+ console . error ( "Failed to upgrade database in onupgradeneeded" , error )
205+ upgradeRequest . transaction ?. abort ( )
206+ reject ( error instanceof Error ? error : new Error ( String ( error ) ) )
125207 }
208+ }
126209
127- upgradeRequest . onsuccess = ( ) => {
128- console . log ( " IndexedDB upgraded" )
129- this . db = upgradeRequest . result
130- resolve ( upgradeRequest . result )
131- }
210+ upgradeRequest . onsuccess = ( ) => {
211+ console . log ( ` IndexedDB upgraded for table " ${ this . table } "` )
212+ upgradeRequest . result . close ( )
213+ resolve ( )
214+ }
132215
133- upgradeRequest . onerror = ( ) => reject ( upgradeRequest . error )
216+ upgradeRequest . onerror = ( event ) => {
217+ console . error ( "Failed to upgrade database" , event , upgradeRequest . error )
218+ reject ( upgradeRequest . error || new Error ( "Failed to upgrade database" ) )
134219 }
135220
136- checkRequest . onerror = ( ) => reject ( checkRequest . error )
221+ upgradeRequest . onblocked = ( ) => {
222+ const blockingTables = Array . from ( BaseIDBStorage . initPromises . keys ( ) )
223+ . filter ( table => table !== this . table )
224+ console . warn (
225+ `Database upgrade blocked for table "${ this . table } " (DB: ${ this . DB_NAME } ), ` +
226+ `waiting for other connections to close. ` +
227+ `Other tables with active connections: ${ blockingTables . length > 0 ? blockingTables . join ( ', ' ) : 'none' } `
228+ )
229+ }
137230 } )
138231 }
139232
@@ -170,27 +263,45 @@ export abstract class BaseIDBStorage<T = Record<string, unknown>> {
170263 }
171264
172265 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 ) { }
266+ let db = await this . initDb ( )
267+
268+ for ( let retryCount = 0 ; retryCount < 2 ; retryCount ++ ) {
269+ let trans : IDBTransaction | undefined
270+ try {
271+ trans = db . transaction ( this . table , mode ?? 'readwrite' )
272+ const store = trans . objectStore ( this . table )
273+ const result = await operation ( store )
274+ const transaction = trans
275+ await new Promise < void > ( ( resolve , reject ) => {
276+ transaction . oncomplete = ( ) => resolve ( )
277+ transaction . onerror = ( ) => reject ( transaction . error )
278+ transaction . onabort = ( ) => reject ( new Error ( 'Transaction aborted' ) )
279+ } )
280+ return result
281+ } catch ( e ) {
282+ const errorType = detectTransactionError ( e )
283+
284+ if ( errorType === 'Unknown' ) {
285+ console . error ( "Failed to process with transaction" , e )
286+ if ( trans && ! trans . error && trans . mode !== 'readonly' ) {
287+ try {
288+ trans . abort ( )
289+ } catch ( ignored ) { }
290+ }
291+ throw e
292+ }
293+
294+ if ( errorType === 'StoreNotFound' ) {
295+ this . db ?. close ( )
296+ await this . upgrade ( )
297+ }
298+
299+ this . db = undefined
300+ BaseIDBStorage . initPromises . delete ( this . table )
301+ db = await this . initDb ( )
191302 }
192- throw e
193303 }
304+ throw new Error ( "Max retries exceeded" )
194305 }
195306
196307 protected assertIndex ( store : IDBObjectStore , key : Key < T > | Key < T > [ ] ) : IDBIndex {
0 commit comments