fetch-assets.ts 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. import picocolors from 'picocolors';
  2. import { $$fetch, defaultRequestInit, ResponseError } from './fetch-retry';
  3. import { waitWithAbort } from 'foxts/wait';
  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 async function fetchAssets(url: string, fallbackUrls: null | undefined | string[] | readonly string[]) {
  24. const controller = new AbortController();
  25. const createFetchFallbackPromise = async (url: string, index: number) => {
  26. if (index >= 0) {
  27. // Most assets can be downloaded within 250ms. To avoid wasting bandwidth, we will wait for 500ms before downloading from the fallback URL.
  28. try {
  29. await waitWithAbort(50 + (index + 1) * 100, controller.signal);
  30. } catch {
  31. console.log(picocolors.gray('[fetch cancelled early]'), picocolors.gray(url));
  32. throw new CustomAbortError();
  33. }
  34. }
  35. if (controller.signal.aborted) {
  36. console.log(picocolors.gray('[fetch cancelled]'), picocolors.gray(url));
  37. throw new CustomAbortError();
  38. }
  39. const res = await $$fetch(url, { signal: controller.signal, ...defaultRequestInit });
  40. const text = await res.text();
  41. if (text.length < 2) {
  42. throw new ResponseError(res, url, 'empty response w/o 304');
  43. }
  44. controller.abort();
  45. return text;
  46. };
  47. if (!fallbackUrls || fallbackUrls.length === 0) {
  48. return createFetchFallbackPromise(url, -1);
  49. }
  50. return Promise.any([
  51. createFetchFallbackPromise(url, -1),
  52. ...fallbackUrls.map(createFetchFallbackPromise)
  53. ]);
  54. }