@@ -25,6 +25,12 @@ import { PersistencePromise } from './persistence_promise';
25
25
26
26
const LOG_TAG = 'SimpleDb' ;
27
27
28
+ /**
29
+ * The maximum number of retry attempts for an IndexedDb transaction that fails
30
+ * with a DOMException.
31
+ */
32
+ const TRANSACTION_RETRY_COUNT = 3 ;
33
+
28
34
export interface SimpleDbSchemaConverter {
29
35
createOrUpgrade (
30
36
db : IDBDatabase ,
@@ -242,32 +248,58 @@ export class SimpleDb {
242
248
} ;
243
249
}
244
250
245
- runTransaction < T > (
251
+ async runTransaction < T > (
246
252
mode : 'readonly' | 'readwrite' ,
253
+ idempotent : boolean ,
247
254
objectStores : string [ ] ,
248
255
transactionFn : ( transaction : SimpleDbTransaction ) => PersistencePromise < T >
249
256
) : Promise < T > {
250
- const transaction = SimpleDbTransaction . open ( this . db , mode , objectStores ) ;
251
- const transactionFnResult = transactionFn ( transaction )
252
- . catch ( error => {
253
- // Abort the transaction if there was an error.
254
- transaction . abort ( error ) ;
255
- // We cannot actually recover, and calling `abort()` will cause the transaction's
256
- // completion promise to be rejected. This in turn means that we won't use
257
- // `transactionFnResult` below. We return a rejection here so that we don't add the
258
- // possibility of returning `void` to the type of `transactionFnResult`.
259
- return PersistencePromise . reject < T > ( error ) ;
260
- } )
261
- . toPromise ( ) ;
262
-
263
- // As noted above, errors are propagated by aborting the transaction. So
264
- // we swallow any error here to avoid the browser logging it as unhandled.
265
- transactionFnResult . catch ( ( ) => { } ) ;
266
-
267
- // Wait for the transaction to complete (i.e. IndexedDb's onsuccess event to
268
- // fire), but still return the original transactionFnResult back to the
269
- // caller.
270
- return transaction . completionPromise . then ( ( ) => transactionFnResult ) ;
257
+ let attemptNumber = 0 ;
258
+
259
+ while ( true ) {
260
+ ++ attemptNumber ;
261
+
262
+ const transaction = SimpleDbTransaction . open ( this . db , mode , objectStores ) ;
263
+ try {
264
+ const transactionFnResult = transactionFn ( transaction )
265
+ . catch ( error => {
266
+ // Abort the transaction if there was an error.
267
+ transaction . abort ( error ) ;
268
+ // We cannot actually recover, and calling `abort()` will cause the transaction's
269
+ // completion promise to be rejected. This in turn means that we won't use
270
+ // `transactionFnResult` below. We return a rejection here so that we don't add the
271
+ // possibility of returning `void` to the type of `transactionFnResult`.
272
+ return PersistencePromise . reject < T > ( error ) ;
273
+ } )
274
+ . toPromise ( ) ;
275
+
276
+ // As noted above, errors are propagated by aborting the transaction. So
277
+ // we swallow any error here to avoid the browser logging it as unhandled.
278
+ transactionFnResult . catch ( ( ) => { } ) ;
279
+
280
+ // Wait for the transaction to complete (i.e. IndexedDb's onsuccess event to
281
+ // fire), but still return the original transactionFnResult back to the
282
+ // caller.
283
+ await transaction . completionPromise ;
284
+ return transactionFnResult ;
285
+ } catch ( e ) {
286
+ // TODO: We could probably be smarter about this an not retry exceptions
287
+ // that are likely unrecoverable (such as quota exceptions).
288
+ const retryable =
289
+ idempotent &&
290
+ isDomException ( e ) &&
291
+ attemptNumber < TRANSACTION_RETRY_COUNT ;
292
+ debug (
293
+ 'Transaction failed with error: %s. Retrying: %s.' ,
294
+ e . message ,
295
+ retryable
296
+ ) ;
297
+
298
+ if ( ! retryable ) {
299
+ return Promise . reject ( e ) ;
300
+ }
301
+ }
302
+ }
271
303
}
272
304
273
305
close ( ) : void {
@@ -755,3 +787,15 @@ function checkForAndReportiOSError(error: DOMException): Error {
755
787
}
756
788
return error ;
757
789
}
790
+
791
+ /**
792
+ * Checks whether an error is a DOMException (e.g. as thrown by IndexedDb).
793
+ *
794
+ * Supports both browsers and Node with persistence.
795
+ */
796
+ function isDomException ( e : Error ) : boolean {
797
+ return (
798
+ ( typeof DOMException !== 'undefined' && e instanceof DOMException ) ||
799
+ e . constructor . name === 'DOMException'
800
+ ) ;
801
+ }
0 commit comments