Skip to content

Commit

Permalink
Merge pull request #4032 from apollographql/fix-issue-3992
Browse files Browse the repository at this point in the history
Various improvements around previousResult/newData change detection.
  • Loading branch information
benjamn authored Oct 22, 2018
2 parents 9d1468a + e66027c commit 48a224d
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 61 deletions.
25 changes: 9 additions & 16 deletions packages/apollo-cache-inmemory/src/inMemoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,12 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
if (this.addTypename) {
let result = this.typenameDocumentCache.get(document);
if (!result) {
this.typenameDocumentCache.set(
document,
(result = addTypenameToDocument(document)),
);
result = addTypenameToDocument(document);
this.typenameDocumentCache.set(document, result);
// If someone calls transformDocument and then mistakenly passes the
// result back into an API that also calls transformDocument, make sure
// we don't keep creating new query documents.
this.typenameDocumentCache.set(result, result);
}
return result;
}
Expand Down Expand Up @@ -337,20 +339,11 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
// This method is wrapped in the constructor so that it will be called only
// if the data that would be broadcast has changed.
private maybeBroadcastWatch(c: Cache.WatchOptions) {
const previousResult = c.previousResult && c.previousResult();

const newData = this.diff({
c.callback(this.diff({
query: c.query,
variables: c.variables,
previousResult,
previousResult: c.previousResult && c.previousResult(),
optimistic: c.optimistic,
});

if (previousResult &&
previousResult === newData.result) {
return;
}

c.callback(newData);
}));
}
}
23 changes: 15 additions & 8 deletions packages/apollo-client/src/ApolloClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
GraphQLRequest,
execute,
} from 'apollo-link';
import { ExecutionResult } from 'graphql';
import { ExecutionResult, DocumentNode } from 'graphql';
import { ApolloCache, DataProxy } from 'apollo-cache';
import {
isProduction,
Expand Down Expand Up @@ -51,13 +51,6 @@ export type ApolloClientOptions<TCacheShape> = {
defaultOptions?: DefaultOptions;
};

const supportedDirectives = new ApolloLink(
(operation: Operation, forward: NextLink) => {
operation.query = removeConnectionDirectiveFromDocument(operation.query);
return forward(operation);
},
);

/**
* This is the primary Apollo Client class. It is used to send GraphQL documents (i.e. queries
* and mutations) to a GraphQL spec-compliant server over a {@link NetworkInterface} instance,
Expand Down Expand Up @@ -117,6 +110,20 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
`);
}

const supportedCache = new Map<DocumentNode, DocumentNode>();
const supportedDirectives = new ApolloLink(
(operation: Operation, forward: NextLink) => {
let result = supportedCache.get(operation.query);
if (! result) {
result = removeConnectionDirectiveFromDocument(operation.query);
supportedCache.set(operation.query, result);
supportedCache.set(result, result);
}
operation.query = result;
return forward(operation);
},
);

// remove apollo-client supported directives
this.link = supportedDirectives.concat(link);
this.cache = cache;
Expand Down
9 changes: 4 additions & 5 deletions packages/apollo-client/src/core/ObservableQuery.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isEqual, tryFunctionOrLogError } from 'apollo-utilities';
import { isEqual, tryFunctionOrLogError, cloneDeep } from 'apollo-utilities';
import { GraphQLError } from 'graphql';
import { NetworkStatus, isNetworkRequestInFlight } from './networkStatus';
import { Observable, Observer, Subscription } from '../util/Observable';
Expand Down Expand Up @@ -201,7 +201,7 @@ export class ObservableQuery<
}

const result = {
data,
data: cloneDeep(data),
loading: isNetworkRequestInFlight(networkStatus),
networkStatus,
} as ApolloQueryResult<TData>;
Expand All @@ -215,8 +215,7 @@ export class ObservableQuery<
}

if (!partial) {
const stale = false;
this.lastResult = { ...result, stale };
this.lastResult = { ...result, stale: false };
}

return { ...result, partial } as ApolloCurrentResult<TData>;
Expand Down Expand Up @@ -586,7 +585,7 @@ export class ObservableQuery<

const observer: Observer<ApolloQueryResult<TData>> = {
next: (result: ApolloQueryResult<TData>) => {
this.lastResult = result;
this.lastResult = cloneDeep(result);
this.observers.forEach(obs => obs.next && obs.next(result));
},
error: (error: ApolloError) => {
Expand Down
26 changes: 11 additions & 15 deletions packages/apollo-client/src/core/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getQueryDefinition,
isProduction,
hasDirectives,
isEqual,
} from 'apollo-utilities';

import { QueryScheduler } from '../scheduler/scheduler';
Expand Down Expand Up @@ -54,16 +55,6 @@ export interface QueryInfo {
cancel?: (() => void);
}

const defaultQueryInfo = {
listeners: [],
invalidated: false,
document: null,
newData: null,
lastRequestId: null,
observableQuery: null,
subscriptions: [],
};

export interface QueryPromise {
resolve: (result: ApolloQueryResult<any>) => void;
reject: (error: Error) => void;
Expand Down Expand Up @@ -619,10 +610,7 @@ export class QueryManager<TStore> {
resultFromStore &&
lastResult.networkStatus === resultFromStore.networkStatus &&
lastResult.stale === resultFromStore.stale &&
// We can do a strict equality check here because we include a `previousResult`
// with `readQueryFromStore`. So if the results are the same they will be
// referentially equal.
lastResult.data === resultFromStore.data
isEqual(lastResult.data, resultFromStore.data)
);

if (isDifferentResult || previouslyHadError) {
Expand Down Expand Up @@ -1231,7 +1219,15 @@ export class QueryManager<TStore> {
}

private getQuery(queryId: string) {
return this.queries.get(queryId) || { ...defaultQueryInfo };
return this.queries.get(queryId) || {
listeners: [],
invalidated: false,
document: null,
newData: null,
lastRequestId: null,
observableQuery: null,
subscriptions: [],
};
}

private setQuery(queryId: string, updater: (prev: QueryInfo) => any) {
Expand Down
3 changes: 1 addition & 2 deletions packages/apollo-client/src/core/__tests__/ObservableQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,8 +324,7 @@ describe('ObservableQuery', () => {
const current = observable.currentResult();
expect(stripSymbols(current.data)).toEqual(data);
const secondCurrent = observable.currentResult();
// ensure ref equality
expect(current.data).toBe(secondCurrent.data);
expect(current.data).toEqual(secondCurrent.data);
done();
}
});
Expand Down
5 changes: 0 additions & 5 deletions packages/apollo-utilities/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions packages/apollo-utilities/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@
"filesize": "npm run minify"
},
"dependencies": {
"fast-json-stable-stringify": "^2.0.0",
"fclone": "^1.0.11"
"fast-json-stable-stringify": "^2.0.0"
},
"jest": {
"transform": {
Expand Down
62 changes: 54 additions & 8 deletions packages/apollo-utilities/src/util/cloneDeep.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,54 @@
import fclone from 'fclone';

/**
* Deeply clones a value to create a new instance.
*/
export function cloneDeep<T>(value: T): T {
return fclone(value);
}
const { toString } = Object.prototype;

/**
* Deeply clones a value to create a new instance.
*/
export function cloneDeep<T>(value: T): T {
return cloneDeepHelper(value, new Map());
}

function cloneDeepHelper<T>(val: T, seen: Map<any, any>): T {
switch (toString.call(val)) {
case "[object Array]": {
if (seen.has(val)) return seen.get(val);
const copy: T & any[] = (val as any).slice(0);
seen.set(val, copy);
copy.forEach(function (child, i) {
copy[i] = cloneDeepHelper(child, seen);
});
return copy;
}

case "[object Date]":
return new Date(+val) as T & Date;

case "[object Object]": {
if (seen.has(val)) return seen.get(val);
// High fidelity polyfills of Object.create and Object.getPrototypeOf are
// possible in all JS environments, so we will assume they exist/work.
const copy = Object.create(Object.getPrototypeOf(val));
seen.set(val, copy);

if (typeof Object.getOwnPropertyDescriptor === "function") {
const handleKey = function (key: string | symbol) {
const desc = Object.getOwnPropertyDescriptor(val, key);
desc.value = cloneDeepHelper((val as any)[key], seen);
Object.defineProperty(copy, key, desc);
};
Object.getOwnPropertyNames(val).forEach(handleKey);
if (typeof Object.getOwnPropertySymbols === "function") {
Object.getOwnPropertySymbols(val).forEach(handleKey);
}
} else {
Object.keys(val).forEach(key => {
copy[key] = cloneDeepHelper((val as any)[key], seen);
});
}

return copy;
}

default:
return val;
}
}

0 comments on commit 48a224d

Please sign in to comment.