diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8563a5a5c09..3ea42ff5e21 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
**Note:** This is a cumulative changelog that outlines all of the Apollo Client project child package changes that were bundled into a specific `apollo-client` release.
+## Apollo Client vNEXT
+
+### Apollo Cache In-Memory
+
+- Support `new InMemoryCache({ freezeResults: true })` to help enforce immutability.
+ [@benjamn](https://github.com/benjamn) in [#4514](https://github.com/apollographql/apollo-client/pull/4514)
+
## Apollo Client 2.5.1
### apollo-client 2.5.1
diff --git a/package.json b/package.json
index 0b6e23580df..5a610388858 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,7 @@
{
"name": "apollo-cache-inmemory",
"path": "./packages/apollo-cache-inmemory/lib/bundle.cjs.min.js",
- "maxSize": "4.9 kB"
+ "maxSize": "4.95 kB"
},
{
"name": "apollo-client",
diff --git a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/cache.ts.snap b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/cache.ts.snap
index 0cd8b06df71..d972da111c5 100644
--- a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/cache.ts.snap
+++ b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/cache.ts.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Cache writeFragment will write some deeply nested data into the store at any id (1/2) 1`] = `
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (1/3) 1`] = `
Object {
"bar": Object {
"i": 7,
@@ -17,7 +17,7 @@ Object {
}
`;
-exports[`Cache writeFragment will write some deeply nested data into the store at any id (1/2) 2`] = `
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (1/3) 2`] = `
Object {
"bar": Object {
"i": 7,
@@ -38,7 +38,7 @@ Object {
}
`;
-exports[`Cache writeFragment will write some deeply nested data into the store at any id (1/2) 3`] = `
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (1/3) 3`] = `
Object {
"bar": Object {
"i": 10,
@@ -59,7 +59,7 @@ Object {
}
`;
-exports[`Cache writeFragment will write some deeply nested data into the store at any id (1/2) 4`] = `
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (1/3) 4`] = `
Object {
"bar": Object {
"i": 10,
@@ -80,7 +80,7 @@ Object {
}
`;
-exports[`Cache writeFragment will write some deeply nested data into the store at any id (1/2) 5`] = `
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (1/3) 5`] = `
Object {
"bar": Object {
"i": 7,
@@ -101,7 +101,7 @@ Object {
}
`;
-exports[`Cache writeFragment will write some deeply nested data into the store at any id (1/2) 6`] = `
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (1/3) 6`] = `
Object {
"bar": Object {
"i": 10,
@@ -122,7 +122,7 @@ Object {
}
`;
-exports[`Cache writeFragment will write some deeply nested data into the store at any id (2/2) 1`] = `
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (2/3) 1`] = `
Object {
"bar": Object {
"i": 7,
@@ -139,7 +139,7 @@ Object {
}
`;
-exports[`Cache writeFragment will write some deeply nested data into the store at any id (2/2) 2`] = `
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (2/3) 2`] = `
Object {
"bar": Object {
"i": 7,
@@ -160,7 +160,7 @@ Object {
}
`;
-exports[`Cache writeFragment will write some deeply nested data into the store at any id (2/2) 3`] = `
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (2/3) 3`] = `
Object {
"bar": Object {
"i": 10,
@@ -181,7 +181,7 @@ Object {
}
`;
-exports[`Cache writeFragment will write some deeply nested data into the store at any id (2/2) 4`] = `
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (2/3) 4`] = `
Object {
"bar": Object {
"i": 10,
@@ -202,7 +202,7 @@ Object {
}
`;
-exports[`Cache writeFragment will write some deeply nested data into the store at any id (2/2) 5`] = `
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (2/3) 5`] = `
Object {
"bar": Object {
"i": 7,
@@ -223,7 +223,129 @@ Object {
}
`;
-exports[`Cache writeFragment will write some deeply nested data into the store at any id (2/2) 6`] = `
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (2/3) 6`] = `
+Object {
+ "bar": Object {
+ "i": 10,
+ "j": 11,
+ "k": 12,
+ },
+ "foo": Object {
+ "e": 4,
+ "f": 5,
+ "g": 6,
+ "h": Object {
+ "generated": false,
+ "id": "bar",
+ "type": "id",
+ "typename": "Bar",
+ },
+ },
+}
+`;
+
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (3/3) 1`] = `
+Object {
+ "bar": Object {
+ "i": 7,
+ },
+ "foo": Object {
+ "e": 4,
+ "h": Object {
+ "generated": false,
+ "id": "bar",
+ "type": "id",
+ "typename": undefined,
+ },
+ },
+}
+`;
+
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (3/3) 2`] = `
+Object {
+ "bar": Object {
+ "i": 7,
+ "j": 8,
+ "k": 9,
+ },
+ "foo": Object {
+ "e": 4,
+ "f": 5,
+ "g": 6,
+ "h": Object {
+ "generated": false,
+ "id": "bar",
+ "type": "id",
+ "typename": undefined,
+ },
+ },
+}
+`;
+
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (3/3) 3`] = `
+Object {
+ "bar": Object {
+ "i": 10,
+ "j": 8,
+ "k": 9,
+ },
+ "foo": Object {
+ "e": 4,
+ "f": 5,
+ "g": 6,
+ "h": Object {
+ "generated": false,
+ "id": "bar",
+ "type": "id",
+ "typename": undefined,
+ },
+ },
+}
+`;
+
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (3/3) 4`] = `
+Object {
+ "bar": Object {
+ "i": 10,
+ "j": 11,
+ "k": 12,
+ },
+ "foo": Object {
+ "e": 4,
+ "f": 5,
+ "g": 6,
+ "h": Object {
+ "generated": false,
+ "id": "bar",
+ "type": "id",
+ "typename": undefined,
+ },
+ },
+}
+`;
+
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (3/3) 5`] = `
+Object {
+ "bar": Object {
+ "i": 7,
+ "j": 8,
+ "k": 9,
+ },
+ "foo": Object {
+ "e": 4,
+ "f": 5,
+ "g": 6,
+ "h": Object {
+ "generated": false,
+ "id": "bar",
+ "type": "id",
+ "typename": "Bar",
+ },
+ },
+}
+`;
+
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (3/3) 6`] = `
Object {
"bar": Object {
"i": 10,
diff --git a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/mapCache.ts.snap b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/mapCache.ts.snap
index c3711026b09..4682281f7f0 100644
--- a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/mapCache.ts.snap
+++ b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/mapCache.ts.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (1/2) 1`] = `
+exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (1/3) 1`] = `
Object {
"bar": Object {
"i": 7,
@@ -17,7 +17,7 @@ Object {
}
`;
-exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (1/2) 2`] = `
+exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (1/3) 2`] = `
Object {
"bar": Object {
"i": 7,
@@ -38,7 +38,7 @@ Object {
}
`;
-exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (1/2) 3`] = `
+exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (1/3) 3`] = `
Object {
"bar": Object {
"i": 10,
@@ -59,7 +59,7 @@ Object {
}
`;
-exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (1/2) 4`] = `
+exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (1/3) 4`] = `
Object {
"bar": Object {
"i": 10,
@@ -80,7 +80,7 @@ Object {
}
`;
-exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (1/2) 5`] = `
+exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (1/3) 5`] = `
Object {
"bar": Object {
"i": 7,
@@ -101,7 +101,7 @@ Object {
}
`;
-exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (1/2) 6`] = `
+exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (1/3) 6`] = `
Object {
"bar": Object {
"i": 10,
@@ -122,7 +122,7 @@ Object {
}
`;
-exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (2/2) 1`] = `
+exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (2/3) 1`] = `
Object {
"bar": Object {
"i": 7,
@@ -139,7 +139,7 @@ Object {
}
`;
-exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (2/2) 2`] = `
+exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (2/3) 2`] = `
Object {
"bar": Object {
"i": 7,
@@ -160,7 +160,7 @@ Object {
}
`;
-exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (2/2) 3`] = `
+exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (2/3) 3`] = `
Object {
"bar": Object {
"i": 10,
@@ -181,7 +181,7 @@ Object {
}
`;
-exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (2/2) 4`] = `
+exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (2/3) 4`] = `
Object {
"bar": Object {
"i": 10,
@@ -202,7 +202,7 @@ Object {
}
`;
-exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (2/2) 5`] = `
+exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (2/3) 5`] = `
Object {
"bar": Object {
"i": 7,
@@ -223,7 +223,129 @@ Object {
}
`;
-exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (2/2) 6`] = `
+exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (2/3) 6`] = `
+Object {
+ "bar": Object {
+ "i": 10,
+ "j": 11,
+ "k": 12,
+ },
+ "foo": Object {
+ "e": 4,
+ "f": 5,
+ "g": 6,
+ "h": Object {
+ "generated": false,
+ "id": "bar",
+ "type": "id",
+ "typename": "Bar",
+ },
+ },
+}
+`;
+
+exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (3/3) 1`] = `
+Object {
+ "bar": Object {
+ "i": 7,
+ },
+ "foo": Object {
+ "e": 4,
+ "h": Object {
+ "generated": false,
+ "id": "bar",
+ "type": "id",
+ "typename": undefined,
+ },
+ },
+}
+`;
+
+exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (3/3) 2`] = `
+Object {
+ "bar": Object {
+ "i": 7,
+ "j": 8,
+ "k": 9,
+ },
+ "foo": Object {
+ "e": 4,
+ "f": 5,
+ "g": 6,
+ "h": Object {
+ "generated": false,
+ "id": "bar",
+ "type": "id",
+ "typename": undefined,
+ },
+ },
+}
+`;
+
+exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (3/3) 3`] = `
+Object {
+ "bar": Object {
+ "i": 10,
+ "j": 8,
+ "k": 9,
+ },
+ "foo": Object {
+ "e": 4,
+ "f": 5,
+ "g": 6,
+ "h": Object {
+ "generated": false,
+ "id": "bar",
+ "type": "id",
+ "typename": undefined,
+ },
+ },
+}
+`;
+
+exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (3/3) 4`] = `
+Object {
+ "bar": Object {
+ "i": 10,
+ "j": 11,
+ "k": 12,
+ },
+ "foo": Object {
+ "e": 4,
+ "f": 5,
+ "g": 6,
+ "h": Object {
+ "generated": false,
+ "id": "bar",
+ "type": "id",
+ "typename": undefined,
+ },
+ },
+}
+`;
+
+exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (3/3) 5`] = `
+Object {
+ "bar": Object {
+ "i": 7,
+ "j": 8,
+ "k": 9,
+ },
+ "foo": Object {
+ "e": 4,
+ "f": 5,
+ "g": 6,
+ "h": Object {
+ "generated": false,
+ "id": "bar",
+ "type": "id",
+ "typename": "Bar",
+ },
+ },
+}
+`;
+
+exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (3/3) 6`] = `
Object {
"bar": Object {
"i": 10,
diff --git a/packages/apollo-cache-inmemory/src/__tests__/cache.ts b/packages/apollo-cache-inmemory/src/__tests__/cache.ts
index f0cb6e27ed1..58e89177908 100644
--- a/packages/apollo-cache-inmemory/src/__tests__/cache.ts
+++ b/packages/apollo-cache-inmemory/src/__tests__/cache.ts
@@ -25,6 +25,12 @@ describe('Cache', () => {
resultCaching: false,
}).restore(cloneDeep(data))
),
+ initialDataForCaches.map(
+ data => new InMemoryCache({
+ addTypename: false,
+ freezeResults: true,
+ }).restore(cloneDeep(data))
+ ),
];
cachesList.forEach((caches, i) => {
@@ -48,6 +54,11 @@ describe('Cache', () => {
...config,
resultCaching: false,
}),
+ new InMemoryCache({
+ addTypename: false,
+ ...config,
+ freezeResults: true,
+ }),
];
caches.forEach((cache, i) => {
diff --git a/packages/apollo-cache-inmemory/src/__tests__/roundtrip.ts b/packages/apollo-cache-inmemory/src/__tests__/roundtrip.ts
index ad4a337c711..30a70ae8914 100644
--- a/packages/apollo-cache-inmemory/src/__tests__/roundtrip.ts
+++ b/packages/apollo-cache-inmemory/src/__tests__/roundtrip.ts
@@ -16,8 +16,25 @@ import {
const fragmentMatcherFunction = new HeuristicFragmentMatcher().match;
+function assertDeeplyFrozen(value: any, stack: any[] = []) {
+ if (
+ value !== null &&
+ typeof value === 'object' &&
+ stack.indexOf(value) < 0
+ ) {
+ expect(Object.isExtensible(value)).toBe(false);
+ expect(Object.isFrozen(value)).toBe(true);
+ stack.push(value);
+ Object.keys(value).forEach(key => {
+ assertDeeplyFrozen(value[key], stack);
+ });
+ expect(stack.pop()).toBe(value);
+ }
+}
+
function storeRoundtrip(query: DocumentNode, result: any, variables = {}) {
const reader = new StoreReader();
+ const immutableReader = new StoreReader({ freezeResults: true });
const writer = new StoreWriter();
const store = writer.writeQueryToStore({
@@ -41,6 +58,22 @@ function storeRoundtrip(query: DocumentNode, result: any, variables = {}) {
expect(store).toBeInstanceOf(DepTrackingCache);
expect(reader.readQueryFromStore(readOptions)).toBe(reconstructedResult);
+ const immutableResult = immutableReader.readQueryFromStore(readOptions);
+ expect(immutableResult).toEqual(reconstructedResult);
+ expect(immutableReader.readQueryFromStore(readOptions)).toBe(immutableResult);
+ if (process.env.NODE_ENV !== 'production') {
+ try {
+ // Note: this illegal assignment will only throw in strict mode, but that's
+ // safe to assume because this test file is a module.
+ (immutableResult as any).illegal = "this should not work";
+ throw new Error("unreached");
+ } catch (e) {
+ expect(e.message).not.toMatch(/unreached/);
+ expect(e).toBeInstanceOf(TypeError);
+ }
+ assertDeeplyFrozen(immutableResult);
+ }
+
// Now make sure subtrees of the result are identical even after we write
// an additional bogus field to the store.
writer.writeQueryToStore({
@@ -203,6 +236,19 @@ describe('roundtrip', () => {
});
it('with GraphQLJSON scalar type', () => {
+ const updateClub = {
+ uid: '1d7f836018fc11e68d809dfee940f657',
+ name: 'Eple',
+ settings: {
+ name: 'eple',
+ currency: 'AFN',
+ calendarStretch: 2,
+ defaultPreAllocationPeriod: 1,
+ confirmationEmailCopy: null,
+ emailDomains: null,
+ },
+ };
+
storeRoundtrip(
gql`
{
@@ -214,20 +260,14 @@ describe('roundtrip', () => {
}
`,
{
- updateClub: {
- uid: '1d7f836018fc11e68d809dfee940f657',
- name: 'Eple',
- settings: {
- name: 'eple',
- currency: 'AFN',
- calendarStretch: 2,
- defaultPreAllocationPeriod: 1,
- confirmationEmailCopy: null,
- emailDomains: null,
- },
- },
+ updateClub,
},
);
+
+ // Just because we read from the store using { freezeResults: true }, the
+ // original data should not be frozen.
+ expect(Object.isExtensible(updateClub)).toBe(true);
+ expect(Object.isFrozen(updateClub)).toBe(false);
});
describe('directives', () => {
diff --git a/packages/apollo-cache-inmemory/src/inMemoryCache.ts b/packages/apollo-cache-inmemory/src/inMemoryCache.ts
index 3d4af3f4f4a..85e3ba0088e 100644
--- a/packages/apollo-cache-inmemory/src/inMemoryCache.ts
+++ b/packages/apollo-cache-inmemory/src/inMemoryCache.ts
@@ -26,6 +26,7 @@ import { ObjectCache } from './objectCache';
export interface InMemoryCacheConfig extends ApolloReducerConfig {
resultCaching?: boolean;
+ freezeResults?: boolean;
}
const defaultConfig: InMemoryCacheConfig = {
@@ -33,6 +34,7 @@ const defaultConfig: InMemoryCacheConfig = {
dataIdFromObject: defaultDataIdFromObject,
addTypename: true,
resultCaching: true,
+ freezeResults: false,
};
export function defaultDataIdFromObject(result: any): string | null {
@@ -128,8 +130,11 @@ export class InMemoryCache extends ApolloCache {
// original this.data cache object.
this.optimisticData = this.data;
- this.storeReader = new StoreReader(this.cacheKeyRoot);
this.storeWriter = new StoreWriter();
+ this.storeReader = new StoreReader({
+ cacheKeyRoot: this.cacheKeyRoot,
+ freezeResults: config.freezeResults,
+ });
const cache = this;
const { maybeBroadcastWatch } = cache;
diff --git a/packages/apollo-cache-inmemory/src/readFromStore.ts b/packages/apollo-cache-inmemory/src/readFromStore.ts
index 495b120a1c8..01b14678778 100644
--- a/packages/apollo-cache-inmemory/src/readFromStore.ts
+++ b/packages/apollo-cache-inmemory/src/readFromStore.ts
@@ -21,6 +21,7 @@ import {
shouldInclude,
toIdValue,
mergeDeepArray,
+ maybeDeepFreeze,
} from 'apollo-utilities';
import { Cache } from 'apollo-cache';
@@ -93,15 +94,24 @@ type ExecSelectionSetOptions = {
execContext: ExecContext;
};
+export interface StoreReaderConfig {
+ cacheKeyRoot?: CacheKeyNode;
+ freezeResults?: boolean;
+};
+
export class StoreReader {
- constructor(
- private cacheKeyRoot = new CacheKeyNode,
- ) {
- const reader = this;
+ private freezeResults: boolean;
+
+ constructor({
+ cacheKeyRoot = new CacheKeyNode,
+ freezeResults = false,
+ }: StoreReaderConfig = {}) {
const {
executeStoreQuery,
executeSelectionSet,
- } = reader;
+ } = this;
+
+ this.freezeResults = freezeResults;
this.executeStoreQuery = wrap((options: ExecStoreQueryOptions) => {
return executeStoreQuery.call(this, options);
@@ -117,7 +127,7 @@ export class StoreReader {
// underlying store is capable of tracking dependencies and invalidating
// the cache when relevant data have changed.
if (contextValue.store instanceof DepTrackingCache) {
- return reader.cacheKeyRoot.lookup(
+ return cacheKeyRoot.lookup(
query,
contextValue.store,
fragmentMatcher,
@@ -138,7 +148,7 @@ export class StoreReader {
execContext,
}: ExecSelectionSetOptions) {
if (execContext.contextValue.store instanceof DepTrackingCache) {
- return reader.cacheKeyRoot.lookup(
+ return cacheKeyRoot.lookup(
selectionSet,
execContext.contextValue.store,
execContext.fragmentMatcher,
@@ -376,6 +386,10 @@ export class StoreReader {
// defensive shallow copies than necessary.
finalResult.result = mergeDeepArray(objectsToMerge);
+ if (this.freezeResults && process.env.NODE_ENV !== 'production') {
+ Object.freeze(finalResult.result);
+ }
+
return finalResult;
}
@@ -417,6 +431,9 @@ export class StoreReader {
// Handle all scalar types here
if (!field.selectionSet) {
assertSelectionSetForIdValue(field, readStoreResult.result);
+ if (this.freezeResults && process.env.NODE_ENV !== 'production') {
+ maybeDeepFreeze(readStoreResult);
+ }
return readStoreResult;
}
@@ -495,6 +512,10 @@ export class StoreReader {
return item;
});
+ if (this.freezeResults && process.env.NODE_ENV !== 'production') {
+ Object.freeze(result);
+ }
+
return { result, missing };
}
}