diff --git a/packages/apollo-cache-inmemory/src/inMemoryCache.ts b/packages/apollo-cache-inmemory/src/inMemoryCache.ts index e31d3db6f38..ce80f6c0b61 100644 --- a/packages/apollo-cache-inmemory/src/inMemoryCache.ts +++ b/packages/apollo-cache-inmemory/src/inMemoryCache.ts @@ -259,10 +259,12 @@ export class InMemoryCache extends ApolloCache { 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; } @@ -337,20 +339,11 @@ export class InMemoryCache extends ApolloCache { // 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); + })); } } diff --git a/packages/apollo-client/src/ApolloClient.ts b/packages/apollo-client/src/ApolloClient.ts index b729fcc4319..b06da361ecf 100644 --- a/packages/apollo-client/src/ApolloClient.ts +++ b/packages/apollo-client/src/ApolloClient.ts @@ -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, @@ -51,13 +51,6 @@ export type ApolloClientOptions = { 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, @@ -117,6 +110,20 @@ export default class ApolloClient implements DataProxy { `); } + const supportedCache = new Map(); + 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; diff --git a/packages/apollo-client/src/core/ObservableQuery.ts b/packages/apollo-client/src/core/ObservableQuery.ts index d515f89d431..e89a8f08acd 100644 --- a/packages/apollo-client/src/core/ObservableQuery.ts +++ b/packages/apollo-client/src/core/ObservableQuery.ts @@ -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'; @@ -201,7 +201,7 @@ export class ObservableQuery< } const result = { - data, + data: cloneDeep(data), loading: isNetworkRequestInFlight(networkStatus), networkStatus, } as ApolloQueryResult; @@ -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; @@ -586,7 +585,7 @@ export class ObservableQuery< const observer: Observer> = { next: (result: ApolloQueryResult) => { - this.lastResult = result; + this.lastResult = cloneDeep(result); this.observers.forEach(obs => obs.next && obs.next(result)); }, error: (error: ApolloError) => { diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 02263ac13af..d14652c5772 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -12,6 +12,7 @@ import { getQueryDefinition, isProduction, hasDirectives, + isEqual, } from 'apollo-utilities'; import { QueryScheduler } from '../scheduler/scheduler'; @@ -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) => void; reject: (error: Error) => void; @@ -619,10 +610,7 @@ export class QueryManager { 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) { @@ -1231,7 +1219,15 @@ export class QueryManager { } 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) { diff --git a/packages/apollo-client/src/core/__tests__/ObservableQuery.ts b/packages/apollo-client/src/core/__tests__/ObservableQuery.ts index 2456913c958..2d9204cecba 100644 --- a/packages/apollo-client/src/core/__tests__/ObservableQuery.ts +++ b/packages/apollo-client/src/core/__tests__/ObservableQuery.ts @@ -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(); } }); diff --git a/packages/apollo-utilities/package-lock.json b/packages/apollo-utilities/package-lock.json index 8dc713cca8a..34496cec49e 100644 --- a/packages/apollo-utilities/package-lock.json +++ b/packages/apollo-utilities/package-lock.json @@ -8,11 +8,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "fclone": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz", - "integrity": "sha1-EOhdo4v+p/xZk0HClu4ddyZu5kA=" } } } diff --git a/packages/apollo-utilities/package.json b/packages/apollo-utilities/package.json index eb40a970332..5f80d592a27 100644 --- a/packages/apollo-utilities/package.json +++ b/packages/apollo-utilities/package.json @@ -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": { diff --git a/packages/apollo-utilities/src/util/cloneDeep.ts b/packages/apollo-utilities/src/util/cloneDeep.ts index b31ab798cea..c02d1a788b0 100644 --- a/packages/apollo-utilities/src/util/cloneDeep.ts +++ b/packages/apollo-utilities/src/util/cloneDeep.ts @@ -1,8 +1,54 @@ -import fclone from 'fclone'; - -/** - * Deeply clones a value to create a new instance. - */ -export function cloneDeep(value: T): T { - return fclone(value); -} +const { toString } = Object.prototype; + +/** + * Deeply clones a value to create a new instance. + */ +export function cloneDeep(value: T): T { + return cloneDeepHelper(value, new Map()); +} + +function cloneDeepHelper(val: T, seen: Map): 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; + } +}