import { default as createQueue, PRIORITY as QUEUE_PRIORITY, } from "../../utils/queue"; import { SsrError, SsrTimeoutError } from "../../others/errors"; import { SsrHttpRateLimitError, SsrHttpResponseError, SsrNetworkError, SsrNetworkTimeoutError, } from "../errors"; import { fetchHtml, fetchJson } from "../fetch"; import makePendingPromisePool from "../../utils/pending-promises"; import { AbortError } from "../../utils/promise"; const DEFAULT_RETRIES = 2; export const PRIORITY = { FG_HIGH: QUEUE_PRIORITY.HIGHEST, FG_LOW: QUEUE_PRIORITY.HIGH, BG_HIGH: QUEUE_PRIORITY.NORMAL, BG_NORMAL: QUEUE_PRIORITY.LOW, BG_LOW: QUEUE_PRIORITY.LOWEST, }; const resolvePromiseOrWaitForPending = makePendingPromisePool(); export default (options = {}) => { const { retries, rateLimitTick, ...queueOptions } = { retries: DEFAULT_RETRIES, rateLimitTick: 500, ...options, }; const queue = createQueue(queueOptions); const { add, emitter, ...queueToReturn } = queue; let lastRateLimitError = null; let rateLimitTimerId = null; let currentRateLimit = { waiting: 0, remaining: null, limit: null, resetAt: null, }; const rateLimitTicker = () => { const expiresInMs = lastRateLimitError && lastRateLimitError.resetAt ? lastRateLimitError.resetAt - new Date() + 1000 : 0; if (expiresInMs <= 0) { emitter.emit("waiting", { waiting: 0, remaining: null, limit: null, resetAt: null, }); if (rateLimitTimerId) clearTimeout(rateLimitTimerId); return; } const { remaining, limit, resetAt } = lastRateLimitError; emitter.emit("waiting", { waiting: expiresInMs, remaining, limit, resetAt, }); if (rateLimitTimerId) clearTimeout(rateLimitTimerId); rateLimitTimerId = setTimeout(rateLimitTicker, rateLimitTick); }; const retriedFetch = async ( fetchFunc, url, options, priority = PRIORITY.FG_LOW ) => { for (let i = 0; i <= retries; i++) { try { return await add(async () => { if (lastRateLimitError) { await lastRateLimitError.waitBeforeRetry(); lastRateLimitError = null; } return fetchFunc(url, options) .then((response) => { currentRateLimit = { ...response.rateLimit, waiting: 0 }; return response; }) .catch((err) => { if (err instanceof SsrTimeoutError) throw new SsrNetworkTimeoutError(err.timeout); throw err; }); }, priority); } catch (err) { if (err instanceof SsrHttpResponseError) { const { remaining, limit, resetAt } = err; currentRateLimit = { waiting: 0, remaining, limit, resetAt }; } if (err instanceof SsrNetworkError) { const shouldRetry = err.shouldRetry(); if (!shouldRetry || i === retries) throw err; if (err instanceof SsrHttpRateLimitError) { if ( err.remaining <= 0 && err.resetAt && (!lastRateLimitError || !lastRateLimitError.resetAt || lastRateLimitError.resetAt < err.resetAt) ) { lastRateLimitError = err; rateLimitTicker(); } } else { lastRateLimitError = null; } } else if (!(err instanceof DOMException)) { throw err; } else { throw AbortError(); } } } throw new SsrError("Unknown error"); }; const queuedFetchJson = async (url, options, priority = PRIORITY.FG_LOW) => resolvePromiseOrWaitForPending(url, () => retriedFetch(fetchJson, url, options, priority) ); const queuedFetchHtml = async (url, options, priority = PRIORITY.FG_LOW) => resolvePromiseOrWaitForPending(url, () => retriedFetch(fetchHtml, url, options, priority) ); const getRateLimit = () => currentRateLimit; return { fetchJson: queuedFetchJson, fetchHtml: queuedFetchHtml, getRateLimit, ...queueToReturn, }; };