@@ -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