feat: add retry to accounting (#166)
* 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
This commit is contained in:
@@ -1,15 +1,13 @@
|
|||||||
import { ReactElement, useState } from 'react'
|
import { CircularProgress, Container } from '@material-ui/core'
|
||||||
import Button from '@material-ui/core/Button'
|
import Button from '@material-ui/core/Button'
|
||||||
import Dialog from '@material-ui/core/Dialog'
|
import Dialog from '@material-ui/core/Dialog'
|
||||||
import DialogActions from '@material-ui/core/DialogActions'
|
import DialogActions from '@material-ui/core/DialogActions'
|
||||||
import DialogContent from '@material-ui/core/DialogContent'
|
import DialogContent from '@material-ui/core/DialogContent'
|
||||||
import DialogContentText from '@material-ui/core/DialogContentText'
|
import DialogContentText from '@material-ui/core/DialogContentText'
|
||||||
import DialogTitle from '@material-ui/core/DialogTitle'
|
import DialogTitle from '@material-ui/core/DialogTitle'
|
||||||
import { Container, CircularProgress } from '@material-ui/core'
|
|
||||||
import { useSnackbar } from 'notistack'
|
import { useSnackbar } from 'notistack'
|
||||||
|
import { ReactElement, useState } from 'react'
|
||||||
import { beeDebugApi } from '../services/bee'
|
import { beeDebugApi } from '../services/bee'
|
||||||
|
|
||||||
import EthereumAddress from './EthereumAddress'
|
import EthereumAddress from './EthereumAddress'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -17,7 +15,7 @@ interface Props {
|
|||||||
uncashedAmount: 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 [open, setOpen] = useState<boolean>(false)
|
||||||
const [loadingCashout, setLoadingCashout] = useState<boolean>(false)
|
const [loadingCashout, setLoadingCashout] = useState<boolean>(false)
|
||||||
const { enqueueSnackbar } = useSnackbar()
|
const { enqueueSnackbar } = useSnackbar()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { LastCashoutActionResponse } from '@ethersphere/bee-js'
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Token } from '../models/Token'
|
import { Token } from '../models/Token'
|
||||||
import { beeDebugApi } from '../services/bee'
|
import { beeDebugApi } from '../services/bee'
|
||||||
|
import { makeRetriablePromise, unwrapPromiseSettlements } from '../utils'
|
||||||
import { Balance, Settlement, useApiPeerBalances, useApiSettlements } from './apiHooks'
|
import { Balance, Settlement, useApiPeerBalances, useApiSettlements } from './apiHooks'
|
||||||
|
|
||||||
interface UseAccountingHook {
|
interface UseAccountingHook {
|
||||||
@@ -80,11 +81,10 @@ export const useAccounting = (): UseAccountingHook => {
|
|||||||
const settlements = useApiSettlements()
|
const settlements = useApiSettlements()
|
||||||
const balances = useApiPeerBalances()
|
const balances = useApiPeerBalances()
|
||||||
|
|
||||||
const [err, setErr] = useState<Error | null>(null)
|
|
||||||
const [isLoadingUncashed, setIsloadingUncashed] = useState<boolean>(false)
|
const [isLoadingUncashed, setIsloadingUncashed] = useState<boolean>(false)
|
||||||
const [uncashedAmounts, setUncashedAmounts] = useState<LastCashoutActionResponse[] | undefined>(undefined)
|
const [uncashedAmounts, setUncashedAmounts] = useState<LastCashoutActionResponse[] | undefined>(undefined)
|
||||||
|
|
||||||
const error = balances.error || settlements.error || err
|
const error = balances.error || settlements.error
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// We don't have any settlements loaded yet or we are already loading/have loaded the uncashed amounts
|
// We don't have any settlements loaded yet or we are already loading/have loaded the uncashed amounts
|
||||||
@@ -93,12 +93,13 @@ export const useAccounting = (): UseAccountingHook => {
|
|||||||
setIsloadingUncashed(true)
|
setIsloadingUncashed(true)
|
||||||
const promises = settlements.settlements.settlements
|
const promises = settlements.settlements.settlements
|
||||||
.filter(({ received }) => received.toBigNumber.gt('0'))
|
.filter(({ received }) => received.toBigNumber.gt('0'))
|
||||||
.map(({ peer }) => beeDebugApi.chequebook.getPeerLastCashout(peer))
|
.map(({ peer }) => makeRetriablePromise(() => beeDebugApi.chequebook.getPeerLastCashout(peer)))
|
||||||
|
|
||||||
Promise.all(promises)
|
Promise.allSettled(promises).then(settlements => {
|
||||||
.then(setUncashedAmounts)
|
const results = unwrapPromiseSettlements(settlements)
|
||||||
.catch(setErr)
|
setUncashedAmounts(results.fulfilled)
|
||||||
.finally(() => setIsloadingUncashed(false))
|
setIsloadingUncashed(false)
|
||||||
|
})
|
||||||
}, [settlements, isLoadingUncashed, uncashedAmounts, error])
|
}, [settlements, isLoadingUncashed, uncashedAmounts, error])
|
||||||
|
|
||||||
const accounting = mergeAccounting(balances.peerBalances, settlements.settlements?.settlements, uncashedAmounts)
|
const accounting = mergeAccounting(balances.peerBalances, settlements.settlements?.settlements, uncashedAmounts)
|
||||||
|
|||||||
@@ -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}`)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user