@@ -25,6 +25,19 @@ 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
+
34
+ // The different modes supported by `SimpleDb.runTransaction()`
35
+ type SimpleDbTransactionMode =
36
+ | 'readonly'
37
+ | 'readwrite'
38
+ | 'readonly-idempotent'
39
+ | 'readwrite-idempotent' ;
40
+
28
41
export interface SimpleDbSchemaConverter {
29
42
createOrUpgrade (
30
43
db : IDBDatabase ,
@@ -242,32 +255,64 @@ export class SimpleDb {
242
255
} ;
243
256
}
244
257
245
- runTransaction < T > (
246
- mode : 'readonly' | 'readwrite' ,
258
+ async runTransaction < T > (
259
+ mode : SimpleDbTransactionMode ,
247
260
objectStores : string [ ] ,
248
261
transactionFn : ( transaction : SimpleDbTransaction ) => PersistencePromise < T >
249
262
) : 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 ) ;
263
+ const readonly = mode . startsWith ( 'readonly' ) ;
264
+ const idempotent = mode . endsWith ( 'idempotent' ) ;
265
+ let attemptNumber = 0 ;
266
+
267
+ while ( true ) {
268
+ ++ attemptNumber ;
269
+
270
+ const transaction = SimpleDbTransaction . open (
271
+ this . db ,
272
+ readonly ? 'readonly' : 'readwrite' ,
273
+ objectStores
274
+ ) ;
275
+ try {
276
+ const transactionFnResult = transactionFn ( transaction )
277
+ . catch ( error => {
278
+ // Abort the transaction if there was an error.
279
+ transaction . abort ( error ) ;
280
+ // We cannot actually recover, and calling `abort()` will cause the transaction's
281
+ // completion promise to be rejected. This in turn means that we won't use
282
+ // `transactionFnResult` below. We return a rejection here so that we don't add the
283
+ // possibility of returning `void` to the type of `transactionFnResult`.
284
+ return PersistencePromise . reject < T > ( error ) ;
285
+ } )
286
+ . toPromise ( ) ;
287
+
288
+ // As noted above, errors are propagated by aborting the transaction. So
289
+ // we swallow any error here to avoid the browser logging it as unhandled.
290
+ transactionFnResult . catch ( ( ) => { } ) ;
291
+
292
+ // Wait for the transaction to complete (i.e. IndexedDb's onsuccess event to
293
+ // fire), but still return the original transactionFnResult back to the
294
+ // caller.
295
+ await transaction . completionPromise ;
296
+ return transactionFnResult ;
297
+ } catch ( e ) {
298
+ // TODO(schmidt-sebastian): We could probably be smarter about this and
299
+ // not retry exceptions that are likely unrecoverable (such as quota
300
+ // exceeded errors).
301
+ const retryable =
302
+ idempotent &&
303
+ isDomException ( e ) &&
304
+ attemptNumber < TRANSACTION_RETRY_COUNT ;
305
+ debug (
306
+ 'Transaction failed with error: %s. Retrying: %s.' ,
307
+ e . message ,
308
+ retryable
309
+ ) ;
310
+
311
+ if ( ! retryable ) {
312
+ return Promise . reject ( e ) ;
313
+ }
314
+ }
315
+ }
271
316
}
272
317
273
318
close ( ) : void {
@@ -755,3 +800,13 @@ function checkForAndReportiOSError(error: DOMException): Error {
755
800
}
756
801
return error ;
757
802
}
803
+
804
+ /** Checks whether an error is a DOMException (e.g. as thrown by IndexedDb). */
805
+ function isDomException ( error : Error ) : boolean {
806
+ // DOMException is not a global type in Node with persistence, and hence we
807
+ // check the constructor name if the type in unknown.
808
+ return (
809
+ ( typeof DOMException !== 'undefined' && error instanceof DOMException ) ||
810
+ error . constructor . name === 'DOMException'
811
+ ) ;
812
+ }
0 commit comments