fetch-assets.ts 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. import picocolors from 'picocolors';
  2. import { defaultRequestInit, fetchWithRetry } from './fetch-retry';
  3. import { setTimeout } from 'node:timers/promises';
  4. // eslint-disable-next-line sukka/unicorn/custom-error-definition -- typescript is better
  5. export class CustomAbortError extends Error {
  6. public readonly name = 'AbortError';
  7. public readonly digest = 'AbortError';
  8. }
  9. export class Custom304NotModifiedError extends Error {
  10. public readonly name = 'Custom304NotModifiedError';
  11. public readonly digest = 'Custom304NotModifiedError';
  12. constructor(public readonly url: string, public readonly data: string) {
  13. super('304 Not Modified');
  14. }
  15. }
  16. export class CustomNoETagFallbackError extends Error {
  17. public readonly name = 'CustomNoETagFallbackError';
  18. public readonly digest = 'CustomNoETagFallbackError';
  19. constructor(public readonly data: string) {
  20. super('No ETag Fallback');
  21. }
  22. }
  23. export const sleepWithAbort = (ms: number, signal: AbortSignal) => new Promise<void>((resolve, reject) => {
  24. if (signal.aborted) {
  25. reject(signal.reason as Error);
  26. return;
  27. }
  28. signal.addEventListener('abort', stop, { once: true });
  29. // eslint-disable-next-line sukka/prefer-timer-id -- node:timers/promises
  30. setTimeout(ms, undefined, { ref: false }).then(resolve).catch(reject).finally(() => signal.removeEventListener('abort', stop));
  31. function stop(this: AbortSignal) { reject(this.reason as Error); }
  32. });
  33. export async function fetchAssets(url: string, fallbackUrls: string[] | readonly string[]) {
  34. const controller = new AbortController();
  35. const createFetchFallbackPromise = async (url: string, index: number) => {
  36. if (index > 0) {
  37. // Most assets can be downloaded within 250ms. To avoid wasting bandwidth, we will wait for 500ms before downloading from the fallback URL.
  38. try {
  39. await sleepWithAbort(500 + (index + 1) * 10, controller.signal);
  40. } catch {
  41. console.log(picocolors.gray('[fetch cancelled early]'), picocolors.gray(url));
  42. throw new CustomAbortError();
  43. }
  44. }
  45. if (controller.signal.aborted) {
  46. console.log(picocolors.gray('[fetch cancelled]'), picocolors.gray(url));
  47. throw new CustomAbortError();
  48. }
  49. const res = await fetchWithRetry(url, { signal: controller.signal, ...defaultRequestInit });
  50. const text = await res.text();
  51. controller.abort();
  52. return text;
  53. };
  54. return Promise.any([
  55. createFetchFallbackPromise(url, -1),
  56. ...fallbackUrls.map(createFetchFallbackPromise)
  57. ]);
  58. }