Repository: FormidableLabs/urql (ts)

Issue:
(retry) - Allow retryIf to avoid retrying subscriptions on non-network error
<!--
  Please describe your bug clearly and concisely.
  Include a screenshot or a reproduction to illustrate
  the bug and how to reproduce it.
  Please also leave a note on what version of urql you're using
  and include a list of exchanges (if any).
-->

i use both retry-exchange and subscriptions. if there is an error on the server, while generating a subscription-event, then the error correctly arrives to the client, retry-middleware does a retry, and from this point on, `useSubscription` will be triggered multiple times for the same event.

urql version & exchanges:
urql=1.10.2 or 1.10.3
retry-exchange=0.1.10

**Steps to reproduce**

i created a codesandbox-example, this is the server:
https://codesandbox.io/s/urql-sub-retry-problem-server-6rrhd (nothing special here)
this is the client:
https://codesandbox.io/s/urql-sub-retry-problem-client-h8ie7

open the client, open the url with the running example, and open devtools, watch the console-output. (do not leave this running too long, or the tab might become unresponsive)
the server is set up so that it generates a new event every second, and has a 60% chance to `throw` while doing so.
while you get no errors, the new events arrive correctly, but after the first error happens, retry happens, and things get to arrive duplicated. when more errors happen, it becomes even more duplicated.

if you change the codesandbox example, to switch `urql` to version `1.10.1`, then it is suddenly fixed. it now works correctly.

unfortunately, in my local codebase reverting to `urql=1.10.1` does not fix the problem. i do not understand why, maybe a combination of package-versions?

**Expected behavior**
actually i am not even sure what exactly should happen, a subscription-event cannot really be retried. but duplicate events should not happen.

**Actual behavior**
i am getting duplicate events.

<!--
  Your issue will be addressed much faster if you include a
  reproduction. Here are some templates to help you create one:
  - client: https://codesandbox.io/s/urql-issue-template-client-iui0o
  - server: https://codesandbox.io/s/urql-issue-template-server-0ufyz
-->


Pull request description:
(retry) - Add operation argument to retryIf predicate
Resolve #1116 

## Summary

Suppose there's a `error.graphqlErrors` error and that triggers a retry. This type of error won't have broken the operation pipe for subscriptions, so retrying will start an identical operation in that case for subscriptions, depending on the transport protocol and method that's used.

We could also consider adding a small dedup predicate to `subscriptionExchange` but for now we should allow the `retryExchange` to be configured to ignore certain cases per operation kind.

## Set of changes

- Add `operation: Operation` argument to `retryIf` predicate


Patch:
diff --git a/.changeset/gentle-grapes-love.md b/.changeset/gentle-grapes-love.md
new file mode 100644
index 00000000..1bc2cd67
--- /dev/null
+++ b/.changeset/gentle-grapes-love.md
@@ -0,0 +1,5 @@
+---
+'@urql/exchange-retry': minor
+---
+
+Add a second `Operation` input argument to the `retryIf` predicate, so that retrying can be actively avoided for specific types of operations, e.g. mutations or subscriptions, in certain user-defined cases.
diff --git a/exchanges/retry/src/retryExchange.ts b/exchanges/retry/src/retryExchange.ts
index 3daf89eb..8cf2bc82 100644
--- a/exchanges/retry/src/retryExchange.ts
+++ b/exchanges/retry/src/retryExchange.ts
@@ -24,7 +24,7 @@ interface RetryExchangeOptions {
   randomDelay?: boolean;
   maxNumberAttempts?: number;
   /** Conditionally determine whether an error should be retried */
-  retryIf?: (e: CombinedError) => boolean;
+  retryIf?: (error: CombinedError, operation: Operation) => boolean;
 }
 
 export const retryExchange = ({
@@ -106,7 +106,7 @@ 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)) {
+        if (!res.error || !retryIf(res.error, res.operation)) {
           return true;
         }
 


Test patch:
diff --git a/exchanges/retry/src/retryExchange.test.ts b/exchanges/retry/src/retryExchange.test.ts
index 449c7425..205a6031 100644
--- a/exchanges/retry/src/retryExchange.test.ts
+++ b/exchanges/retry/src/retryExchange.test.ts
@@ -116,7 +116,7 @@ it(`retries if it hits an error and works for multiple concurrent operations`, (
   next(op);
 
   expect(mockRetryIf).toHaveBeenCalledTimes(1);
-  expect(mockRetryIf).toHaveBeenCalledWith(queryOneError);
+  expect(mockRetryIf).toHaveBeenCalledWith(queryOneError, op);
 
   jest.runAllTimers();
 
@@ -131,7 +131,7 @@ it(`retries if it hits an error and works for multiple concurrent operations`, (
 
   jest.runAllTimers();
 
-  expect(mockRetryIf).toHaveBeenCalledWith(queryTwoError);
+  expect(mockRetryIf).toHaveBeenCalledWith(queryTwoError, opTwo);
 
   // max number of retries for each op
   expect(response).toHaveBeenCalledTimes(mockOptions.maxNumberAttempts * 2);
@@ -176,7 +176,7 @@ it('should retry x number of times and then return the successful result', () =>
   jest.runAllTimers();
 
   expect(mockRetryIf).toHaveBeenCalledTimes(numberRetriesBeforeSuccess);
-  expect(mockRetryIf).toHaveBeenCalledWith(queryOneError);
+  expect(mockRetryIf).toHaveBeenCalledWith(queryOneError, op);
 
   // one for original source, one for retry
   expect(response).toHaveBeenCalledTimes(1 + numberRetriesBeforeSuccess);