Repository: urql-graphql/urql
Pull request #3137 (2023-04-06 17:04:44)

fix(core): Fix early closing of subscriptions
## Summary

Fixes an issue where `hasNext` would be updated to `false` and a subscription would be ended early when the `subscriptionExchange`’s sink doesn't actually set `hasNext: true` on a following result.

This is a similar issue to the missing `defaultHasNext` in `makeResult` we had to add.

## Set of changes

- Add default `hasNext` value to merged results
- Add unit tests



Patch:
diff --git a/.changeset/nice-flowers-think.md b/.changeset/nice-flowers-think.md
new file mode 100644
index 00000000..89604532
--- /dev/null
+++ b/.changeset/nice-flowers-think.md
@@ -0,0 +1,5 @@
+---
+'@urql/core': patch
+---
+
+Fix `hasNext` being defaulted to `false` when a new subscription event is received on the `subscriptionExchange` that doesn't have `hasNext` set.
diff --git a/packages/core/src/utils/result.ts b/packages/core/src/utils/result.ts
index d2dd879b..f1d20bfa 100644
--- a/packages/core/src/utils/result.ts
+++ b/packages/core/src/utils/result.ts
@@ -141,7 +141,8 @@ export const mergeResultPatch = (
       ? new CombinedError({ graphQLErrors: errors, response })
       : undefined,
     extensions: hasExtensions ? extensions : undefined,
-    hasNext: !!nextResult.hasNext,
+    hasNext:
+      nextResult.hasNext != null ? nextResult.hasNext : prevResult.hasNext,
     stale: false,
   };
 };


Test patch:
diff --git a/packages/core/src/utils/result.test.ts b/packages/core/src/utils/result.test.ts
index d5c3663e..9670b6e9 100644
--- a/packages/core/src/utils/result.test.ts
+++ b/packages/core/src/utils/result.test.ts
@@ -1,11 +1,10 @@
 import { describe, it, expect } from 'vitest';
 import { OperationResult } from '../types';
-import { queryOperation } from '../test-utils';
+import { queryOperation, subscriptionOperation } from '../test-utils';
 import { makeResult, mergeResultPatch } from './result';
 
 describe('makeResult', () => {
   it('adds extensions and errors correctly', () => {
-    const response = {};
     const origResult = {
       data: undefined,
       errors: ['error message'],
@@ -14,8 +13,9 @@ describe('makeResult', () => {
       },
     };
 
-    const result = makeResult(queryOperation, origResult, response);
+    const result = makeResult(queryOperation, origResult);
 
+    expect(result.hasNext).toBe(false);
     expect(result.operation).toBe(queryOperation);
     expect(result.data).toBe(undefined);
     expect(result.extensions).toEqual(origResult.extensions);
@@ -23,9 +23,45 @@ describe('makeResult', () => {
       `[CombinedError: [GraphQL] error message]`
     );
   });
+
+  it('default hasNext to true for subscriptions', () => {
+    const origResult = {
+      data: undefined,
+      errors: ['error message'],
+      extensions: {
+        extensionKey: 'extensionValue',
+      },
+    };
+
+    const result = makeResult(subscriptionOperation, origResult);
+    expect(result.hasNext).toBe(true);
+  });
 });
 
 describe('mergeResultPatch', () => {
+  it('should default hasNext to true if the last result was set to true', () => {
+    const prevResult: OperationResult = {
+      operation: subscriptionOperation,
+      data: {
+        __typename: 'Subscription',
+        event: 1,
+      },
+      stale: false,
+      hasNext: true,
+    };
+
+    const merged = mergeResultPatch(prevResult, {
+      data: {
+        __typename: 'Subscription',
+        event: 2,
+      },
+    });
+
+    expect(merged.data).not.toBe(prevResult.data);
+    expect(merged.data.event).toBe(2);
+    expect(merged.hasNext).toBe(true);
+  });
+
   it('should ignore invalid patches', () => {
     const prevResult: OperationResult = {
       operation: queryOperation,