Skip to content

Instantly share code, notes, and snippets.

@norswap
Created June 2, 2023 16:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save norswap/6afea17f98af956ece1fca353a75371f to your computer and use it in GitHub Desktop.
Save norswap/6afea17f98af956ece1fca353a75371f to your computer and use it in GitHub Desktop.
Fetching abstraction with retries & throttling
/**
* TODO
*
* @module fetch
*/
import { sleep } from "src/utils/js-utils"
// =================================================================================================
export type FetchParams<Result> = {
/** The fetching function to execute. */
fetchFn: () => Promise<Result>
/**
* We will not trigger a new fetch if there is already a fetch in-flight, and there has been less
* than `throttlePeriod` milliseconds since it was fired.
*
* Unlike for instance lodash's throttle, we do enable back-to-back fetches, as long as a fetch
* request comes in after the previous fetch has completed.
*/
throttlePeriod: number
/**
* Defines the retry policy:
* - If false, never retry if the initial fetch fails.
* - If true, retry indefinitely.
* - If a number, retry at most that many times.
* - If a function, call it with the current retry count and the error, and retry if it returns true.
*/
retry: boolean|number|((count: number, error: Error) => boolean)
/**
* Defines the delay between two attempts to fetch (in milliseconds), based on the current number
* of attempts so far.
*/
retryDelay: (count: number) => number
}
// -------------------------------------------------------------------------------------------------
export function fetchParamsDefaults(): Partial<FetchParams<unknown>> {
return {
throttlePeriod: 2000,
retry: 3,
retryDelay: (count: number) => 1000 * (2 ^ count)
}
}
// -------------------------------------------------------------------------------------------------
function shouldRetry(params: FetchParams<any>, retryCount: number, error: Error): boolean {
if (typeof params.retry === "boolean")
return params.retry
if (typeof params.retry === "number")
return retryCount < params.retry
if (typeof params.retry === "function")
return params.retry(retryCount, error)
console.error("Retry parameter must be boolean, number or function:", params.retry)
return false
}
// -------------------------------------------------------------------------------------------------
export function fetch<Result>(params: FetchParams<Result>): () => Promise<Result> {
params = { ...fetchParamsDefaults(), ...params }
// Used for throttling
let lastRequestTimestamp = 0
// used to avoid "zombie" updates: old data overwriting newer game data.
let sequenceNumber = 1
let lastCompletedNumber = 0
return async () => {
const seqNum = sequenceNumber++
// Throttle
const timestamp = Date.now()
if (timestamp - lastRequestTimestamp < params.throttlePeriod)
return // there is a recent-ish refresh in flight
lastRequestTimestamp = timestamp
// Fetch, and handle retries
let retryCount = -1
let result: Result
let retry = shouldRetry(params, retryCount, null)
while (retry) {
++retryCount
if (retryCount > 0) await sleep(params.retryDelay(retryCount))
try {
result = await params.fetchFn()
} catch (e) {
retry = shouldRetry(params, retryCount, e)
}
}
// Filter zombie updates
if (seqNum < lastCompletedNumber) return
lastCompletedNumber = seqNum
// Allow another fetch immediately
lastRequestTimestamp = 0
return result
}
}
// =================================================================================================
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment