Repository: urql-graphql/urql
Pull request #3174 (2023-04-20 16:38:04)

fix(core): Fix regression that broke cache-and-network
Resolves regression from #3157

## Summary

This prevents us from blocking `network-only` operations which follow `cache-and-network` operations, if the latter has queued them up while the cached result comes in.

In other words, if `result.stale` is set, we still allow `dispatched.delete` to remove the key and unblock it if the current operations queue contains that operation. This basically is the equivalent of saying that we unblock operations, even if they're marked as `stale` if a new operation for them is already waiting to be sent.

## Set of changes

- Allow operations to be run if they're queued up already while a `result.stale` marked result comes in



Patch:
diff --git a/.changeset/moody-hats-attend.md b/.changeset/moody-hats-attend.md
new file mode 100644
index 00000000..3b53a687
--- /dev/null
+++ b/.changeset/moody-hats-attend.md
@@ -0,0 +1,5 @@
+---
+'@urql/core': patch
+---
+
+Fix regression which would disallow `network-only` operations after `cache-and-network` completed.
diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts
index 5aafdf61..703b3764 100755
--- a/packages/core/src/client.ts
+++ b/packages/core/src/client.ts
@@ -663,8 +663,18 @@ export const Client: new (opts: ClientOptions) => Client = function Client(
         result$,
         // Store replay result
         onPush(result => {
-          if (!result.hasNext && !result.stale)
+          if (result.stale) {
+            // If the current result has queued up an operation of the same
+            // key, then `stale` refers to it
+            for (const operation of queue) {
+              if (operation.key === result.operation.key) {
+                dispatched.delete(operation.key);
+                break;
+              }
+            }
+          } else if (!result.hasNext) {
             dispatched.delete(operation.key);
+          }
           replays.set(operation.key, result);
         }),
         // Cleanup active states on end of source


Test patch:
diff --git a/packages/core/src/client.test.ts b/packages/core/src/client.test.ts
index 2e148ed2..dd14d6aa 100755
--- a/packages/core/src/client.test.ts
+++ b/packages/core/src/client.test.ts
@@ -648,6 +648,58 @@ describe('deduplication behavior', () => {
     expect(onOperation).toHaveBeenCalledTimes(1);
     expect(onResult).toHaveBeenCalledTimes(3);
   });
+
+  it('does not deduplicate cache-and-network’s follow-up operations', () => {
+    const onOperation = vi.fn();
+    const onResult = vi.fn();
+
+    const operationOne = makeOperation('query', queryOperation, {
+      ...queryOperation.context,
+      requestPolicy: 'cache-and-network',
+    });
+
+    const operationTwo = makeOperation('query', queryOperation, {
+      ...queryOperation.context,
+      requestPolicy: 'network-only',
+    });
+
+    let shouldSend = true;
+    const exchange: Exchange = () => ops$ =>
+      pipe(
+        ops$,
+        onPush(onOperation),
+        map(op => ({
+          hasNext: false,
+          stale: true,
+          data: 'test',
+          operation: op,
+        })),
+        filter(() => {
+          if (shouldSend) {
+            shouldSend = false;
+            client.reexecuteOperation(operationTwo);
+            return true;
+          } else {
+            return false;
+          }
+        })
+      );
+
+    const client = createClient({
+      url: 'test',
+      exchanges: [exchange],
+    });
+
+    const operationThree = makeOperation('query', queryOperation, {
+      ...queryOperation.context,
+      requestPolicy: 'network-only',
+    });
+
+    pipe(client.executeRequestOperation(operationOne), subscribe(onResult));
+    pipe(client.executeRequestOperation(operationThree), subscribe(onResult));
+
+    expect(onOperation).toHaveBeenCalledTimes(2);
+  });
 });
 
 describe('shared sources behavior', () => {