Skip to content

Commit

Permalink
Merge pull request #1636 from apollographql/recycler-standby
Browse files Browse the repository at this point in the history
Recycler standby
  • Loading branch information
helfer committed May 2, 2017
2 parents 44069ca + 64d2a62 commit 3cad0cf
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 5 deletions.
1 change: 1 addition & 0 deletions 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
Expand Down
1 change: 1 addition & 0 deletions src/core/ObservableQuery.ts
Expand Up @@ -337,6 +337,7 @@ export class ObservableQuery<T> extends Observable<ApolloQueryResult<T>> {
// 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);
Expand Down
12 changes: 10 additions & 2 deletions src/core/QueryManager.ts
Expand Up @@ -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();

Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -631,6 +636,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);
Expand Down Expand Up @@ -807,7 +815,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();
}
});
Expand Down
3 changes: 2 additions & 1 deletion src/core/watchQueryOptions.ts
Expand Up @@ -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
Expand Down
102 changes: 102 additions & 0 deletions test/ObservableQuery.ts
Expand Up @@ -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<any>;
const testQuery = gql`
query {
author {
firstName
lastName
}
}`;
const data = {
author: {
firstName: 'John',
lastName: 'Smith',
},
};

let timesFired = 0;
const networkInterface: NetworkInterface = {
query(request: Request): Promise<ExecutionResult> {
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<any>;
const testQuery = gql`
query {
author {
firstName
lastName
}
}`;
const data = {
author: {
firstName: 'John',
lastName: 'Smith',
},
};

let timesFired = 0;
const networkInterface: NetworkInterface = {
query(request: Request): Promise<ExecutionResult> {
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', () => {
Expand Down
34 changes: 32 additions & 2 deletions test/QueryManager.ts
Expand Up @@ -2458,7 +2458,6 @@ describe('QueryManager', () => {
const mockObservableQuery: ObservableQuery<any> = {
refetch(variables: any): Promise<ExecutionResult> {
refetchCount ++;
done();
return null as never;
},
options,
Expand All @@ -2471,8 +2470,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<any> = {
refetch(variables: any): Promise<ExecutionResult> {
refetchCount ++;
return null as never;
},
options,
queryManager: queryManager,
} as any as ObservableQuery<any>;

const queryId = 'super-fake-id';
queryManager.addObservableQuery<any>(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) => {
Expand Down
81 changes: 81 additions & 0 deletions test/client.ts
Expand Up @@ -1646,7 +1646,88 @@ describe('client', () => {
},
});
});
});

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'));
}
});
});

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', () => {
Expand Down

0 comments on commit 3cad0cf

Please sign in to comment.