diff --git a/packages/relay-runtime/network/RelayNetworkTypes.js b/packages/relay-runtime/network/RelayNetworkTypes.js index 20e700beb1174..d881a15f2c6bd 100644 --- a/packages/relay-runtime/network/RelayNetworkTypes.js +++ b/packages/relay-runtime/network/RelayNetworkTypes.js @@ -51,6 +51,7 @@ export type GraphQLResponseWithData = {| +extensions?: PayloadExtensions, +label?: string, +path?: Array, + +hasNext?: boolean, |}; export type GraphQLResponseWithoutData = {| @@ -59,6 +60,7 @@ export type GraphQLResponseWithoutData = {| +extensions?: PayloadExtensions, +label?: string, +path?: Array, + +hasNext?: boolean, |}; export type GraphQLResponseWithExtensionsOnly = {| @@ -72,6 +74,7 @@ export type GraphQLResponseWithExtensionsOnly = {| // does not necessarily indicate that there was an error. +data: null, +extensions: PayloadExtensions, + +hasNext?: boolean, |}; export type GraphQLSingularResponse = diff --git a/packages/relay-runtime/query/__tests__/fetchQueryInternal-test.js b/packages/relay-runtime/query/__tests__/fetchQueryInternal-test.js index 48332bb19fe01..c90e6d03c9631 100644 --- a/packages/relay-runtime/query/__tests__/fetchQueryInternal-test.js +++ b/packages/relay-runtime/query/__tests__/fetchQueryInternal-test.js @@ -964,6 +964,30 @@ describe('getObservableForActiveRequest', () => { expect(events).toEqual(['next']); }); + it('calls next asynchronously with subsequent non-final payloads (OSS)', () => { + fetchQuery(environment, query).subscribe({}); + const observable = getObservableForActiveRequest( + environment, + query.request, + ); + expect(observable).not.toEqual(null); + if (!observable) { + return; + } + + response = { + ...response, + extensions: {}, + hasNext: true, + }; + + observable.subscribe(observer); + expect(events).toEqual([]); + + environment.mock.nextValue(gqlQuery, response); + expect(events).toEqual(['next']); + }); + it('calls complete asynchronously with subsequent final payload', () => { fetchQuery(environment, query).subscribe({}); const observable = getObservableForActiveRequest( @@ -982,6 +1006,30 @@ describe('getObservableForActiveRequest', () => { expect(events).toEqual(['complete']); }); + it('calls complete asynchronously with subsequent final payload (OSS)', () => { + fetchQuery(environment, query).subscribe({}); + const observable = getObservableForActiveRequest( + environment, + query.request, + ); + expect(observable).not.toEqual(null); + if (!observable) { + return; + } + + response = { + ...response, + extensions: {}, + hasNext: false, + }; + + observable.subscribe(observer); + expect(events).toEqual([]); + + environment.mock.nextValue(gqlQuery, response); + expect(events).toEqual(['complete']); + }); + describe('when loading @module', () => { let operationLoader; let resolveModule; diff --git a/packages/relay-runtime/store/RelayModernQueryExecutor.js b/packages/relay-runtime/store/RelayModernQueryExecutor.js index e9e5a1b525dbe..efa77cb14631d 100644 --- a/packages/relay-runtime/store/RelayModernQueryExecutor.js +++ b/packages/relay-runtime/store/RelayModernQueryExecutor.js @@ -400,7 +400,7 @@ class Executor { if (responsesWithData.length === 0) { // no results with data, nothing to process // this can occur with extensions-only payloads - const isFinal = responses.some(x => x.extensions?.is_final === true); + const isFinal = responses.some(x => responseIsFinal(x)); if (isFinal) { this._state = 'loading_final'; this._updateActiveState(); @@ -1019,7 +1019,7 @@ class Executor { incrementalPlaceholders: null, moduleImportPayloads: null, source: RelayRecordSource.create(), - isFinal: response.extensions?.is_final === true, + isFinal: responseIsFinal(response), }; this._publishQueue.commitPayload( this._operation, @@ -1296,7 +1296,7 @@ function normalizeResponse( return { ...relayPayload, errors, - isFinal: response.extensions?.is_final === true, + isFinal: responseIsFinal(response), }; } @@ -1318,4 +1318,17 @@ function validateOptimisticResponsePayload( } } +/** + * Check for both FB specific (extensions?.is_final) + * and spec-complaint (hasNext) properties. + */ +function responseIsFinal(response: GraphQLSingularResponse): boolean { + if (response.extensions?.is_final === true) { + return true; + } else if (response.hasNext === false) { + return true; + } + return false; +} + module.exports = {execute};