Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recycler standby #1636

Merged
merged 11 commits into from
May 2, 2017
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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