Skip to content

Commit

Permalink
feat: add retry to accounting (#166)
Browse files Browse the repository at this point in the history
* feat: add retry to accounting

* fix: fix off by one bug in retry logic

* docs: add jsdocs to new utility functions

* style: rename DepositModal to CheckoutModal
  • Loading branch information
Cafe137 committed Aug 12, 2021
1 parent ec42eaf commit a62243f
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 12 deletions.
8 changes: 3 additions & 5 deletions src/components/CashoutModal.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import { ReactElement, useState } from 'react'
import { CircularProgress, Container } from '@material-ui/core'
import Button from '@material-ui/core/Button'
import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle'
import { Container, CircularProgress } from '@material-ui/core'
import { useSnackbar } from 'notistack'

import { ReactElement, useState } from 'react'
import { beeDebugApi } from '../services/bee'

import EthereumAddress from './EthereumAddress'

interface Props {
peerId: string
uncashedAmount: string
}

export default function DepositModal({ peerId, uncashedAmount }: Props): ReactElement {
export default function CheckoutModal({ peerId, uncashedAmount }: Props): ReactElement {
const [open, setOpen] = useState<boolean>(false)
const [loadingCashout, setLoadingCashout] = useState<boolean>(false)
const { enqueueSnackbar } = useSnackbar()
Expand Down
15 changes: 8 additions & 7 deletions src/hooks/accounting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { LastCashoutActionResponse } from '@ethersphere/bee-js'
import { useEffect, useState } from 'react'
import { Token } from '../models/Token'
import { beeDebugApi } from '../services/bee'
import { makeRetriablePromise, unwrapPromiseSettlements } from '../utils'
import { Balance, Settlement, useApiPeerBalances, useApiSettlements } from './apiHooks'

interface UseAccountingHook {
Expand Down Expand Up @@ -80,11 +81,10 @@ export const useAccounting = (): UseAccountingHook => {
const settlements = useApiSettlements()
const balances = useApiPeerBalances()

const [err, setErr] = useState<Error | null>(null)
const [isLoadingUncashed, setIsloadingUncashed] = useState<boolean>(false)
const [uncashedAmounts, setUncashedAmounts] = useState<LastCashoutActionResponse[] | undefined>(undefined)

const error = balances.error || settlements.error || err
const error = balances.error || settlements.error

useEffect(() => {
// We don't have any settlements loaded yet or we are already loading/have loaded the uncashed amounts
Expand All @@ -93,12 +93,13 @@ export const useAccounting = (): UseAccountingHook => {
setIsloadingUncashed(true)
const promises = settlements.settlements.settlements
.filter(({ received }) => received.toBigNumber.gt('0'))
.map(({ peer }) => beeDebugApi.chequebook.getPeerLastCashout(peer))
.map(({ peer }) => makeRetriablePromise(() => beeDebugApi.chequebook.getPeerLastCashout(peer)))

Promise.all(promises)
.then(setUncashedAmounts)
.catch(setErr)
.finally(() => setIsloadingUncashed(false))
Promise.allSettled(promises).then(settlements => {
const results = unwrapPromiseSettlements(settlements)
setUncashedAmounts(results.fulfilled)
setIsloadingUncashed(false)
})
}, [settlements, isLoadingUncashed, uncashedAmounts, error])

const accounting = mergeAccounting(balances.peerBalances, settlements.settlements?.settlements, uncashedAmounts)
Expand Down
74 changes: 74 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,77 @@ export function makeBigNumber(value: BigNumber | BigInt | number | string): BigN

throw new TypeError(`Not a BigNumber or BigNumber convertible value. Type: ${typeof value} value: ${value}`)
}

export type PromiseSettlements<T> = {
fulfilled: PromiseFulfilledResult<T>[]
rejected: PromiseRejectedResult[]
}

export type UnwrappedPromiseSettlements<T> = {
fulfilled: T[]
rejected: string[]
}

export async function sleepMs(ms: number): Promise<void> {
await new Promise<void>(resolve =>
setTimeout(() => {
resolve()
}, ms),
)
}

/**
* Maps the returned results of `Promise.allSettled` to an object
* with `fulfilled` and `rejected` arrays for easy access.
*
* The results still need to be unwrapped to get the fulfilled values or rejection reasons.
*/
export function mapPromiseSettlements<T>(promises: PromiseSettledResult<T>[]): PromiseSettlements<T> {
const fulfilled = promises.filter(promise => promise.status === 'fulfilled') as PromiseFulfilledResult<T>[]
const rejected = promises.filter(promise => promise.status === 'rejected') as PromiseRejectedResult[]

return { fulfilled, rejected }
}

/**
* Maps the returned values of `Promise.allSettled` to an object
* with `fulfilled` and `rejected` arrays for easy access.
*
* For rejected promises, the value is the stringified `reason`,
* or `'Unknown error'` string when it is unavailable.
*/
export function unwrapPromiseSettlements<T>(
promiseSettledResults: PromiseSettledResult<T>[],
): UnwrappedPromiseSettlements<T> {
const values = mapPromiseSettlements(promiseSettledResults)
const fulfilled = values.fulfilled.map(x => x.value)
const rejected = values.rejected.map(x => (x.reason ? String(x.reason) : 'Unknown error'))

return { fulfilled, rejected }
}

/**
* Wraps a `Promise<T>` or async function inside a new `Promise<T>`,
* which retries the original function up to `maxRetries` times,
* waiting `delayMs` milliseconds between failed attempts.
*
* If all attempts fail, then this `Promise<T>` also rejects.
*/
export function makeRetriablePromise<T>(fn: () => Promise<T>, maxRetries = 3, delayMs = 1000): Promise<T> {
return new Promise(async (resolve, reject) => {
for (let tries = 0; tries < maxRetries; tries++) {
try {
const results = await fn()
resolve(results)

return
} catch (error) {
if (tries < maxRetries - 1) {
await sleepMs(delayMs)
} else {
reject(error)
}
}
}
})
}

0 comments on commit a62243f

Please sign in to comment.