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

Accommodate @client @export variable changes in ObservableQuery #4604

Merged
merged 9 commits into from
Apr 5, 2019
6 changes: 2 additions & 4 deletions packages/apollo-client/src/__tests__/fetchMore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,8 @@ describe('updateQuery on a simple query', () => {
res.entry.value = 2;
return res;
});

expect(latestResult.data.entry.value).toBe(2);
})
.then(() => expect(latestResult.data.entry.value).toBe(2))
.then(() => sub.unsubscribe());
});
});
Expand Down Expand Up @@ -123,9 +122,8 @@ describe('updateQuery on a query with required and optional variables', () => {
res.entry.value = 2;
return res;
});

expect(latestResult.data.entry.value).toBe(2);
})
.then(() => expect(latestResult.data.entry.value).toBe(2))
.then(() => sub.unsubscribe());
});
});
Expand Down
125 changes: 125 additions & 0 deletions packages/apollo-client/src/__tests__/local-state/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,4 +624,129 @@ describe('@client @export tests', () => {
});
},
);

it(
'should refetch if an @export variable changes, the current fetch ' +
'policy is not cache-only, and the query includes fields that need to ' +
'be resolved remotely',
done => {
const query = gql`
query currentAuthorPostCount($authorId: Int!) {
currentAuthorId @client @export(as: "authorId")
postCount(authorId: $authorId)
}
`;

const testAuthorId1 = 100;
const testPostCount1 = 200;

const testAuthorId2 = 101;
const testPostCount2 = 201;

let resultCount = 0;

const link = new ApolloLink(() =>
Observable.of({
data: {
postCount: resultCount === 0 ? testPostCount1 : testPostCount2
},
}),
);

const cache = new InMemoryCache();
const client = new ApolloClient({
cache,
link,
resolvers: {},
});

client.writeData({ data: { currentAuthorId: testAuthorId1 } });

const obs = client.watchQuery({ query });
obs.subscribe({
next({ data }) {
if (resultCount === 0) {
expect({ ...data }).toMatchObject({
currentAuthorId: testAuthorId1,
postCount: testPostCount1,
});
client.writeData({ data: { currentAuthorId: testAuthorId2 } });
} else if (resultCount === 1) {
expect({ ...data }).toMatchObject({
currentAuthorId: testAuthorId2,
postCount: testPostCount2,
});
done();
}

resultCount +=1;
}
});
}
);

it(
'should NOT refetch if an @export variable has not changed, the ' +
'current fetch policy is not cache-only, and the query includes fields ' +
'that need to be resolved remotely',
done => {
const query = gql`
query currentAuthorPostCount($authorId: Int!) {
currentAuthorId @client @export(as: "authorId")
postCount(authorId: $authorId)
}
`;

const testAuthorId1 = 100;
const testPostCount1 = 200;

const testPostCount2 = 201;

let resultCount = 0;

let fetchCount = 0;
const link = new ApolloLink(() => {
fetchCount += 1;
return Observable.of({
data: {
postCount: testPostCount1
},
});
});

const cache = new InMemoryCache();
const client = new ApolloClient({
cache,
link,
resolvers: {},
});

client.writeData({ data: { currentAuthorId: testAuthorId1 } });

const obs = client.watchQuery({ query });
obs.subscribe({
next(result) {
if (resultCount === 0) {
expect(fetchCount).toBe(1);
expect(result.data).toMatchObject({
currentAuthorId: testAuthorId1,
postCount: testPostCount1,
});

client.writeQuery({
query,
variables: { authorId: testAuthorId1 },
data: { postCount: testPostCount2 }
});
} else if (resultCount === 1) {
// Should not have refetched
expect(fetchCount).toBe(1);
done();
}

resultCount +=1;
},
});
}
);
});
48 changes: 39 additions & 9 deletions packages/apollo-client/src/core/ObservableQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export class ObservableQuery<
...this.options.variables,
...(queryStoreValue.variables as TVariables),
};
this.variables = this.options.variables;
}

result = {
Expand Down Expand Up @@ -241,10 +242,7 @@ export class ObservableQuery<
}

if (!partial) {
this.lastResult = { ...result, stale: false };
this.lastResultSnapshot = this.queryManager.assumeImmutableResults
? this.lastResult
: cloneDeep(this.lastResult);
this.updateLastResult({ ...result, stale: false });
}

return { ...result, partial };
Expand Down Expand Up @@ -536,6 +534,15 @@ export class ObservableQuery<
this.queryManager.startPollingQuery(this.options, this.queryId);
}

private updateLastResult(newResult: ApolloQueryResult<TData>) {
const previousResult = this.lastResult;
this.lastResult = newResult;
this.lastResultSnapshot = this.queryManager.assumeImmutableResults
? this.lastResult
: cloneDeep(this.lastResult);
return previousResult;
}

private onSubscribe(observer: Observer<ApolloQueryResult<TData>>) {
const first = !this.observers.size;
this.observers.add(observer);
Expand Down Expand Up @@ -565,11 +572,34 @@ export class ObservableQuery<
}

const observer: Observer<ApolloQueryResult<TData>> = {
next: (result: ApolloQueryResult<TData>) => {
this.lastResult = result;
this.lastResultSnapshot = this.queryManager.assumeImmutableResults
? result
: cloneDeep(result);
next: async (result: ApolloQueryResult<TData>): Promise<any> => {
const { queryManager } = this;
const { query, variables, fetchPolicy } = this.options;
const lastVariables = this.variables;
const previousResult = this.updateLastResult(result);

// Before calling `next` on each observer, we need to first see if
// the query is using `@client @export` directives, and update
// any variables that might have changed. If `@export` variables have
// changed, and the query is calling against both local and remote
// data, a refetch is needed to pull in new data, using the
// updated `@export` variables.
if (queryManager.transform(query).hasClientExports) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By using cached data from queryManager.transform(query) we can avoid repeatedly calling hasClientExports or queryManager.getLocalState().serverQuery (below).

this.variables = this.options.variables = (
await queryManager.getLocalState().addExportedVariables(query, variables)
) as TVariables;

if (
!result.loading &&
previousResult &&
fetchPolicy !== 'cache-only' &&
queryManager.transform(query).serverQuery &&
!isEqual(lastVariables, this.variables)
) {
return this.refetch();
}
}

iterateObserversSafely(this.observers, 'next', result);
},
error: (error: ApolloError) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-client/src/core/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,7 @@ export class QueryManager<TStore> {
}>
>();

private transform(document: DocumentNode) {
public transform(document: DocumentNode) {
Copy link
Member

@benjamn benjamn Apr 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love making this public, but at least the result type is read-only.

const { transformCache } = this;

if (!transformCache.has(document)) {
Expand Down