import * as admin from "firebase-admin" ; import { logger , LockError , LockStore } from "@deliverybot/core" ; export class FirebaseLockStore implements LockStore { static LOCK_MAX_RETRIES = 8 ; static LOCK_MIN_RETRIES = 2 ; static LOCK_RETRY_DELAY = 1000 * 15 ; static LOCK_LEASE = 1000 * 60 * 3 ; private firebase : admin . app . App ; private store : FirebaseFirestore . Firestore ; constructor ( firebase ) { this . firebase = firebase ; this . store = this . firebase . firestore ( ) ; } private lockKey ( key ) { return this . store . runTransaction ( async ( tx ) => { const doc = this . store . doc ( `locks/${ key }` ) ; const cur = await tx . get ( doc ) ; const data = cur . data ( ) ; if ( ! data || data . lockedAt < Date . now ( ) - FirebaseLockStore . LOCK_LEASE ) { tx . set ( doc , { lockedAt : Date . now ( ) } ) ; return true ; } return false ; } ) ; } private async unlockKey ( key ) { await this . store . doc ( `locks/${ key }` ) . delete ( ) ; } private retryWithJitter ( ) { return ( Math . floor ( Math . random ( ) * ( FirebaseLockStore . LOCK_MAX_RETRIES - FirebaseLockStore . LOCK_MIN_RETRIES + 1 ) ) + FirebaseLockStore . LOCK_MIN_RETRIES ) ; } public lock ( key , handler ) { const retries = this . retryWithJitter ( ) ; let tries = 0 ; return new Promise ( ( resolve , reject ) => { const run = async ( ) => { tries ++ ; const locked = await this . lockKey ( key ) ; if ( ! locked ) { if ( tries > retries ) { reject ( new LockError ( `Lock retry limit reached retries=${ tries } key=${ key }` ) ) ; return ; } logger . warn ( { key , tries } , "retrying lock" ) ; setTimeout ( run , FirebaseLockStore . LOCK_RETRY_DELAY ) ; return ; } logger . info ( { key } , "locked key" ) ; try { await handler ( ) ; resolve ( ) ; } catch ( err ) { reject ( err ) ; } logger . info ( { key } , "unlock key" ) ; this . unlockKey ( key ) . catch ( error => { logger . warn ( { error } , "unlock failed" ) ; } ) ; } ; run ( ) ; } ) ; } }