Skip to content

Commit

Permalink
Merge pull request #4543 from apollographql/assumeImmutableResults
Browse files Browse the repository at this point in the history
Avoid costly cloneDeep snapshots when cache results are known to be immutable.
  • Loading branch information
benjamn authored Mar 7, 2019
2 parents 94e0d1e + 1e740be commit 9ce1011
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 89 deletions.
5 changes: 3 additions & 2 deletions packages/apollo-boost/src/__tests__/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,9 @@ describe('config', () => {
version,
});

expect(client.clientAwareness.name).toEqual(name);
expect(client.clientAwareness.version).toEqual(version);
const { clientAwareness } = client.queryManager as any;
expect(clientAwareness.name).toEqual(name);
expect(clientAwareness.version).toEqual(version);
});

const makePromise = res =>
Expand Down
118 changes: 52 additions & 66 deletions packages/apollo-client/src/ApolloClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export type ApolloClientOptions<TCacheShape> = {
connectToDevTools?: boolean;
queryDeduplication?: boolean;
defaultOptions?: DefaultOptions;
assumeImmutableResults?: boolean;
resolvers?: Resolvers | Resolvers[];
typeDefs?: string | string[] | DocumentNode | DocumentNode[];
fragmentMatcher?: FragmentMatcher;
Expand All @@ -72,19 +73,16 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
public link: ApolloLink;
public store: DataStore<TCacheShape>;
public cache: ApolloCache<TCacheShape>;
public queryManager: QueryManager<TCacheShape> | undefined;
public readonly queryManager: QueryManager<TCacheShape>;
public disableNetworkFetches: boolean;
public version: string;
public queryDeduplication: boolean;
public defaultOptions: DefaultOptions = {};
public readonly typeDefs: ApolloClientOptions<TCacheShape>['typeDefs'];

private devToolsHookCb: Function;
private proxy: ApolloCache<TCacheShape> | undefined;
private ssrMode: boolean;
private resetStoreCallbacks: Array<() => Promise<any>> = [];
private clearStoreCallbacks: Array<() => Promise<any>> = [];
private clientAwareness: Record<string, string> = {};
private localState: LocalState<TCacheShape>;

/**
Expand All @@ -106,6 +104,12 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
* options supplied to `watchQuery`, `query`, or
* `mutate`.
*
* @param assumeImmutableResults When this option is true, the client will assume results
* read from the cache are never mutated by application code,
* which enables substantial performance optimizations. Passing
* `{ freezeResults: true }` to the `InMemoryCache` constructor
* can help enforce this immutability.
*
* @param name A custom name that can be used to identify this client, when
* using Apollo client awareness features. E.g. "iOS".
*
Expand All @@ -123,6 +127,7 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
connectToDevTools,
queryDeduplication = true,
defaultOptions,
assumeImmutableResults = false,
resolvers,
typeDefs,
fragmentMatcher,
Expand Down Expand Up @@ -166,7 +171,6 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
this.store = new DataStore(cache);
this.disableNetworkFetches = ssrMode || ssrForceFetchDelay > 0;
this.queryDeduplication = queryDeduplication;
this.ssrMode = ssrMode;
this.defaultOptions = defaultOptions || {};
this.typeDefs = typeDefs;

Expand Down Expand Up @@ -231,20 +235,37 @@ export default class ApolloClient<TCacheShape> implements DataProxy {

this.version = version;

if (clientAwarenessName) {
this.clientAwareness.name = clientAwarenessName;
}

if (clientAwarenessVersion) {
this.clientAwareness.version = clientAwarenessVersion;
}

this.localState = new LocalState({
cache,
client: this,
resolvers,
fragmentMatcher,
});

this.queryManager = new QueryManager({
link: this.link,
store: this.store,
queryDeduplication,
ssrMode,
clientAwareness: {
name: clientAwarenessName!,
version: clientAwarenessVersion!,
},
localState: this.localState,
assumeImmutableResults,
onBroadcast: () => {
if (this.devToolsHookCb) {
this.devToolsHookCb({
action: {},
state: {
queries: this.queryManager.queryStore.getStore(),
mutations: this.queryManager.mutationStore.getStore(),
},
dataWithOptimisticResults: this.cache.extract(true),
});
}
},
});
}

/**
Expand Down Expand Up @@ -295,7 +316,7 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
options = { ...options, fetchPolicy: 'cache-first' };
}

return this.initQueryManager().watchQuery<T, TVariables>(options);
return this.queryManager.watchQuery<T, TVariables>(options);
}

/**
Expand Down Expand Up @@ -327,7 +348,7 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
options = { ...options, fetchPolicy: 'cache-first' };
}

return this.initQueryManager().query<T>(options);
return this.queryManager.query<T>(options);
}

/**
Expand All @@ -347,7 +368,7 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
} as MutationOptions<T, TVariables>;
}

return this.initQueryManager().mutate<T>(options);
return this.queryManager.mutate<T>(options);
}

/**
Expand All @@ -357,7 +378,7 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
public subscribe<T = any, TVariables = OperationVariables>(
options: SubscriptionOptions<TVariables>,
): Observable<T> {
return this.initQueryManager().startGraphQLSubscription<T>(options);
return this.queryManager.startGraphQLSubscription<T>(options);
}

/**
Expand All @@ -373,7 +394,7 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
options: DataProxy.Query<TVariables>,
optimistic: boolean = false,
): T | null {
return this.initProxy().readQuery<T, TVariables>(options, optimistic);
return this.cache.readQuery<T, TVariables>(options, optimistic);
}

/**
Expand All @@ -394,7 +415,7 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
options: DataProxy.Fragment<TVariables>,
optimistic: boolean = false,
): T | null {
return this.initProxy().readFragment<T, TVariables>(options, optimistic);
return this.cache.readFragment<T, TVariables>(options, optimistic);
}

/**
Expand All @@ -405,8 +426,8 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
public writeQuery<TData = any, TVariables = OperationVariables>(
options: DataProxy.WriteQueryOptions<TData, TVariables>,
): void {
const result = this.initProxy().writeQuery<TData, TVariables>(options);
this.initQueryManager().broadcastQueries();
const result = this.cache.writeQuery<TData, TVariables>(options);
this.queryManager.broadcastQueries();
return result;
}

Expand All @@ -424,8 +445,8 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
public writeFragment<TData = any, TVariables = OperationVariables>(
options: DataProxy.WriteFragmentOptions<TData, TVariables>,
): void {
const result = this.initProxy().writeFragment<TData, TVariables>(options);
this.initQueryManager().broadcastQueries();
const result = this.cache.writeFragment<TData, TVariables>(options);
this.queryManager.broadcastQueries();
return result;
}

Expand All @@ -442,8 +463,8 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
public writeData<TData = any>(
options: DataProxy.WriteDataOptions<TData>,
): void {
const result = this.initProxy().writeData<TData>(options);
this.initQueryManager().broadcastQueries();
const result = this.cache.writeData<TData>(options);
this.queryManager.broadcastQueries();
return result;
}

Expand All @@ -459,32 +480,10 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
* This initializes the query manager that tracks queries and the cache
*/
public initQueryManager(): QueryManager<TCacheShape> {
if (!this.queryManager) {
this.queryManager = new QueryManager({
link: this.link,
store: this.store,
queryDeduplication: this.queryDeduplication,
ssrMode: this.ssrMode,
clientAwareness: this.clientAwareness,
localState: this.localState,
onBroadcast: () => {
if (this.devToolsHookCb) {
this.devToolsHookCb({
action: {},
state: {
queries: this.queryManager
? this.queryManager.queryStore.getStore()
: {},
mutations: this.queryManager
? this.queryManager.mutationStore.getStore()
: {},
},
dataWithOptimisticResults: this.cache.extract(true),
});
}
},
});
}
invariant.warn(
'Calling the initQueryManager method is no longer necessary, ' +
'and it will be removed from ApolloClient in version 3.0.',
);
return this.queryManager;
}

Expand Down Expand Up @@ -581,7 +580,7 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
* Exposes the cache's complete state, in a serializable format for later restoration.
*/
public extract(optimistic?: boolean): TCacheShape {
return this.initProxy().extract(optimistic);
return this.cache.extract(optimistic);
}

/**
Expand All @@ -592,7 +591,7 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
* and also (potentially) during hot reloads.
*/
public restore(serializedState: TCacheShape): ApolloCache<TCacheShape> {
return this.initProxy().restore(serializedState);
return this.cache.restore(serializedState);
}

/**
Expand Down Expand Up @@ -622,17 +621,4 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
public setLocalStateFragmentMatcher(fragmentMatcher: FragmentMatcher) {
this.localState.setFragmentMatcher(fragmentMatcher);
}

/**
* Initializes a data proxy for this client instance if one does not already
* exist and returns either a previously initialized proxy instance or the
* newly initialized instance.
*/
private initProxy(): ApolloCache<TCacheShape> {
if (!this.proxy) {
this.initQueryManager();
this.proxy = this.cache;
}
return this.proxy;
}
}
18 changes: 0 additions & 18 deletions packages/apollo-client/src/__tests__/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,6 @@ import { withWarning } from '../util/wrap';
import { mockSingleLink } from '../__mocks__/mockLinks';

describe('client', () => {
it('creates query manager lazily', () => {
const client = new ApolloClient({
link: ApolloLink.empty(),
cache: new InMemoryCache(),
});

expect(client.queryManager).toBeUndefined();

// We only create the query manager on the first query
client.initQueryManager();
expect(client.queryManager).toBeDefined();
expect(client.cache).toBeDefined();
});

it('can be loaded via require', () => {
/* tslint:disable */
const ApolloClientRequire = require('../').default;
Expand All @@ -46,10 +32,6 @@ describe('client', () => {
cache: new InMemoryCache(),
});

expect(client.queryManager).toBeUndefined();

// We only create the query manager on the first query
client.initQueryManager();
expect(client.queryManager).toBeDefined();
expect(client.cache).toBeDefined();
});
Expand Down
6 changes: 4 additions & 2 deletions packages/apollo-client/src/core/ObservableQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,8 @@ export class ObservableQuery<

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

return { ...result, partial };
Expand Down Expand Up @@ -611,7 +612,8 @@ export class ObservableQuery<
const observer: Observer<ApolloQueryResult<TData>> = {
next: (result: ApolloQueryResult<TData>) => {
this.lastResult = result;
this.lastResultSnapshot = cloneDeep(result);
this.lastResultSnapshot = this.queryManager.assumeImmutableResults
? result : cloneDeep(result);
this.observers.forEach(obs => obs.next && obs.next(result));
},
error: (error: ApolloError) => {
Expand Down
4 changes: 4 additions & 0 deletions packages/apollo-client/src/core/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export class QueryManager<TStore> {
public mutationStore: MutationStore = new MutationStore();
public queryStore: QueryStore = new QueryStore();
public dataStore: DataStore<TStore>;
public readonly assumeImmutableResults: boolean;

private deduplicator: ApolloLink;
private queryDeduplication: boolean;
Expand Down Expand Up @@ -94,6 +95,7 @@ export class QueryManager<TStore> {
ssrMode = false,
clientAwareness = {},
localState,
assumeImmutableResults,
}: {
link: ApolloLink;
queryDeduplication?: boolean;
Expand All @@ -102,6 +104,7 @@ export class QueryManager<TStore> {
ssrMode?: boolean;
clientAwareness?: Record<string, string>;
localState?: LocalState<TStore>;
assumeImmutableResults?: boolean;
}) {
this.link = link;
this.deduplicator = ApolloLink.from([new Deduplicator(), link]);
Expand All @@ -111,6 +114,7 @@ export class QueryManager<TStore> {
this.clientAwareness = clientAwareness;
this.localState = localState || new LocalState({ cache: store.getCache() });
this.ssrMode = ssrMode;
this.assumeImmutableResults = !!assumeImmutableResults;
}

/**
Expand Down
Loading

0 comments on commit 9ce1011

Please sign in to comment.