{"version":3,"file":"urql-exchange-retry.mjs","sources":["../src/retryExchange.ts"],"sourcesContent":["import {\n  makeSubject,\n  pipe,\n  merge,\n  filter,\n  fromValue,\n  debounce,\n  mergeMap,\n  takeUntil,\n} from 'wonka';\n\nimport type { Exchange, Operation, CombinedError } from '@urql/core';\nimport { makeOperation } from '@urql/core';\n\n/** Input parameters for the {@link retryExchange}. */\nexport interface RetryExchangeOptions {\n  /** Specify the minimum time to wait until retrying.\n   *\n   * @remarks\n   * `initialDelayMs` specifies the minimum time (in milliseconds) to wait\n   * until a failed operation is retried.\n   *\n   * @defaultValue `1_000` - one second\n   */\n  initialDelayMs?: number;\n  /** Specifies the maximum time to wait until retrying.\n   *\n   * @remarks\n   * `maxDelayMs` specifies the maximum time (in milliseconds) to wait\n   * until a failed operation is retried. While `initialDelayMs`\n   * specifies the minimum amount of time, `randomDelay` may cause\n   * the delay to increase over multiple attempts.\n   *\n   * @defaultValue `15_000` - 15 seconds\n   */\n  maxDelayMs?: number;\n  /** Enables a random exponential backoff to increase the delay over multiple retries.\n   *\n   * @remarks\n   * `randomDelay`, unless disabled, increases the time until a failed\n   * operation is retried over multiple attempts. It increases the time\n   * starting at `initialDelayMs` by 1.5x with an added factor of 0–1,\n   * until `maxDelayMs` is reached.\n   *\n   * @defaultValue `true` - enables random exponential backoff\n   */\n  randomDelay?: boolean;\n  /** Specifies the maximum times an operation should be sent to the API.\n   *\n   * @remarks\n   * `maxNumberAttempts` defines the number of attempts an operation should\n   * be retried until it's considered failed.\n   *\n   * @defaultValue `2` - Retry once, i.e. two attempts\n   */\n  maxNumberAttempts?: number;\n  /** Predicate allowing you to selectively not retry `Operation`s.\n   *\n   * @remarks\n   * `retryIf` is called with a {@link CombinedError} and the {@link Operation} that\n   * failed. If this function returns false the failed `Operation` is not retried.\n   *\n   * @defaultValue `(error) => !!error.networkError` - retries only on network errors.\n   */\n  retryIf?(error: CombinedError, operation: Operation): boolean;\n  /** Transform function allowing you to selectively replace a retried `Operation` or return nullish value.\n   *\n   * @remarks\n   * `retryWhen` is called with a {@link CombinedError} and the {@link Operation} that\n   * failed. If this function returns an `Operation`, `retryExchange` will replace the\n   * failed `Operation` and retry. It won't retry the `Operation` if a nullish value\n   * is returned.\n   *\n   * The `retryIf` function, if defined, takes precedence and overrides this option.\n   */\n  retryWith?(\n    error: CombinedError,\n    operation: Operation\n  ): Operation | null | undefined;\n}\n\ninterface RetryState {\n  count: number;\n  delay: number | null;\n}\n\n/** Exchange factory that retries failed operations.\n *\n * @param options - A {@link RetriesExchangeOptions} configuration object.\n * @returns the created retry {@link Exchange}.\n *\n * @remarks\n * The `retryExchange` retries failed operations with specified delays\n * and exponential backoff.\n *\n * You may define a {@link RetryExchangeOptions.retryIf} or\n * {@link RetryExchangeOptions.retryWhen} function to only retry\n * certain kinds of operations, e.g. only queries.\n *\n * @example\n * ```ts\n * retryExchange({\n *   initialDelayMs: 1000,\n *   maxDelayMs: 15000,\n *   randomDelay: true,\n *   maxNumberAttempts: 2,\n *   retryIf: err => err && err.networkError,\n * });\n * ```\n */\nexport const retryExchange = (options: RetryExchangeOptions): Exchange => {\n  const { retryIf, retryWith } = options;\n  const MIN_DELAY = options.initialDelayMs || 1000;\n  const MAX_DELAY = options.maxDelayMs || 15_000;\n  const MAX_ATTEMPTS = options.maxNumberAttempts || 2;\n  const RANDOM_DELAY =\n    options.randomDelay != null ? !!options.randomDelay : true;\n\n  return ({ forward, dispatchDebug }) =>\n    operations$ => {\n      const { source: retry$, next: nextRetryOperation } =\n        makeSubject<Operation>();\n\n      const retryWithBackoff$ = pipe(\n        retry$,\n        mergeMap((operation: Operation) => {\n          const retry: RetryState = operation.context.retry || {\n            count: 0,\n            delay: null,\n          };\n\n          const retryCount = ++retry.count;\n          let delayAmount = retry.delay || MIN_DELAY;\n\n          const backoffFactor = Math.random() + 1.5;\n          if (RANDOM_DELAY) {\n            // if randomDelay is enabled and it won't exceed the max delay, apply a random\n            // amount to the delay to avoid thundering herd problem\n            if (delayAmount * backoffFactor < MAX_DELAY) {\n              delayAmount *= backoffFactor;\n            } else {\n              delayAmount = MAX_DELAY;\n            }\n          } else {\n            // otherwise, increase the delay proportionately by the initial delay\n            delayAmount = Math.min(retryCount * MIN_DELAY, MAX_DELAY);\n          }\n\n          // ensure the delay is carried over to the next context\n          retry.delay = delayAmount;\n\n          // We stop the retries if a teardown event for this operation comes in\n          // But if this event comes through regularly we also stop the retries, since it's\n          // basically the query retrying itself, no backoff should be added!\n          const teardown$ = pipe(\n            operations$,\n            filter(op => {\n              return (\n                (op.kind === 'query' || op.kind === 'teardown') &&\n                op.key === operation.key\n              );\n            })\n          );\n\n          dispatchDebug({\n            type: 'retryAttempt',\n            message: `The operation has failed and a retry has been triggered (${retryCount} / ${MAX_ATTEMPTS})`,\n            operation,\n            data: {\n              retryCount,\n              delayAmount,\n            },\n          });\n\n          // Add new retryDelay and retryCount to operation\n          return pipe(\n            fromValue(\n              makeOperation(operation.kind, operation, {\n                ...operation.context,\n                retry,\n              })\n            ),\n            debounce(() => delayAmount),\n            // Stop retry if a teardown comes in\n            takeUntil(teardown$)\n          );\n        })\n      );\n\n      return pipe(\n        merge([operations$, retryWithBackoff$]),\n        forward,\n        filter(res => {\n          const retry = res.operation.context.retry as RetryState | undefined;\n          // Only retry if the error passes the conditional retryIf function (if passed)\n          // or if the error contains a networkError\n          if (\n            !res.error ||\n            (retryIf\n              ? !retryIf(res.error, res.operation)\n              : !retryWith && !res.error.networkError)\n          ) {\n            // Reset the delay state for a successful operation\n            if (retry) {\n              retry.count = 0;\n              retry.delay = null;\n            }\n            return true;\n          }\n\n          const maxNumberAttemptsExceeded =\n            ((retry && retry.count) || 0) >= MAX_ATTEMPTS - 1;\n          if (!maxNumberAttemptsExceeded) {\n            const operation = retryWith\n              ? retryWith(res.error, res.operation)\n              : res.operation;\n            if (!operation) return true;\n\n            // Send failed responses to be retried by calling next on the retry$ subject\n            // Exclude operations that have been retried more than the specified max\n            nextRetryOperation(operation);\n            return false;\n          }\n\n          dispatchDebug({\n            type: 'retryExhausted',\n            message:\n              'Maximum number of retries has been reached. No further retries will be performed.',\n            operation: res.operation,\n          });\n\n          return true;\n        })\n      );\n    };\n};\n"],"names":["retryExchange","options","retryIf","retryWith","MIN_DELAY","initialDelayMs","MAX_DELAY","maxDelayMs","MAX_ATTEMPTS","maxNumberAttempts","RANDOM_DELAY","randomDelay","forward","dispatchDebug","operations$","source","retry$","next","nextRetryOperation","makeSubject","retryWithBackoff$","mergeMap","operation","retry","context","count","delay","retryCount","delayAmount","backoffFactor","Math","random","min","teardown$","filter","op","kind","key","process","env","NODE_ENV","type","message","data","takeUntil","debounce","fromValue","makeOperation","res","error","networkError","merge"],"mappings":";;;;AA8GaA,IAAAA,gBAAiBC;EAC5B,KAAMC,SAAEA,GAAOC,WAAEA,KAAcF;EAC/B,IAAMG,IAAYH,EAAQI,kBAAkB;EAC5C,IAAMC,IAAYL,EAAQM,cAAc;EACxC,IAAMC,IAAeP,EAAQQ,qBAAqB;EAClD,IAAMC,IACmB,QAAvBT,EAAQU,gBAAwBV,EAAQU,eAAc;EAExD,OAAO,EAAGC,YAASC,sBACjBC;IACE,KAAQC,QAAQC,GAAQC,MAAMC,KAC5BC;IAEF,IAAMC,IAEJC,GAAUC;MACR,IAAMC,IAAoBD,EAAUE,QAAQD,SAAS;QACnDE,OAAO;QACPC,OAAO;;MAGT,IAAMC,MAAeJ,EAAME;MAC3B,IAAIG,IAAcL,EAAMG,SAAStB;MAEjC,IAAMyB,IAAgBC,KAAKC,WAAW;MACtC,IAAIrB;QAGF,IAAIkB,IAAcC,IAAgBvB;UAChCsB,KAAeC;;UAEfD,IAActB;;;QAIhBsB,IAAcE,KAAKE,IAAIL,IAAavB,GAAWE;;MAIjDiB,EAAMG,QAAQE;MAKd,IAAMK,IAEJC,GAAOC,MAEU,YAAZA,EAAGC,QAAgC,eAAZD,EAAGC,SAC3BD,EAAGE,QAAQf,EAAUe,KAHzBH,CADApB;MASF,iBAAAwB,QAAAC,IAAAC,YAAA3B,EAAc;QACZ4B,MAAM;QACNC,SAAU,4DAA2Df,OAAgBnB;QACrFc;QACAqB,MAAM;UACJhB;UACAC;;QACDb,QAAA;;MAIH,OASE6B,EAAUX,EATZ,CAOEY,GAAS,MAAMjB,GAAfiB,CANAC,EACEC,EAAczB,EAAUc,MAAMd,GAAW;WACpCA,EAAUE;QACbD;;AAEH,OAxDLF,CADAL;IAiEF,OAGEkB,GAAOc;MACL,IAAMzB,IAAQyB,EAAI1B,UAAUE,QAAQD;MAGpC,MACGyB,EAAIC,UACJ/C,IACIA,EAAQ8C,EAAIC,OAAOD,EAAI1B,aACvBnB,KAAc6C,EAAIC,MAAMC,gBAC7B;QAEA,IAAI3B,GAAO;UACTA,EAAME,QAAQ;UACdF,EAAMG,QAAQ;AAChB;QACA,QAAO;AACT;MAIA,OADIH,KAASA,EAAME,SAAU,MAAMjB,IAAe,IAClB;QAC9B,IAAMc,IAAYnB,IACdA,EAAU6C,EAAIC,OAAOD,EAAI1B,aACzB0B,EAAI1B;QACR,KAAKA;UAAW,QAAO;;QAIvBJ,EAAmBI;QACnB,QAAO;AACT;MAEA,iBAAAgB,QAAAC,IAAAC,YAAA3B,EAAc;QACZ4B,MAAM;QACNC,SACE;QACFpB,WAAW0B,EAAI1B;QAASP,QAAA;;MAG1B,QAAO;AAAI,OAvCbmB,CADAtB,EADAuC,EAAM,EAACrC,GAAaM;AAAmB;AA4C1C;;"}