Created
June 2, 2023 16:51
-
-
Save norswap/6afea17f98af956ece1fca353a75371f to your computer and use it in GitHub Desktop.
Fetching abstraction with retries & throttling
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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