index.js 2.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. import retry from 'retry';
  2. import isNetworkError from 'is-network-error';
  3. export class AbortError extends Error {
  4. constructor(message) {
  5. super();
  6. if (message instanceof Error) {
  7. this.originalError = message;
  8. ({message} = message);
  9. } else {
  10. this.originalError = new Error(message);
  11. this.originalError.stack = this.stack;
  12. }
  13. this.name = 'AbortError';
  14. this.message = message;
  15. }
  16. }
  17. const decorateErrorWithCounts = (error, attemptNumber, options) => {
  18. // Minus 1 from attemptNumber because the first attempt does not count as a retry
  19. const retriesLeft = options.retries - (attemptNumber - 1);
  20. error.attemptNumber = attemptNumber;
  21. error.retriesLeft = retriesLeft;
  22. return error;
  23. };
  24. export default async function pRetry(input, options) {
  25. return new Promise((resolve, reject) => {
  26. options = {...options};
  27. options.onFailedAttempt ??= () => {};
  28. options.shouldRetry ??= () => true;
  29. options.retries ??= 10;
  30. const operation = retry.operation(options);
  31. const abortHandler = () => {
  32. operation.stop();
  33. reject(options.signal?.reason);
  34. };
  35. if (options.signal && !options.signal.aborted) {
  36. options.signal.addEventListener('abort', abortHandler, {once: true});
  37. }
  38. const cleanUp = () => {
  39. options.signal?.removeEventListener('abort', abortHandler);
  40. operation.stop();
  41. };
  42. operation.attempt(async attemptNumber => {
  43. try {
  44. const result = await input(attemptNumber);
  45. cleanUp();
  46. resolve(result);
  47. } catch (error) {
  48. try {
  49. if (!(error instanceof Error)) {
  50. throw new TypeError(`Non-error was thrown: "${error}". You should only throw errors.`);
  51. }
  52. if (error instanceof AbortError) {
  53. throw error.originalError;
  54. }
  55. if (error instanceof TypeError && !isNetworkError(error)) {
  56. throw error;
  57. }
  58. decorateErrorWithCounts(error, attemptNumber, options);
  59. if (!(await options.shouldRetry(error))) {
  60. operation.stop();
  61. reject(error);
  62. }
  63. await options.onFailedAttempt(error);
  64. if (!operation.retry(error)) {
  65. throw operation.mainError();
  66. }
  67. } catch (finalError) {
  68. decorateErrorWithCounts(finalError, attemptNumber, options);
  69. cleanUp();
  70. reject(finalError);
  71. }
  72. }
  73. });
  74. });
  75. }