Repository: urql-graphql/urql
Pull request #2872 (2022-12-09 10:47:09)

fix(core): add cache ssr hit
## Summary

Discovered in https://github.com/urql-graphql/urql/discussions/2867 when our ssr-exchange fufills a cache hit we incorrectly mark it as a cache-miss.


Patch:
diff --git a/.changeset/big-melons-leave.md b/.changeset/big-melons-leave.md
new file mode 100644
index 00000000..6fffd6a0
--- /dev/null
+++ b/.changeset/big-melons-leave.md
@@ -0,0 +1,5 @@
+---
+'@urql/core': patch
+---
+
+Correctly mark cache-hits from the ssr-exchange
diff --git a/packages/core/src/exchanges/ssr.ts b/packages/core/src/exchanges/ssr.ts
index 9505e13e..9f9fe3a1 100644
--- a/packages/core/src/exchanges/ssr.ts
+++ b/packages/core/src/exchanges/ssr.ts
@@ -1,7 +1,7 @@
 import { GraphQLError } from 'graphql';
 import { pipe, share, filter, merge, map, tap } from 'wonka';
 import { Exchange, OperationResult, Operation } from '../types';
-import { CombinedError } from '../utils';
+import { addMetadata, CombinedError } from '../utils';
 import { reexecuteOperation } from './cache';
 
 export interface SerializedResult {
@@ -145,13 +145,24 @@ export const ssrExchange = (params: SSRExchangeParams = {}): SSRExchange => {
       ),
       map(op => {
         const serialized = data[op.key]!;
-        const result = deserializeResult(op, serialized, includeExtensions);
+        const cachedResult = deserializeResult(
+          op,
+          serialized,
+          includeExtensions
+        );
+
         if (staleWhileRevalidate && !revalidated.has(op.key)) {
-          result.stale = true;
+          cachedResult.stale = true;
           revalidated.add(op.key);
           reexecuteOperation(client, op);
         }
 
+        const result: OperationResult = {
+          ...cachedResult,
+          operation: addMetadata(op, {
+            cacheOutcome: 'hit',
+          }),
+        };
         return result;
       })
     );


Test patch:
diff --git a/packages/core/src/exchanges/ssr.test.ts b/packages/core/src/exchanges/ssr.test.ts
index a2f72131..f2d1dd55 100644
--- a/packages/core/src/exchanges/ssr.test.ts
+++ b/packages/core/src/exchanges/ssr.test.ts
@@ -196,7 +196,18 @@ it('resolves cached query results correctly', () => {
   const data = ssr.extractData();
   expect(Object.keys(data).length).toBe(1);
   expect(output).not.toHaveBeenCalled();
-  expect(onPush).toHaveBeenCalledWith(queryResponse);
+  expect(onPush).toHaveBeenCalledWith({
+    ...queryResponse,
+    operation: {
+      ...queryResponse.operation,
+      context: {
+        ...queryResponse.operation.context,
+        meta: {
+          cacheOutcome: 'hit',
+        },
+      },
+    },
+  });
 });
 
 it('resolves deferred, cached query results correctly', () => {
@@ -222,7 +233,19 @@ it('resolves deferred, cached query results correctly', () => {
   expect(Object.keys(data).length).toBe(1);
   expect(output).toHaveBeenCalledTimes(1);
   expect(onPush).toHaveBeenCalledTimes(2);
-  expect(onPush.mock.calls[1][0]).toEqual({ hasNext: true, ...queryResponse });
+  expect(onPush.mock.calls[1][0]).toEqual({
+    hasNext: true,
+    ...queryResponse,
+    operation: {
+      ...queryResponse.operation,
+      context: {
+        ...queryResponse.operation.context,
+        meta: {
+          cacheOutcome: 'hit',
+        },
+      },
+    },
+  });
 });
 
 it('deletes cached results in non-suspense environments', async () => {
@@ -242,7 +265,18 @@ it('deletes cached results in non-suspense environments', async () => {
   await Promise.resolve();
 
   expect(Object.keys(ssr.extractData()).length).toBe(0);
-  expect(onPush).toHaveBeenCalledWith(queryResponse);
+  expect(onPush).toHaveBeenCalledWith({
+    ...queryResponse,
+    operation: {
+      ...queryResponse.operation,
+      context: {
+        ...queryResponse.operation.context,
+        meta: {
+          cacheOutcome: 'hit',
+        },
+      },
+    },
+  });
 
   // NOTE: The operation should not be duplicated
   expect(output).not.toHaveBeenCalled();