@@ -13,6 +13,8 @@ const logger = createLogger('IdempotencyService')
1313export interface IdempotencyConfig {
1414 ttlSeconds ?: number
1515 namespace ?: string
16+ /** When true, failed keys are deleted rather than stored so the operation is retried on the next attempt. */
17+ retryFailures ?: boolean
1618}
1719
1820export interface IdempotencyResult {
@@ -58,6 +60,7 @@ export class IdempotencyService {
5860 this . config = {
5961 ttlSeconds : config . ttlSeconds ?? DEFAULT_TTL ,
6062 namespace : config . namespace ?? 'default' ,
63+ retryFailures : config . retryFailures ?? false ,
6164 }
6265 this . storageMethod = getStorageMethod ( )
6366 logger . info ( `IdempotencyService using ${ this . storageMethod } storage` , {
@@ -340,6 +343,21 @@ export class IdempotencyService {
340343 logger . debug ( `Stored idempotency result in database: ${ normalizedKey } ` )
341344 }
342345
346+ private async deleteKey (
347+ normalizedKey : string ,
348+ storageMethod : 'redis' | 'database'
349+ ) : Promise < void > {
350+ if ( storageMethod === 'redis' ) {
351+ const redis = getRedisClient ( )
352+ if ( redis ) await redis . del ( `${ REDIS_KEY_PREFIX } ${ normalizedKey } ` ) . catch ( ( ) => { } )
353+ } else {
354+ await db
355+ . delete ( idempotencyKey )
356+ . where ( eq ( idempotencyKey . key , normalizedKey ) )
357+ . catch ( ( ) => { } )
358+ }
359+ }
360+
343361 async executeWithIdempotency < T > (
344362 provider : string ,
345363 identifier : string ,
@@ -360,6 +378,10 @@ export class IdempotencyService {
360378 }
361379
362380 if ( existingResult ?. status === 'failed' ) {
381+ if ( this . config . retryFailures ) {
382+ await this . deleteKey ( claimResult . normalizedKey , claimResult . storageMethod )
383+ return this . executeWithIdempotency ( provider , identifier , operation , additionalContext )
384+ }
363385 logger . info ( `Previous operation failed for: ${ claimResult . normalizedKey } ` )
364386 throw new Error ( existingResult . error || 'Previous operation failed' )
365387 }
@@ -391,11 +413,15 @@ export class IdempotencyService {
391413 } catch ( error ) {
392414 const errorMessage = error instanceof Error ? error . message : 'Unknown error'
393415
394- await this . storeResult (
395- claimResult . normalizedKey ,
396- { success : false , error : errorMessage , status : 'failed' } ,
397- claimResult . storageMethod
398- )
416+ if ( this . config . retryFailures ) {
417+ await this . deleteKey ( claimResult . normalizedKey , claimResult . storageMethod )
418+ } else {
419+ await this . storeResult (
420+ claimResult . normalizedKey ,
421+ { success : false , error : errorMessage , status : 'failed' } ,
422+ claimResult . storageMethod
423+ )
424+ }
399425
400426 logger . warn ( `Operation failed: ${ claimResult . normalizedKey } - ${ errorMessage } ` )
401427 throw error
@@ -454,4 +480,5 @@ export const webhookIdempotency = new IdempotencyService({
454480export const pollingIdempotency = new IdempotencyService ( {
455481 namespace : 'polling' ,
456482 ttlSeconds : 60 * 60 * 24 * 3 , // 3 days
483+ retryFailures : true ,
457484} )
0 commit comments