fetch-assets.ts 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. import picocolors from 'picocolors';
  2. import { defaultRequestInit, requestWithLog, ResponseError } from './fetch-retry';
  3. import { wait } 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 function sleepWithAbort(ms: number, signal: AbortSignal) {
  24. return new Promise<void>((resolve, reject) => {
  25. if (signal.aborted) {
  26. reject(signal.reason as Error);
  27. return;
  28. }
  29. signal.addEventListener('abort', stop, { once: true });
  30. wait(ms).then(resolve).catch(reject).finally(() => signal.removeEventListener('abort', stop));
  31. function stop(this: AbortSignal) { reject(this.reason as Error); }
  32. });
  33. }
  34. export async function fetchAssetsWithout304(url: string, fallbackUrls: string[] | readonly string[]) {
  35. const controller = new AbortController();
  36. const createFetchFallbackPromise = async (url: string, index: number) => {
  37. if (index > 0) {
  38. // Most assets can be downloaded within 250ms. To avoid wasting bandwidth, we will wait for 500ms before downloading from the fallback URL.
  39. try {
  40. await sleepWithAbort(500 + (index + 1) * 10, controller.signal);
  41. } catch {
  42. console.log(picocolors.gray('[fetch cancelled early]'), picocolors.gray(url));
  43. throw new CustomAbortError();
  44. }
  45. }
  46. if (controller.signal.aborted) {
  47. console.log(picocolors.gray('[fetch cancelled]'), picocolors.gray(url));
  48. throw new CustomAbortError();
  49. }
  50. const res = await requestWithLog(url, { signal: controller.signal, ...defaultRequestInit });
  51. const text = await res.body.text();
  52. if (text.length < 2) {
  53. throw new ResponseError(res, url, 'empty response w/o 304');
  54. }
  55. controller.abort();
  56. return text;
  57. };
  58. return Promise.any([
  59. createFetchFallbackPromise(url, -1),
  60. ...fallbackUrls.map(createFetchFallbackPromise)
  61. ]);
  62. }