Repository: FormidableLabs/urql
Pull request #1881 (2021-08-23 14:46:11)

(retry) - Add retryWith option to update operations

changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: d1142ea2632a4ba2141af831163ac2e70490038e

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                 | Type  |
| -------------------- | ----- |
| @urql/exchange-retry | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/FormidableLabs/urql/new/feat/retry-with?filename=.changeset/moody-pugs-destroy.md&value=---%0A%22%40urql%2Fexchange-retry%22%3A%20patch%0A---%0A%0A(retry)%20-%20Add%20retryWith%20option%20to%20update%20operations%0A)



Patch:
diff --git a/.changeset/stale-candles-enjoy.md b/.changeset/stale-candles-enjoy.md
new file mode 100644
index 00000000..0a70237b
--- /dev/null
+++ b/.changeset/stale-candles-enjoy.md
@@ -0,0 +1,5 @@
+---
+'@urql/exchange-retry': minor
+---
+
+Add a new `retryWith` option which allows operations to be updated when a request is being retried.
diff --git a/exchanges/retry/src/retryExchange.ts b/exchanges/retry/src/retryExchange.ts
index a0e60d0b..1fcc2823 100644
--- a/exchanges/retry/src/retryExchange.ts
+++ b/exchanges/retry/src/retryExchange.ts
@@ -25,6 +25,11 @@ export interface RetryExchangeOptions {
   maxNumberAttempts?: number;
   /** Conditionally determine whether an error should be retried */
   retryIf?: (error: CombinedError, operation: Operation) => boolean;
+  /** Conditionally update operations as they're retried (retryIf can be replaced with this) */
+  retryWith?: (
+    error: CombinedError,
+    operation: Operation
+  ) => Operation | null | undefined;
 }
 
 export const retryExchange = ({
@@ -32,16 +37,14 @@ export const retryExchange = ({
   maxDelayMs,
   randomDelay,
   maxNumberAttempts,
-  retryIf: retryIfOption,
+  retryIf,
+  retryWith,
 }: RetryExchangeOptions): Exchange => {
   const MIN_DELAY = initialDelayMs || 1000;
   const MAX_DELAY = maxDelayMs || 15000;
   const MAX_ATTEMPTS = maxNumberAttempts || 2;
   const RANDOM_DELAY = randomDelay || true;
 
-  const retryIf =
-    retryIfOption || ((err: CombinedError) => err && err.networkError);
-
   return ({ forward, dispatchDebug }) => ops$ => {
     const sharedOps$ = pipe(ops$, share);
     const {
@@ -107,7 +110,12 @@ export const retryExchange = ({
       filter(res => {
         // Only retry if the error passes the conditional retryIf function (if passed)
         // or if the error contains a networkError
-        if (!res.error || !retryIf(res.error, res.operation)) {
+        if (
+          !res.error ||
+          (retryIf
+            ? !retryIf(res.error, res.operation)
+            : !retryWith && !res.error.networkError)
+        ) {
           return true;
         }
 
@@ -115,9 +123,14 @@ export const retryExchange = ({
           (res.operation.context.retryCount || 0) >= MAX_ATTEMPTS - 1;
 
         if (!maxNumberAttemptsExceeded) {
+          const operation = retryWith
+            ? retryWith(res.error, res.operation)
+            : res.operation;
+          if (!operation) return true;
+
           // Send failed responses to be retried by calling next on the retry$ subject
           // Exclude operations that have been retried more than the specified max
-          nextRetryOperation(res.operation);
+          nextRetryOperation(operation);
           return false;
         }
 


Test patch:
diff --git a/exchanges/retry/src/retryExchange.test.ts b/exchanges/retry/src/retryExchange.test.ts
index 7cf3d224..9b9248df 100644
--- a/exchanges/retry/src/retryExchange.test.ts
+++ b/exchanges/retry/src/retryExchange.test.ts
@@ -3,6 +3,7 @@ import { pipe, map, makeSubject, publish, tap } from 'wonka';
 import {
   gql,
   createClient,
+  makeOperation,
   Operation,
   OperationResult,
   ExchangeIO,
@@ -227,3 +228,105 @@ it(`should still retry if retryIf undefined but there is a networkError`, () =>
   expect(response).toHaveBeenCalledTimes(mockOptions.maxNumberAttempts);
   expect(result).toHaveBeenCalledTimes(1);
 });
+
+it('should allow retryWhen to return falsy value and act as replacement of retryIf', () => {
+  const errorWithNetworkError = {
+    ...queryOneError,
+    networkError: 'scary network error',
+  };
+  const response = jest.fn(
+    (forwardOp: Operation): OperationResult => {
+      expect(forwardOp.key).toBe(op.key);
+      return {
+        operation: forwardOp,
+        // @ts-ignore
+        error: errorWithNetworkError,
+      };
+    }
+  );
+
+  const result = jest.fn();
+  const forward: ExchangeIO = ops$ => {
+    return pipe(ops$, map(response));
+  };
+
+  const retryWith = jest.fn(() => null);
+
+  pipe(
+    retryExchange({
+      ...mockOptions,
+      retryIf: undefined,
+      retryWith,
+    })({
+      forward,
+      client,
+      dispatchDebug,
+    })(ops$),
+    tap(result),
+    publish
+  );
+
+  next(op);
+
+  jest.runAllTimers();
+
+  // max number of retries, plus original call
+  expect(retryWith).toHaveBeenCalledTimes(1);
+  expect(response).toHaveBeenCalledTimes(1);
+  expect(result).toHaveBeenCalledTimes(1);
+});
+
+it('should allow retryWhen to return new operations when retrying', () => {
+  const errorWithNetworkError = {
+    ...queryOneError,
+    networkError: 'scary network error',
+  };
+  const response = jest.fn(
+    (forwardOp: Operation): OperationResult => {
+      expect(forwardOp.key).toBe(op.key);
+      return {
+        operation: forwardOp,
+        // @ts-ignore
+        error: errorWithNetworkError,
+      };
+    }
+  );
+
+  const result = jest.fn();
+  const forward: ExchangeIO = ops$ => {
+    return pipe(ops$, map(response));
+  };
+
+  const retryWith = jest.fn((_error, operation) => {
+    return makeOperation(operation.kind, operation, {
+      ...operation.context,
+      counter: (operation.context?.counter || 0) + 1,
+    });
+  });
+
+  pipe(
+    retryExchange({
+      ...mockOptions,
+      retryIf: undefined,
+      retryWith,
+    })({
+      forward,
+      client,
+      dispatchDebug,
+    })(ops$),
+    tap(result),
+    publish
+  );
+
+  next(op);
+
+  jest.runAllTimers();
+
+  // max number of retries, plus original call
+  expect(retryWith).toHaveBeenCalledTimes(mockOptions.maxNumberAttempts - 1);
+  expect(response).toHaveBeenCalledTimes(mockOptions.maxNumberAttempts);
+  expect(result).toHaveBeenCalledTimes(1);
+
+  expect(response.mock.calls[1][0]).toHaveProperty('context.counter', 1);
+  expect(response.mock.calls[2][0]).toHaveProperty('context.counter', 2);
+});