fetch-assets.ts 2.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
  1. import picocolors from 'picocolors';
  2. import { defaultRequestInit, fetchWithRetry } from './fetch-retry';
  3. import { setTimeout } from 'timers/promises';
  4. class CustomAbortError extends Error {
  5. public readonly name = 'AbortError';
  6. public readonly digest = 'AbortError';
  7. }
  8. const sleepWithAbort = (ms: number, signal: AbortSignal) => new Promise<void>((resolve, reject) => {
  9. if (signal.aborted) {
  10. reject(signal.reason as Error);
  11. return;
  12. }
  13. function stop(this: AbortSignal) { reject(this.reason as Error); }
  14. signal.addEventListener('abort', stop, { once: true });
  15. setTimeout(ms, undefined, { ref: false }).then(resolve).catch(reject).finally(() => signal.removeEventListener('abort', stop));
  16. });
  17. export async function fetchAssets(url: string, fallbackUrls: string[] | readonly string[]) {
  18. const controller = new AbortController();
  19. const fetchMainPromise = fetchWithRetry(url, { signal: controller.signal, ...defaultRequestInit })
  20. .then(r => r.text())
  21. .then(text => {
  22. controller.abort();
  23. return text;
  24. });
  25. const createFetchFallbackPromise = async (url: string, index: number) => {
  26. // Most assets can be downloaded within 250ms. To avoid wasting bandwidth, we will wait for 500ms before downloading from the fallback URL.
  27. try {
  28. await sleepWithAbort(500 + (index + 1) * 20, controller.signal);
  29. } catch {
  30. console.log(picocolors.gray('[fetch cancelled early]'), picocolors.gray(url));
  31. throw new CustomAbortError();
  32. }
  33. if (controller.signal.aborted) {
  34. console.log(picocolors.gray('[fetch cancelled]'), picocolors.gray(url));
  35. throw new CustomAbortError();
  36. }
  37. const res = await fetchWithRetry(url, { signal: controller.signal, ...defaultRequestInit });
  38. const text = await res.text();
  39. controller.abort();
  40. return text;
  41. };
  42. return Promise.any([
  43. fetchMainPromise,
  44. ...fallbackUrls.map(createFetchFallbackPromise)
  45. ]).catch(e => {
  46. console.log(`Download Rule for [${url}] failed`);
  47. throw e;
  48. });
  49. }