From e7f9b190d680ec42b505a89ab9d44b8cc3a12cc3 Mon Sep 17 00:00:00 2001 From: Jonas Helfer Date: Sun, 30 Apr 2017 14:03:09 -0700 Subject: [PATCH 01/10] Add standby fetchPolicy type --- src/core/watchQueryOptions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index 9b80f45e6ed..c4ddb2f761f 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -22,9 +22,10 @@ import { * - cache-and-network: returns result from cache first (if it exists), then return network result once it's available * - cache-only: return result from cache if avaiable, fail otherwise. * - network-only: return result from network, fail if network call doesn't succeed. + * - standby: only for queries that aren't actively watched, but should be available for refetch and updateQueries. */ -export type FetchPolicy = 'cache-first' | 'cache-and-network' | 'network-only' | 'cache-only'; +export type FetchPolicy = 'cache-first' | 'cache-and-network' | 'network-only' | 'cache-only' | 'standby'; /** * We can change these options to an ObservableQuery From 236b4c133d7e99093cbf5164e0e0722145b3a735 Mon Sep 17 00:00:00 2001 From: Jonas Helfer Date: Sun, 30 Apr 2017 14:05:23 -0700 Subject: [PATCH 02/10] Do not refetch standby queries on store reset --- src/core/QueryManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 86bc86ba72f..b0976269362 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -807,7 +807,7 @@ export class QueryManager { const fetchPolicy = this.observableQueries[queryId].observableQuery.options.fetchPolicy; - if (fetchPolicy !== 'cache-only') { + if (fetchPolicy !== 'cache-only' && fetchPolicy !== 'standby') { this.observableQueries[queryId].observableQuery.refetch(); } }); From 152852b09478bdf29dd29044782ce1ff0758b7fa Mon Sep 17 00:00:00 2001 From: Jonas Helfer Date: Sun, 30 Apr 2017 14:18:57 -0700 Subject: [PATCH 03/10] Add test for standby query store reset --- test/QueryManager.ts | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/test/QueryManager.ts b/test/QueryManager.ts index c750b8332ae..34bcc587e18 100644 --- a/test/QueryManager.ts +++ b/test/QueryManager.ts @@ -2471,8 +2471,39 @@ describe('QueryManager', () => { setTimeout(() => { assert.equal(refetchCount, 0); done(); - }, 400); + }, 50); + + }); + it('should not call refetch on a standby Observable if the store is reset', (done) => { + const query = gql` + query { + author { + firstName + lastName + } + }`; + const queryManager = createQueryManager({}); + const options = assign({}) as WatchQueryOptions; + options.fetchPolicy = 'standby'; + options.query = query; + let refetchCount = 0; + const mockObservableQuery: ObservableQuery = { + refetch(variables: any): Promise { + refetchCount ++; + return null as never; + }, + options, + queryManager: queryManager, + } as any as ObservableQuery; + + const queryId = 'super-fake-id'; + queryManager.addObservableQuery(queryId, mockObservableQuery); + queryManager.resetStore(); + setTimeout(() => { + assert.equal(refetchCount, 0); + done(); + }, 50); }); it('should throw an error on an inflight query() if the store is reset', (done) => { From b2e81b8caaee40fcaa17cdc1a698fa129e2b2c7c Mon Sep 17 00:00:00 2001 From: Jonas Helfer Date: Sun, 30 Apr 2017 14:22:10 -0700 Subject: [PATCH 04/10] Fix previously non-functional cache-only store reset test --- test/QueryManager.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/QueryManager.ts b/test/QueryManager.ts index 34bcc587e18..899cfd97661 100644 --- a/test/QueryManager.ts +++ b/test/QueryManager.ts @@ -2458,7 +2458,6 @@ describe('QueryManager', () => { const mockObservableQuery: ObservableQuery = { refetch(variables: any): Promise { refetchCount ++; - done(); return null as never; }, options, From 7db71dad48e40fc971f0d385646901129b05f582 Mon Sep 17 00:00:00 2001 From: Jonas Helfer Date: Sun, 30 Apr 2017 14:39:39 -0700 Subject: [PATCH 05/10] Check that queries are not started with standby fetchPolicy --- src/core/QueryManager.ts | 3 +++ test/client.ts | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index b0976269362..7b908825322 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -631,6 +631,9 @@ export class QueryManager { throw new Error('noFetch option is no longer supported since Apollo Client 1.0. Use fetchPolicy instead.'); } + if (options.fetchPolicy === 'standby') { + throw new Error('client.watchQuery cannot be called with fetchPolicy set to "standby"'); + } // get errors synchronously const queryDefinition = getQueryDefinition(options.query); diff --git a/test/client.ts b/test/client.ts index d6ec8ba495b..688ae485298 100644 --- a/test/client.ts +++ b/test/client.ts @@ -1646,7 +1646,16 @@ describe('client', () => { }, }); }); + }); + describe('standby fetchPolicy', () => { + it('cannot be applied during initial construction of a query', () => { + const client = new ApolloClient(); + assert.throws( + () => client.watchQuery({ query: gql`{ abc }`, fetchPolicy: 'standby'}), + 'client.watchQuery cannot be called with fetchPolicy set to "standby"', + ); + }); }); describe('network-only fetchPolicy', () => { From 894db45183a0c9d8a5d7836787c9221f1cf162db Mon Sep 17 00:00:00 2001 From: Jonas Helfer Date: Sun, 30 Apr 2017 14:56:32 -0700 Subject: [PATCH 06/10] Test that standby doesn't cause refetch --- src/core/QueryManager.ts | 2 +- test/ObservableQuery.ts | 102 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 7b908825322..64aa9739c33 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -404,7 +404,7 @@ export class QueryManager { storeResult = result; } - const shouldFetch = needToFetch && fetchPolicy !== 'cache-only'; + const shouldFetch = needToFetch && fetchPolicy !== 'cache-only' && fetchPolicy !== 'standby'; const requestId = this.generateRequestId(); diff --git a/test/ObservableQuery.ts b/test/ObservableQuery.ts index 8dc79a4fb9a..d62eff2a48b 100644 --- a/test/ObservableQuery.ts +++ b/test/ObservableQuery.ts @@ -389,6 +389,108 @@ describe('ObservableQuery', () => { } }); }); + + it('can set queries to standby and will not fetch when doing so', (done) => { + let queryManager: QueryManager; + let observable: ObservableQuery; + const testQuery = gql` + query { + author { + firstName + lastName + } + }`; + const data = { + author: { + firstName: 'John', + lastName: 'Smith', + }, + }; + + let timesFired = 0; + const networkInterface: NetworkInterface = { + query(request: Request): Promise { + timesFired += 1; + return Promise.resolve({ data }); + }, + }; + queryManager = createQueryManager({ networkInterface }); + observable = queryManager.watchQuery({ + query: testQuery, + fetchPolicy: 'cache-first', + notifyOnNetworkStatusChange: false, + }); + + subscribeAndCount(done, observable, (handleCount, result) => { + if (handleCount === 1) { + assert.deepEqual(result.data, data); + assert.equal(timesFired, 1); + + setTimeout(() => { + observable.setOptions({fetchPolicy: 'standby'}); + }, 0); + setTimeout(() => { + // make sure the query didn't get fired again. + assert.equal(timesFired, 1); + done(); + }, 20); + } else if (handleCount === 2) { + assert(false, 'Handle should not be triggered on standby query'); + } + }); + }); + + it('will not fetch when setting a cache-only query to standby', (done) => { + let queryManager: QueryManager; + let observable: ObservableQuery; + const testQuery = gql` + query { + author { + firstName + lastName + } + }`; + const data = { + author: { + firstName: 'John', + lastName: 'Smith', + }, + }; + + let timesFired = 0; + const networkInterface: NetworkInterface = { + query(request: Request): Promise { + timesFired += 1; + return Promise.resolve({ data }); + }, + }; + queryManager = createQueryManager({ networkInterface }); + + queryManager.query({ query: testQuery }).then( () => { + observable = queryManager.watchQuery({ + query: testQuery, + fetchPolicy: 'cache-first', + notifyOnNetworkStatusChange: false, + }); + + subscribeAndCount(done, observable, (handleCount, result) => { + if (handleCount === 1) { + assert.deepEqual(result.data, data); + assert.equal(timesFired, 1); + setTimeout(() => { + observable.setOptions({fetchPolicy: 'standby'}); + }, 0); + setTimeout(() => { + // make sure the query didn't get fired again. + assert.equal(timesFired, 1); + done(); + }, 20); + } else if (handleCount === 2) { + assert(false, 'Handle should not be triggered on standby query'); + } + }); + }); + }); }); describe('setVariables', () => { From 69338c3d998e04b35a09362e7895df6d0de44f5e Mon Sep 17 00:00:00 2001 From: Jonas Helfer Date: Sun, 30 Apr 2017 15:17:00 -0700 Subject: [PATCH 07/10] Write failing test: standby queries do not watch store --- test/client.ts | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/test/client.ts b/test/client.ts index 688ae485298..779f9119d85 100644 --- a/test/client.ts +++ b/test/client.ts @@ -1648,14 +1648,52 @@ describe('client', () => { }); }); - describe('standby fetchPolicy', () => { - it('cannot be applied during initial construction of a query', () => { + describe('standby queries', () => { + // XXX queries can only be set to standby by setOptions. This is simply out of caution, + // not some fundamental reason. We just want to make sure they're not used in unanticipated ways. + // If there's a good use-case, the error and test could be removed. + it('cannot be started with watchQuery or query', () => { const client = new ApolloClient(); assert.throws( () => client.watchQuery({ query: gql`{ abc }`, fetchPolicy: 'standby'}), 'client.watchQuery cannot be called with fetchPolicy set to "standby"', ); }); + + it('are not watching the store or notifying on updates', (done) => { + const query = gql`{ test }`; + const data = { test: 'ok' }; + const data2 = { test: 'not ok' }; + + const networkInterface = mockNetworkInterface({ + request: { query }, + result: { data }, + }); + + const client = new ApolloClient({ networkInterface }); + + const obs = client.watchQuery({ query, fetchPolicy: 'cache-first' }); + + let handleCalled = false; + subscribeAndCount(done, obs, (handleCount, result) => { + if (handleCount === 1) { + assert.deepEqual(result.data, data); + obs.setOptions({ fetchPolicy: 'standby' }).then( () => { + client.writeQuery({ query, data: data2 }); + // this write should be completely ignored by the standby query + }); + setTimeout( () => { + if (!handleCalled) { + done(); + } + }, 20); + } + if (handleCount === 2) { + handleCalled = true; + done(new Error('Handle should never be called on standby query')); + } + }); + }); }); describe('network-only fetchPolicy', () => { From 564ebf4f7690401ac3999b59891750a0663aaf33 Mon Sep 17 00:00:00 2001 From: Jonas Helfer Date: Sun, 30 Apr 2017 15:19:29 -0700 Subject: [PATCH 08/10] Make sure standby queries don't watch the store --- src/core/QueryManager.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 64aa9739c33..6f4e5c9027a 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -501,6 +501,11 @@ export class QueryManager { const fetchPolicy = storedQuery ? storedQuery.observableQuery.options.fetchPolicy : options.fetchPolicy; + if (fetchPolicy === 'standby') { + // don't watch the store for queries on standby + return; + } + const shouldNotifyIfLoading = queryStoreValue.previousVariables || fetchPolicy === 'cache-only' || fetchPolicy === 'cache-and-network'; From 0d3e4c4d92241b0cfb6b2ce2ecb98bdf09071f7a Mon Sep 17 00:00:00 2001 From: Jonas Helfer Date: Sun, 30 Apr 2017 15:24:58 -0700 Subject: [PATCH 09/10] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 207672c6402..5993e0e9015 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Change log ### vNEXT +- Feature+Fix: Introduce "standby" fetchPolicy to mark queries that are not currently active, but should be available for refetchQueries and updateQueries [PR #1636](https://github.com/apollographql/apollo-client/pull/1636) - Feature: Print a warning when heuristically matching fragments on interface/union [PR #1635](https://github.com/apollographql/apollo-client/pull/1635) ### 1.1.1 From b016baf31ba2c46675788d5f1894e339836cacbc Mon Sep 17 00:00:00 2001 From: Jonas Helfer Date: Sun, 30 Apr 2017 15:57:13 -0700 Subject: [PATCH 10/10] Make sure standby queries check the store when waking up --- src/core/ObservableQuery.ts | 1 + test/client.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 86d17b1e4d1..8fb5c96a827 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -337,6 +337,7 @@ export class ObservableQuery extends Observable> { // If fetchPolicy went from cache-only to something else, or from something else to network-only const tryFetch: boolean = (oldOptions.fetchPolicy !== 'network-only' && opts.fetchPolicy === 'network-only') || (oldOptions.fetchPolicy === 'cache-only' && opts.fetchPolicy !== 'cache-only') + || (oldOptions.fetchPolicy === 'standby' && opts.fetchPolicy !== 'standby') || false; return this.setVariables(this.options.variables, tryFetch); diff --git a/test/client.ts b/test/client.ts index 779f9119d85..f7162b2a01e 100644 --- a/test/client.ts +++ b/test/client.ts @@ -1694,6 +1694,40 @@ describe('client', () => { } }); }); + + it('return the current result when coming out of standby', (done) => { + const query = gql`{ test }`; + const data = { test: 'ok' }; + const data2 = { test: 'not ok' }; + + const networkInterface = mockNetworkInterface({ + request: { query }, + result: { data }, + }); + + const client = new ApolloClient({ networkInterface }); + + const obs = client.watchQuery({ query, fetchPolicy: 'cache-first' }); + + let handleCalled = false; + subscribeAndCount(done, obs, (handleCount, result) => { + if (handleCount === 1) { + assert.deepEqual(result.data, data); + obs.setOptions({ fetchPolicy: 'standby' }).then( () => { + client.writeQuery({ query, data: data2 }); + // this write should be completely ignored by the standby query + setTimeout( () => { + obs.setOptions({ fetchPolicy: 'cache-first' }); + }, 10); + }); + } + if (handleCount === 2) { + handleCalled = true; + assert.deepEqual(result.data, data2); + done(); + } + }); + }); }); describe('network-only fetchPolicy', () => {