Skip to content

Commit

Permalink
Add Relay.disableQueryCache static method
Browse files Browse the repository at this point in the history
Summary:
This allows for disabling the query cache in `createRelayQuery` and `buildRQL` by calling `Relay.disableQueryCache()`.

Submitted in response to #754 (comment)

Because keys used to store queries include params, the query caches can grow to an unbounded size in certain circumstances (like text search input as a param). This allows the the cache to be disabled manually in circumstances where that is problematic, such as server side rendering.

This PR does not disable the `fragmentCache` in `buildRQL`, which I believe only uses fragments and initial variables, and so has a bounded upper limit in terms of size.
Closes #1302

Reviewed By: josephsavona

Differential Revision: D3792224

Pulled By: wincent

fbshipit-source-id: 9efea99b5422cc2f9ba798f140911d90e1c6842e
  • Loading branch information
KCraw authored and Facebook Github Bot 9 committed Aug 30, 2016
1 parent b0d115d commit f3b3f93
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 50 deletions.
2 changes: 2 additions & 0 deletions src/RelayPublic.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const RelayInternals = require('RelayInternals');
const RelayMutation = require('RelayMutation');
const RelayPropTypes = require('RelayPropTypes');
const RelayQL = require('RelayQL');
const RelayQueryCaching = require('RelayQueryCaching');
const RelayQueryConfig = require('RelayQueryConfig');
const RelayReadyStateRenderer = require('RelayReadyStateRenderer');
const RelayRenderer = require('RelayRenderer');
Expand Down Expand Up @@ -52,6 +53,7 @@ const RelayPublic = {
createContainer: RelayContainer.create,
createQuery: createRelayQuery,
getQueries: getRelayQueries,
disableQueryCaching: RelayQueryCaching.disable,
injectNetworkLayer: RelayStore.injectNetworkLayer.bind(RelayStore),
injectTaskScheduler: RelayStore.injectTaskScheduler.bind(RelayStore),
isContainer: isRelayContainer,
Expand Down
18 changes: 17 additions & 1 deletion src/container/getRelayQueries.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {RelayQuerySet} from 'RelayInternalTypes';
const RelayMetaRoute = require('RelayMetaRoute');
const RelayProfiler = require('RelayProfiler');
const RelayQuery = require('RelayQuery');
const RelayQueryCaching = require('RelayQueryCaching');
import type {RelayQueryConfigInterface} from 'RelayQueryConfig';

const buildRQL = require('buildRQL');
Expand All @@ -36,6 +37,10 @@ function getRelayQueries(
Component: RelayLazyContainer,
route: RelayQueryConfigInterface
): RelayQuerySet {
const queryCachingEnabled = RelayQueryCaching.getEnabled();
if (!queryCachingEnabled) {
return buildQuerySet(Component, route);
}
let cache = queryCache.get(Component);
if (!cache) {
cache = {};
Expand All @@ -45,6 +50,18 @@ function getRelayQueries(
if (cache.hasOwnProperty(cacheKey)) {
return cache[cacheKey];
}
const querySet = buildQuerySet(Component, route);
cache[cacheKey] = querySet;
return querySet;
}

/**
* @internal
*/
function buildQuerySet(
Component: RelayLazyContainer,
route: RelayQueryConfigInterface
): RelayQuerySet {
const querySet = {};
Component.getFragmentNames().forEach(fragmentName => {
querySet[fragmentName] = null;
Expand Down Expand Up @@ -92,7 +109,6 @@ function getRelayQueries(
}
querySet[queryName] = null;
});
cache[cacheKey] = querySet;
return querySet;
}

Expand Down
20 changes: 20 additions & 0 deletions src/query/__tests__/buildRQL-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const QueryBuilder = require('QueryBuilder');
const React = require('React');
const Relay = require('Relay');
const RelayQuery = require('RelayQuery');
const RelayQueryCaching = require('RelayQueryCaching');
const RelayTestUtils = require('RelayTestUtils');

const buildRQL = require('buildRQL');
Expand Down Expand Up @@ -52,6 +53,11 @@ describe('buildRQL', () => {
jasmine.addMatchers(RelayTestUtils.matchers);
});

afterEach(() => {
// Ensure RelayQueryCaching reverts to pristine state.
jest.resetModuleRegistry();
});

describe('Fragment()', () => {
it('returns undefined if the node is not a fragment', () => {
const builder = () => Relay.QL`
Expand Down Expand Up @@ -190,6 +196,20 @@ describe('buildRQL', () => {
expect(node1 === node2).toBe(false);
});

it('returns different queries for the same component if cache is disabled', () => {
RelayQueryCaching.disable();
const builder = Component => Relay.QL`
query {
node(id:$id) {
${Component.getFragment('foo')}
}
}
`;
const node1 = buildRQL.Query(builder, MockContainer, 'foo', {id: null});
const node2 = buildRQL.Query(builder, MockContainer, 'foo', {id: null});
expect(node1).not.toBe(node2);
});

it('filters the variables passed to components', () => {
const builder = (Component, variables) => Relay.QL`
query {
Expand Down
118 changes: 69 additions & 49 deletions src/query/buildRQL.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const Map = require('Map');
const QueryBuilder = require('QueryBuilder');
import type {RelayConcreteNode} from 'RelayQL';
const RelayProfiler = require('RelayProfiler');
const RelayQueryCaching = require('RelayQueryCaching');
import type {RelayContainer, Variables} from 'RelayTypes';

const filterObject = require('filterObject');
Expand Down Expand Up @@ -96,58 +97,20 @@ const buildRQL = {
queryName: string,
values: Variables
): ?ConcreteQuery {
let componentCache = queryCache.get(queryBuilder);
const queryCacheEnabled = RelayQueryCaching.getEnabled();
let node;
if (!componentCache) {
componentCache = new Map();
queryCache.set(queryBuilder, componentCache);
if (!queryCacheEnabled) {
node = buildNode(queryBuilder, Component, queryName, values);
} else {
node = componentCache.get(Component);
}
if (!node) {
const variables = toVariables(values);
invariant(
!isDeprecatedCallWithArgCountGreaterThan(queryBuilder, 2),
'Relay.QL: Deprecated usage detected. If you are trying to define a ' +
'query, use `(Component, variables) => Relay.QL`.'
);
if (isDeprecatedCallWithArgCountGreaterThan(queryBuilder, 0)) {
node = queryBuilder(Component, variables);
let componentCache = queryCache.get(queryBuilder);
if (!componentCache) {
componentCache = new Map();
queryCache.set(queryBuilder, componentCache);
} else {
node = queryBuilder(Component, variables);
const query = QueryBuilder.getQuery(node);
if (query) {
let hasFragment = false;
let hasScalarFieldsOnly = true;
if (query.children) {
query.children.forEach(child => {
if (child) {
hasFragment = hasFragment || child.kind === 'Fragment';
hasScalarFieldsOnly = hasScalarFieldsOnly && (
child.kind === 'Field' &&
(!child.children || child.children.length === 0)
);
}
});
}
if (!hasFragment) {
const children = query.children ? [...query.children] : [];
invariant(
hasScalarFieldsOnly,
'Relay.QL: Expected query `%s` to be empty. For example, use ' +
'`node(id: $id)`, not `node(id: $id) { ... }`.',
query.fieldName
);
const fragmentVariables = filterObject(variables, (_, name) =>
Component.hasVariable(name)
);
children.push(Component.getFragment(queryName, fragmentVariables));
node = {
...query,
children,
};
}
}
node = componentCache.get(Component);
}
if (!node) {
node = buildNode(queryBuilder, Component, queryName, values);
}
componentCache.set(Component, node);
}
Expand All @@ -158,6 +121,63 @@ const buildRQL = {
},
};

/**
* @internal
*/
function buildNode(
queryBuilder: RelayQLQueryBuilder,
Component: any,
queryName: string,
values: Variables
): ?mixed {
const variables = toVariables(values);
invariant(
!isDeprecatedCallWithArgCountGreaterThan(queryBuilder, 2),
'Relay.QL: Deprecated usage detected. If you are trying to define a ' +
'query, use `(Component, variables) => Relay.QL`.'
);
let node;
if (isDeprecatedCallWithArgCountGreaterThan(queryBuilder, 0)) {
node = queryBuilder(Component, variables);
} else {
node = queryBuilder(Component, variables);
const query = QueryBuilder.getQuery(node);
if (query) {
let hasFragment = false;
let hasScalarFieldsOnly = true;
if (query.children) {
query.children.forEach(child => {
if (child) {
hasFragment = hasFragment || child.kind === 'Fragment';
hasScalarFieldsOnly = hasScalarFieldsOnly && (
child.kind === 'Field' &&
(!child.children || child.children.length === 0)
);
}
});
}
if (!hasFragment) {
const children = query.children ? [...query.children] : [];
invariant(
hasScalarFieldsOnly,
'Relay.QL: Expected query `%s` to be empty. For example, use ' +
'`node(id: $id)`, not `node(id: $id) { ... }`.',
query.fieldName
);
const fragmentVariables = filterObject(variables, (_, name) =>
Component.hasVariable(name)
);
children.push(Component.getFragment(queryName, fragmentVariables));
node = {
...query,
children,
};
}
}
}
return node;
}

function toVariables(variables: Variables): {
[key: string]: $FlowIssue; // ConcreteCallVariable should flow into mixed
} {
Expand Down
37 changes: 37 additions & 0 deletions src/tools/RelayQueryCache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule RelayQueryCaching
* @flow
*/

'use strict';

let queryCachingEnabled = true;

/**
* Methods for configuring caching of Relay queries.
*/
const RelayQueryCaching = {
/**
* `disable` turns off caching of queries for `getRelayQueries` and
* `buildRQL`.
*/
disable(): void {
queryCachingEnabled = false;
},

/**
* @internal
*/
getEnabled(): boolean {
return queryCachingEnabled;
},
};

module.exports = RelayQueryCaching;
12 changes: 12 additions & 0 deletions src/tools/__mocks__/RelayQueryCaching.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

'use strict';

module.exports = require.requireActual('RelayQueryCaching');

0 comments on commit f3b3f93

Please sign in to comment.