Repository: apollographql/apollo-client

Problem statement:
v3.0.0-beta.16 mutating local state is not reflected in all components with useQuery
**Intended outcome:**
Store local state in apollo cache and when updating the local state then all components that have a useQuery against that data will be updated.

**Actual outcome:**
It appears only the first subscriber to the useQuery receives updates. 

**How to reproduce the issue:**
At the following code sandbox you can see some local state stored in apollo.  There are three components that are rendering the result of the same useQuery query.  When the Child component calls a mutation on that data, only the Parent component updates.  Both the Child component and the OtherChild component do not update.

If the query is removed from the Parent component, then only the Child component updates.
If the query is removed from both the Parent and Child, then the OtherChild component updates.

https://codesandbox.io/s/apollo-client-prob-6ywxk

**Versions**
- @apollo/client 3.0.0-beta.16
- graphql 14.5.8
- react 16.12.0
- react-dom 16.12.0
- react-scripts 3.0.1

v3.0.0-beta.16 mutating local state is not reflected in all components with useQuery
**Intended outcome:**
Store local state in apollo cache and when updating the local state then all components that have a useQuery against that data will be updated.

**Actual outcome:**
It appears only the first subscriber to the useQuery receives updates. 

**How to reproduce the issue:**
At the following code sandbox you can see some local state stored in apollo.  There are three components that are rendering the result of the same useQuery query.  When the Child component calls a mutation on that data, only the Parent component updates.  Both the Child component and the OtherChild component do not update.

If the query is removed from the Parent component, then only the Child component updates.
If the query is removed from both the Parent and Child, then the OtherChild component updates.

https://codesandbox.io/s/apollo-client-prob-6ywxk

**Versions**
- @apollo/client 3.0.0-beta.16
- graphql 14.5.8
- react 16.12.0
- react-dom 16.12.0
- react-scripts 3.0.1



Hints:

Patch:
diff --git a/config/dangerfile.ts b/config/dangerfile.ts
deleted file mode 100644
--- a/config/dangerfile.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-const { includes } = require('lodash');
-const fs = require('fs');
-
-// Setup
-const github = danger.github;
-const pr = github.pr;
-const commits = github.commits;
-const modified = danger.git.modified_files;
-const bodyAndTitle = (pr.body + pr.title).toLowerCase();
-console.log(commits.map(({ sha }) => sha));
-
-// Custom modifiers for people submitting PRs to be able to say "skip this"
-const trivialPR = bodyAndTitle.includes('trivial');
-const acceptedNoTests = bodyAndTitle.includes('skip new tests');
-
-const typescriptOnly = (file: string) => includes(file, '.ts');
-const filesOnly = (file: string) =>
-  fs.existsSync(file) && fs.lstatSync(file).isFile();
-
-// Custom subsets of known files
-const modifiedAppFiles = modified
-  .filter(p => includes(p, 'src/') || includes(p, 'test/'))
-  .filter(p => filesOnly(p) && typescriptOnly(p));
-
-// Takes a list of file paths, and converts it into clickable links
-const linkableFiles = paths => {
-  const repoURL = pr.head.repo.html_url;
-  const ref = pr.head.ref;
-  const links = paths.map(path => {
-    return createLink(`${repoURL}/blob/${ref}/${path}`, path);
-  });
-  return toSentence(links);
-};
-
-// ["1", "2", "3"] to "1, 2 and 3"
-const toSentence = (array: Array<string>): string => {
-  if (array.length === 1) {
-    return array[0];
-  }
-  return array.slice(0, array.length - 1).join(', ') + ' and ' + array.pop();
-};
-
-// ("/href/thing", "name") to "<a href="/href/thing">name</a>"
-const createLink = (href: string, text: string): string =>
-  `<a href='${href}'>${text}</a>`;
-
-// Raise about missing code inside files
-const raiseIssueAboutPaths = (
-  type: Function,
-  paths: string[],
-  codeToInclude: string,
-) => {
-  if (paths.length > 0) {
-    const files = linkableFiles(paths);
-    const strict = '<code>' + codeToInclude + '</code>';
-    type(`Please ensure that ${strict} is enabled on: ${files}`);
-  }
-};
-
-console.log('GitHub PR Username:', pr && pr.user && pr.user.login);
-
-const githubBotUsernames = ['greenkeeper', 'renovate[bot]'];
-
-const isBot =
-  pr && pr.user && pr.user.login && githubBotUsernames.includes(pr.user.login);
-
-// Rules
-if (!isBot) {
-  // make sure someone else reviews these changes
-  // const someoneAssigned = danger.github.pr.assignee;
-  // if (someoneAssigned === null) {
-  //   warn(
-  //     'Please assign someone to merge this PR, and optionally include people who should review.'
-  //   );
-  // }
-
-  // When there are app-changes and it's not a PR marked as trivial, expect
-  // there to be CHANGELOG changes.
-  const changelogChanges = modified.some(x => x.indexOf('CHANGELOG') > -1);
-  if (modifiedAppFiles.length > 0 && !trivialPR && !changelogChanges) {
-    fail('No CHANGELOG added.');
-  }
-
-  // No PR is too small to warrant a paragraph or two of summary
-  if (pr.body.length === 0) {
-    fail('Please add a description to your PR.');
-  }
-
-  const hasAppChanges = modifiedAppFiles.length > 0;
-
-  const testChanges = modifiedAppFiles.filter(filepath =>
-    filepath.includes('test'),
-  );
-  const hasTestChanges = testChanges.length > 0;
-
-  // Warn when there is a big PR
-  const bigPRThreshold = 500;
-  if (github.pr.additions + github.pr.deletions > bigPRThreshold) {
-    warn(':exclamation: Big PR');
-  }
-
-  // Warn if there are library changes, but not tests
-  if (hasAppChanges && !hasTestChanges) {
-    warn(
-      "There are library changes, but not tests. That's OK as long as you're refactoring existing code",
-    );
-  }
-
-  // Be careful of leaving testing shortcuts in the codebase
-  const onlyTestFiles = testChanges.filter(x => {
-    const content = fs.readFileSync(x).toString();
-    return (
-      content.includes('it.only') ||
-      content.includes('describe.only') ||
-      content.includes('fdescribe') ||
-      content.includes('fit(')
-    );
-  });
-  raiseIssueAboutPaths(fail, onlyTestFiles, 'an `only` was left in the test');
-}
diff --git a/config/jest.config.settings.js b/config/jest.config.js
similarity index 69%
rename from config/jest.config.settings.js
rename to config/jest.config.js
--- a/config/jest.config.settings.js
+++ b/config/jest.config.js
@@ -1,21 +1,19 @@
 module.exports = {
+  rootDir: '..',
   transform: {
     '.(ts|tsx)': 'ts-jest',
   },
-
   globals: {
     'ts-jest': {
       diagnostics: false,
     },
   },
-
   moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
   testURL: 'http://localhost',
-
-  testMatch: ['<rootDir>/src/**/__tests__/**/*.ts'],
   testPathIgnorePatterns: [
     '/node_modules/',
-    '/lib/',
-    '<rootDir>/lib/',
+    '/dist/'
   ],
+  modulePathIgnorePatterns: ['/dist/'],
+  setupFiles: ['<rootDir>/src/config/jest/setup.ts'],
 };
diff --git a/config/prepareDist.js b/config/prepareDist.js
new file mode 100644
--- /dev/null
+++ b/config/prepareDist.js
@@ -0,0 +1,111 @@
+// The Apollo Client source that is published to npm is located in the
+// "dist" directory. This utility script is called when building Apollo Client,
+// to make sure the "dist" directory is prepared for publishing.
+//
+// This script will:
+//
+// - Copy the current root package.json into "dist" after adjusting it for
+//   publishing.
+// - Copy the supporting files from the root into "dist" (e.g. `README.MD`,
+//   `LICENSE`, etc.).
+// - Create a new `package.json` for each sub-set bundle we support, and
+//   store it in the appropriate dist sub-directory.
+
+const fs = require('fs');
+const recast = require('recast');
+
+const distRoot = `${__dirname}/../dist`;
+
+
+/* @apollo/client */
+
+const packageJson = require('../package.json');
+
+// The root package.json is marked as private to prevent publishing
+// from happening in the root of the project. This sets the package back to
+// public so it can be published from the "dist" directory.
+packageJson.private = false;
+
+// Remove package.json items that we don't need to publish
+delete packageJson.scripts;
+delete packageJson.bundlesize;
+
+// The root package.json points to the CJS/ESM source in "dist", to support
+// on-going package development (e.g. running tests, supporting npm link, etc.).
+// When publishing from "dist" however, we need to update the package.json
+// to point to the files within the same directory.
+const distPackageJson = JSON.stringify(packageJson, (_key, value) => {
+  if (typeof value === 'string' && value.startsWith('./dist/')) {
+    const parts = value.split('/');
+    parts.splice(1, 1); // remove dist
+    return parts.join('/');
+  }
+  return value;
+}, 2) + "\n";
+
+// Save the modified package.json to "dist"
+fs.writeFileSync(`${distRoot}/package.json`, distPackageJson);
+
+// Copy supporting files into "dist"
+const srcDir = `${__dirname}/..`;
+const destDir = `${srcDir}/dist`;
+fs.copyFileSync(`${srcDir}/README.md`,  `${destDir}/README.md`);
+fs.copyFileSync(`${srcDir}/LICENSE`,  `${destDir}/LICENSE`);
+
+
+/* @apollo/client/core, @apollo/client/cache, @apollo/client/utilities */
+
+function buildPackageJson(bundleName) {
+  return JSON.stringify({
+    name: `@apollo/client/${bundleName}`,
+    main: `${bundleName}.cjs.js`,
+    module: 'index.js',
+    types: 'index.d.ts',
+  }, null, 2) + "\n";
+}
+
+function loadExportNames(bundleName) {
+  const indexSrc =
+    fs.readFileSync(`${distRoot}/${bundleName}/index.js`);
+  const exportNames = [];
+  recast.visit(recast.parse(indexSrc), {
+    visitExportSpecifier(path) {
+      exportNames.push(path.value.exported.name);
+      return false;
+    },
+  });
+  return exportNames;
+}
+
+function writeCjsIndex(bundleName, exportNames, includeNames = true) {
+  const filterPrefix = includeNames ? '' : '!';
+  fs.writeFileSync(`${distRoot}/${bundleName}/${bundleName}.cjs.js`, [
+    "const allExports = require('../apollo-client.cjs');",
+    `const names = new Set(${JSON.stringify(exportNames)});`,
+    "Object.keys(allExports).forEach(name => {",
+    `  if (${filterPrefix}names.has(name)) {`,
+    "    exports[name] = allExports[name];",
+    "  }",
+    "});",
+    "",
+  ].join('\n'));
+}
+
+// Create `core`, `cache` and `utilities` bundle package.json files, storing
+// them in their associated dist directory. This helps provide a way for the
+// Apollo Client core to be used without React (via `@apollo/client/core`),
+// and AC's cache and utilities to be used by themselves
+// (`@apollo/client/cache` and `@apollo/client/utilities`), via the
+// `core.cjs.js`, `cache.cjs.js` and `utilities.cjs.js` CommonJS entry point
+// files that only include the exports needed for each bundle.
+
+fs.writeFileSync(`${distRoot}/core/package.json`, buildPackageJson('core'));
+writeCjsIndex('core', loadExportNames('react'), false);
+
+fs.writeFileSync(`${distRoot}/cache/package.json`, buildPackageJson('cache'));
+writeCjsIndex('cache', loadExportNames('cache'));
+
+fs.writeFileSync(
+  `${distRoot}/utilities/package.json`,
+  buildPackageJson('utilities')
+);
diff --git a/config/rollup.config.js b/config/rollup.config.js
--- a/config/rollup.config.js
+++ b/config/rollup.config.js
@@ -1,152 +1,169 @@
 import nodeResolve from 'rollup-plugin-node-resolve';
-import typescriptPlugin from 'rollup-plugin-typescript2';
-import typescript from 'typescript';
-import path from 'path';
-import fs from 'fs';
-import { transformSync } from '@babel/core';
-import cjsModulesTransform from '@babel/plugin-transform-modules-commonjs';
-import umdModulesTransform from '@babel/plugin-transform-modules-umd';
 import invariantPlugin from 'rollup-plugin-invariant';
 import { terser as minify } from 'rollup-plugin-terser';
+import cjs from 'rollup-plugin-commonjs';
+import fs from 'fs';
 
-function onwarn(message) {
-  const suppressed = ['UNRESOLVED_IMPORT', 'THIS_IS_UNDEFINED'];
+import packageJson from '../package.json';
 
-  if (!suppressed.find(code => message.code === code)) {
-    return console.warn(message.message);
-  }
-}
+const distDir = './dist';
 
-const defaultGlobals = {
-  'apollo-client': 'apollo.core',
-  'apollo-cache': 'apolloCache.core',
-  'apollo-link': 'apolloLink.core',
-  'apollo-link-dedup': 'apolloLink.dedup',
-  'apollo-utilities': 'apollo.utilities',
-  'graphql-anywhere': 'graphqlAnywhere',
-  'graphql-anywhere/lib/async': 'graphqlAnywhere.async',
-  'apollo-boost': 'apollo.boost',
-  'tslib': 'tslib',
+const globals = {
+  tslib: 'tslib',
   'ts-invariant': 'invariant',
+  'symbol-observable': '$$observable',
+  'graphql/language/printer': 'print',
+  optimism: 'optimism',
+  'graphql/language/visitor': 'visitor',
+  'graphql/execution/execute': 'execute',
+  'graphql-tag': 'graphqlTag',
+  'fast-json-stable-stringify': 'stringify',
+  '@wry/equality': 'wryEquality',
+  graphql: 'graphql',
+  react: 'React',
+  'zen-observable': 'Observable',
 };
 
-export function rollup({
-  name,
-  input = './src/index.ts',
-  outputPrefix = 'bundle',
-  extraGlobals = {},
-}) {
-  const projectDir = path.join(__filename, '..');
-  console.info(`Building project esm ${projectDir}`);
-  const tsconfig = `${projectDir}/tsconfig.json`;
+const hasOwn = Object.prototype.hasOwnProperty;
 
-  const globals = {
-    ...defaultGlobals,
-    ...extraGlobals,
+function external(id) {
+  return hasOwn.call(globals, id);
+}
+
+function prepareESM(input, outputDir) {
+  return {
+    input,
+    external,
+    output: {
+      dir: outputDir,
+      format: 'esm',
+      sourcemap: true,
+    },
+    // The purpose of this job is to ensure each `./dist` ESM file is run
+    // through the `invariantPlugin`, with any resulting changes added
+    // directly back into each ESM file. By setting `preserveModules`
+    // to `true`, we're making sure Rollup doesn't attempt to create a single
+    // combined ESM bundle with the final result of running this job.
+    preserveModules: true,
+    plugins: [
+      nodeResolve(),
+      invariantPlugin({
+        // Instead of completely stripping InvariantError messages in
+        // production, this option assigns a numeric code to the
+        // production version of each error (unique to the call/throw
+        // location), which makes it much easier to trace production
+        // errors back to the unminified code where they were thrown,
+        // where the full error string can be found. See #4519.
+        errorCodes: true,
+      }),
+      cjs({
+        namedExports: {
+          'graphql-tag': ['gql'],
+        },
+      }),
+    ],
   };
+}
 
-  function external(id) {
-    return Object.prototype.hasOwnProperty.call(globals, id);
-  }
+function prepareCJS(input, output) {
+  return {
+    input,
+    external,
+    output: {
+      file: output,
+      format: 'cjs',
+      sourcemap: true,
+      exports: 'named',
+    },
+    plugins: [
+      nodeResolve(),
+      cjs({
+        namedExports: {
+          'graphql-tag': ['gql'],
+        },
+      }),
+    ],
+  };
+}
 
-  function outputFile(format) {
-    return './lib/' + outputPrefix + '.' + format + '.js';
-  }
+function prepareCJSMinified(input) {
+  return {
+    input,
+    output: {
+      file: input.replace('.js', '.min.js'),
+      format: 'cjs',
+    },
+    plugins: [
+      minify({
+        mangle: {
+          toplevel: true,
+        },
+        compress: {
+          global_defs: {
+            '@process.env.NODE_ENV': JSON.stringify('production'),
+          },
+        },
+      }),
+    ],
+  };
+}
 
-  function fromSource(format) {
-    return {
-      input,
-      external,
-      output: {
-        file: outputFile(format),
-        format,
-        sourcemap: true,
-      },
-      plugins: [
-        nodeResolve({
-          extensions: ['.ts', '.tsx'],
-          module: true,
-        }),
-        typescriptPlugin({ typescript, tsconfig }),
-        invariantPlugin({
-          // Instead of completely stripping InvariantError messages in
-          // production, this option assigns a numeric code to the
-          // production version of each error (unique to the call/throw
-          // location), which makes it much easier to trace production
-          // errors back to the unminified code where they were thrown,
-          // where the full error string can be found. See #4519.
-          errorCodes: true,
-        }),
-      ],
-      onwarn,
-    };
-  }
+function prepareUtilities() {
+  const utilsDistDir = `${distDir}/utilities`;
+  return {
+    input: `${utilsDistDir}/index.js`,
+    external,
+    output: {
+      file: `${utilsDistDir}/utilities.cjs.js`,
+      format: 'cjs',
+      sourcemap: true,
+      exports: 'named',
+    },
+    plugins: [
+      nodeResolve(),
+    ],
+  };
+}
 
-  function fromESM(toFormat) {
-    return {
-      input: outputFile('esm'),
-      output: {
-        file: outputFile(toFormat),
-        format: 'esm',
-        sourcemap: false,
-      },
-      // The UMD bundle expects `this` to refer to the global object. By default
-      // Rollup replaces `this` with `undefined`, but this default behavior can
-      // be overridden with the `context` option.
-      context: 'this',
-      plugins: [{
-        transform(source, id) {
-          const output = transformSync(source, {
-            inputSourceMap: JSON.parse(fs.readFileSync(id + '.map')),
-            sourceMaps: true,
-            plugins: [
-              [toFormat === 'umd' ? umdModulesTransform : cjsModulesTransform, {
-                loose: true,
-                allowTopLevelThis: true,
-              }],
-            ],
-          });
+// Build a separate CJS only `testing.js` bundle, that includes React
+// testing utilities like `MockedProvider` (testing utilities are kept out of
+// the main `apollo-client` bundle). This bundle can be accessed directly
+// like:
+//
+// import { MockedProvider } from '@apollo/client/testing';
+function prepareTesting() {
+  const bundleName = 'testing';
 
-          // There doesn't seem to be any way to get Rollup to emit a source map
-          // that goes all the way back to the source file (rather than just to
-          // the bundle.esm.js intermediate file), so we pass sourcemap:false in
-          // the output options above, and manually write the CJS and UMD source
-          // maps here.
-          fs.writeFileSync(
-            outputFile(toFormat) + '.map',
-            JSON.stringify(output.map),
-          );
+  // Create a type file for the new testing bundle that points to the existing
+  // `react/testing` type definitions.
+  fs.writeFileSync(
+    `${distDir}/${bundleName}.d.ts`,
+    "export * from './utilities/testing';"
+  );
 
-          return {
-            code: output.code,
-          };
-        }
-      }],
-    }
-  }
+  return {
+    input: `${distDir}/utilities/testing/index.js`,
+    external,
+    output: {
+      file: `${distDir}/${bundleName}.js`,
+      format: 'cjs',
+    },
+    plugins: [
+      nodeResolve({
+        extensions: ['.js', '.jsx'],
+      }),
+    ],
+  };
+}
 
+function rollup() {
   return [
-    fromSource('esm'),
-    fromESM('cjs'),
-    fromESM('umd'),
-    {
-      input: outputFile('cjs'),
-      output: {
-        file: outputFile('cjs.min'),
-        format: 'esm',
-      },
-      plugins: [
-        minify({
-          mangle: {
-            toplevel: true,
-          },
-          compress: {
-            global_defs: {
-              '@process.env.NODE_ENV': JSON.stringify('production'),
-            },
-          },
-        }),
-      ],
-    },
+    prepareESM(packageJson.module, distDir),
+    prepareCJS(packageJson.module, packageJson.main),
+    prepareCJSMinified(packageJson.main),
+    prepareUtilities(),
+    prepareTesting(),
   ];
 }
+
+export default rollup();
diff --git a/docs/gatsby-config.js b/docs/gatsby-config.js
--- a/docs/gatsby-config.js
+++ b/docs/gatsby-config.js
@@ -11,23 +11,27 @@ module.exports = {
         subtitle: 'Client (React)',
         description: 'A guide to using the Apollo GraphQL Client with React',
         githubRepo: 'apollographql/apollo-client',
-        defaultVersion: 2.6,
+        defaultVersion: 3.0,
+        /*
         versions: {
           '3.0 beta': 'release-3.0',
-          2.5: 'version-2.5',
-          2.4: 'version-2.4',
+          '2.6': 'version-2.6',
+          '2.5': 'version-2.5',
+          '2.4': 'version-2.4',
         },
+        */
         checkLinksOptions: {
           exceptions: [
-            '/api/apollo-client/',
-            '/v3.0-beta/api/core/',
-            '/v2.5/api/apollo-client/',
-            '/v2.4/api/apollo-client/',
+            '/api/core/',
+            '/v2.4/api/core/',
+            '/v2.5/api/core/',
+            '/v2.6/api/core/',
+            '/v3.0/api/core/'
           ],
         },
         typescriptApiBox: {
           data: require('./docs.json'),
-          filepathPrefix: 'packages/apollo-client/src/',
+          filepathPrefix: 'src/',
         },
         sidebarCategories: {
           null: ['index', 'why-apollo', 'get-started'],
@@ -59,15 +63,17 @@ module.exports = {
             'integrations/meteor',
             'integrations/webpack',
           ],
-          Networking: ['networking/network-layer', 'networking/authentication'],
-          'API Reference': [
-            'api/apollo-client',
+          Networking: [
+            'networking/network-layer',
+            'networking/authentication',
+          ],
+          'Apollo Client API': [
+            'api/core',
             'api/react-hooks',
-            'api/react-ssr',
             'api/react-testing',
+            'api/react-ssr',
             'api/react-components',
-            'api/react-hoc',
-            'api/react-common',
+            'api/react-hoc'
           ],
           Migrating: ['migrating/hooks-migration', 'migrating/boost-migration'],
         },
diff --git a/examples/bundling/no-tree-shaking/rollup-ac2/rollup.config.js b/examples/bundling/no-tree-shaking/rollup-ac2/rollup.config.js
new file mode 100644
--- /dev/null
+++ b/examples/bundling/no-tree-shaking/rollup-ac2/rollup.config.js
@@ -0,0 +1,69 @@
+import node from 'rollup-plugin-node-resolve';
+import babel from 'rollup-plugin-babel';
+import cjs from 'rollup-plugin-commonjs';
+import replace from 'rollup-plugin-replace';
+import { terser as minify } from 'rollup-plugin-terser';
+import gzipPlugin from 'rollup-plugin-gzip'
+
+function build({ outputPrefix, externals = [], gzip = false }) {
+  return {
+    input: './src/index.js',
+    output: {
+      file: `./public/js/${outputPrefix}.min.js`,
+      format: 'cjs'
+    },
+    external(id) {
+      return externals.indexOf(id) >= 0;
+    },
+    treeshake: false,
+    plugins: [
+      node(),
+      replace({
+        'process.env.NODE_ENV': JSON.stringify('production')
+      }),
+      babel({
+        exclude: 'node_modules/**',
+        presets: [require('@babel/preset-react')]
+      }),
+      cjs({
+        namedExports: {
+          'react': [
+            'useRef',
+            'useContext',
+            'useReducer',
+            'useEffect',
+            'useState'
+          ],
+          'react-dom': [
+            'render',
+          ],
+        }
+      }),
+      minify({
+        mangle: {
+          toplevel: true
+        },
+        compress: {
+          dead_code: false,
+          global_defs: {
+            '@process.env.NODE_ENV': JSON.stringify('production')
+          }
+        }
+      }),
+      gzip && gzipPlugin()
+    ]
+  };
+}
+
+export default [
+  build({
+    externals: [],
+    outputPrefix: 'app',
+    gzip: true
+  }),
+  build({
+    externals: ['react', 'react-dom'],
+    outputPrefix: 'app-no-react',
+    gzip: true
+  }),
+];
diff --git a/examples/bundling/no-tree-shaking/rollup-ac2/src/App.js b/examples/bundling/no-tree-shaking/rollup-ac2/src/App.js
new file mode 100644
--- /dev/null
+++ b/examples/bundling/no-tree-shaking/rollup-ac2/src/App.js
@@ -0,0 +1,35 @@
+import React from "react";
+import { useQuery } from "@apollo/react-hooks";
+import gql from "graphql-tag";
+
+const ALL_COUNTRIES = gql`
+  query AllCountries {
+    countries {
+      code
+      name
+      emoji
+    }
+  }
+`;
+
+export default function App() {
+  const {
+    loading,
+    data: { countries } = {}
+  } = useQuery(ALL_COUNTRIES);
+
+  return (
+    <main>
+      <h1>Countries</h1>
+      {loading ? (
+        <p>Loading…</p>
+      ) : (
+        <ul>
+          {countries.map(country => (
+            <li key={country.code}>{country.emoji} {country.name}</li>
+          ))}
+        </ul>
+      )}
+    </main>
+  );
+}
diff --git a/examples/bundling/no-tree-shaking/rollup-ac2/src/index.js b/examples/bundling/no-tree-shaking/rollup-ac2/src/index.js
new file mode 100644
--- /dev/null
+++ b/examples/bundling/no-tree-shaking/rollup-ac2/src/index.js
@@ -0,0 +1,19 @@
+import React from "react";
+import { render } from "react-dom";
+import { ApolloClient } from "apollo-client";
+import { InMemoryCache } from "apollo-cache-inmemory";
+import { ApolloProvider } from "@apollo/react-hooks";
+
+import App from "./App";
+
+const client = new ApolloClient({
+  uri: 'https://countries.trevorblades.com/',
+  cache: new InMemoryCache(),
+});
+
+render(
+  <ApolloProvider client={client}>
+    <App />
+  </ApolloProvider>,
+  document.getElementById("root")
+);
diff --git a/examples/bundling/no-tree-shaking/rollup-ac3-no-react/rollup.config.js b/examples/bundling/no-tree-shaking/rollup-ac3-no-react/rollup.config.js
new file mode 100644
--- /dev/null
+++ b/examples/bundling/no-tree-shaking/rollup-ac3-no-react/rollup.config.js
@@ -0,0 +1,68 @@
+import node from 'rollup-plugin-node-resolve';
+import babel from 'rollup-plugin-babel';
+import cjs from 'rollup-plugin-commonjs';
+import replace from 'rollup-plugin-replace';
+import { terser } from 'rollup-plugin-terser';
+import gzipPlugin from 'rollup-plugin-gzip'
+
+function build({ outputPrefix, externals = [], minify = false, gzip = false }) {
+  return {
+    input: './src/index.js',
+    output: {
+      file: `./public/js/${outputPrefix}${minify ? '.min' : ''}.js`,
+      format: 'cjs',
+      sourcemap: true
+    },
+    external(id) {
+      return externals.indexOf(id) >= 0;
+    },
+    treeshake: false,
+    plugins: [
+      node(),
+      replace({
+        'process.env.NODE_ENV': JSON.stringify('production')
+      }),
+      babel({
+        exclude: 'node_modules/**'
+      }),
+      cjs({
+        namedExports: {
+          'react': [
+            'useRef',
+            'useContext',
+            'useReducer',
+            'useEffect',
+            'useState'
+          ]
+        }
+      }),
+      minify && terser({
+        mangle: {
+          toplevel: true
+        },
+        compress: {
+          dead_code: false,
+          global_defs: {
+            '@process.env.NODE_ENV': JSON.stringify('production')
+          }
+        }
+      }),
+      gzip && gzipPlugin()
+    ]
+  };
+}
+
+export default [
+  build({
+    externals: [],
+    outputPrefix: 'app',
+    minify: false,
+    gzip: false
+  }),
+  build({
+    externals: [],
+    outputPrefix: 'app',
+    minify: true,
+    gzip: true
+  })
+];
diff --git a/examples/bundling/no-tree-shaking/rollup-ac3-no-react/src/index.js b/examples/bundling/no-tree-shaking/rollup-ac3-no-react/src/index.js
new file mode 100644
--- /dev/null
+++ b/examples/bundling/no-tree-shaking/rollup-ac3-no-react/src/index.js
@@ -0,0 +1,39 @@
+import { ApolloClient, InMemoryCache, gql } from "@apollo/client";
+
+const ALL_COUNTRIES = gql`
+  query AllCountries {
+    countries @client {
+      code
+      name
+      emoji
+    }
+  }
+`;
+
+const client = new ApolloClient({
+  cache: new InMemoryCache(),
+  resolvers: {
+    Query: {
+      countries() {
+        return [
+          {
+            code: "AD",
+            emoji: "🇦🇩",
+            name: "Andorra",
+            __typename: "Country"
+          },
+          {
+            code: "AE",
+            emoji: "🇦🇪",
+            name: "United Arab Emirates",
+            __typename: "Country"
+          }
+        ];
+      }
+    }
+  }
+});
+
+client.query({ query: ALL_COUNTRIES }).then(response => {
+  console.log(response);
+});
diff --git a/examples/bundling/no-tree-shaking/rollup-ac3/rollup.config.js b/examples/bundling/no-tree-shaking/rollup-ac3/rollup.config.js
new file mode 100644
--- /dev/null
+++ b/examples/bundling/no-tree-shaking/rollup-ac3/rollup.config.js
@@ -0,0 +1,69 @@
+import node from 'rollup-plugin-node-resolve';
+import babel from 'rollup-plugin-babel';
+import cjs from 'rollup-plugin-commonjs';
+import replace from 'rollup-plugin-replace';
+import { terser as minify } from 'rollup-plugin-terser';
+import gzipPlugin from 'rollup-plugin-gzip'
+
+function build({ outputPrefix, externals = [], gzip = false }) {
+  return {
+    input: './src/index.js',
+    output: {
+      file: `./public/js/${outputPrefix}.min.js`,
+      format: 'cjs'
+    },
+    external(id) {
+      return externals.indexOf(id) >= 0;
+    },
+    treeshake: false,
+    plugins: [
+      node(),
+      replace({
+        'process.env.NODE_ENV': JSON.stringify('production')
+      }),
+      babel({
+        exclude: 'node_modules/**',
+        presets: [require('@babel/preset-react')]
+      }),
+      cjs({
+        namedExports: {
+          'react': [
+            'useRef',
+            'useContext',
+            'useReducer',
+            'useEffect',
+            'useState'
+          ],
+          'react-dom': [
+            'render',
+          ],
+        }
+      }),
+      minify({
+        mangle: {
+          toplevel: true
+        },
+        compress: {
+          dead_code: false,
+          global_defs: {
+            '@process.env.NODE_ENV': JSON.stringify('production')
+          }
+        }
+      }),
+      gzip && gzipPlugin()
+    ]
+  };
+}
+
+export default [
+  build({
+    externals: [],
+    outputPrefix: 'app',
+    gzip: true
+  }),
+  build({
+    externals: ['react', 'react-dom'],
+    outputPrefix: 'app-no-react',
+    gzip: true
+  }),
+];
diff --git a/examples/bundling/no-tree-shaking/rollup-ac3/src/App.js b/examples/bundling/no-tree-shaking/rollup-ac3/src/App.js
new file mode 100644
--- /dev/null
+++ b/examples/bundling/no-tree-shaking/rollup-ac3/src/App.js
@@ -0,0 +1,34 @@
+import React from "react";
+import { gql, useQuery } from "@apollo/client";
+
+const ALL_COUNTRIES = gql`
+  query AllCountries {
+    countries {
+      code
+      name
+      emoji
+    }
+  }
+`;
+
+export default function App() {
+  const {
+    loading,
+    data: { countries } = {}
+  } = useQuery(ALL_COUNTRIES);
+
+  return (
+    <main>
+      <h1>Countries</h1>
+      {loading ? (
+        <p>Loading…</p>
+      ) : (
+        <ul>
+          {countries.map(country => (
+            <li key={country.code}>{country.emoji} {country.name}</li>
+          ))}
+        </ul>
+      )}
+    </main>
+  );
+}
diff --git a/examples/bundling/no-tree-shaking/rollup-ac3/src/index.js b/examples/bundling/no-tree-shaking/rollup-ac3/src/index.js
new file mode 100644
--- /dev/null
+++ b/examples/bundling/no-tree-shaking/rollup-ac3/src/index.js
@@ -0,0 +1,17 @@
+import React from "react";
+import { render } from "react-dom";
+import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
+
+import App from "./App";
+
+const client = new ApolloClient({
+  uri: 'https://countries.trevorblades.com/',
+  cache: new InMemoryCache(),
+});
+
+render(
+  <ApolloProvider client={client}>
+    <App />
+  </ApolloProvider>,
+  document.getElementById("root")
+);
diff --git a/examples/bundling/tree-shaking/rollup-ac2/rollup.config.js b/examples/bundling/tree-shaking/rollup-ac2/rollup.config.js
new file mode 100644
--- /dev/null
+++ b/examples/bundling/tree-shaking/rollup-ac2/rollup.config.js
@@ -0,0 +1,70 @@
+import node from 'rollup-plugin-node-resolve';
+import babel from 'rollup-plugin-babel';
+import cjs from 'rollup-plugin-commonjs';
+import replace from 'rollup-plugin-replace';
+import { terser as minify } from 'rollup-plugin-terser';
+import gzipPlugin from 'rollup-plugin-gzip'
+
+function build({ outputPrefix, externals = [], gzip = false }) {
+  return {
+    input: './src/index.js',
+    output: {
+      file: `./public/js/${outputPrefix}.min.js`,
+      format: 'cjs',
+      sourcemap: true
+    },
+    external(id) {
+      return externals.indexOf(id) >= 0;
+    },
+    treeshake: true,
+    plugins: [
+      node(),
+      replace({
+        'process.env.NODE_ENV': JSON.stringify('production')
+      }),
+      babel({
+        exclude: 'node_modules/**',
+        presets: [require('@babel/preset-react')]
+      }),
+      cjs({
+        namedExports: {
+          'react': [
+            'useRef',
+            'useContext',
+            'useReducer',
+            'useEffect',
+            'useState'
+          ],
+          'react-dom': [
+            'render',
+          ],
+        }
+      }),
+      minify({
+        mangle: {
+          toplevel: true
+        },
+        compress: {
+          dead_code: true,
+          global_defs: {
+            '@process.env.NODE_ENV': JSON.stringify('production')
+          }
+        }
+      }),
+      gzip && gzipPlugin()
+    ]
+  };
+}
+
+export default [
+  build({
+    externals: [],
+    outputPrefix: 'app',
+    gzip: true
+  }),
+  build({
+    externals: ['react', 'react-dom'],
+    outputPrefix: 'app-no-react',
+    gzip: true
+  }),
+];
diff --git a/examples/bundling/tree-shaking/rollup-ac2/src/App.js b/examples/bundling/tree-shaking/rollup-ac2/src/App.js
new file mode 100644
--- /dev/null
+++ b/examples/bundling/tree-shaking/rollup-ac2/src/App.js
@@ -0,0 +1,35 @@
+import React from "react";
+import { useQuery } from "@apollo/react-hooks";
+import gql from "graphql-tag";
+
+const ALL_COUNTRIES = gql`
+  query AllCountries {
+    countries {
+      code
+      name
+      emoji
+    }
+  }
+`;
+
+export default function App() {
+  const {
+    loading,
+    data: { countries } = {}
+  } = useQuery(ALL_COUNTRIES);
+
+  return (
+    <main>
+      <h1>Countries</h1>
+      {loading ? (
+        <p>Loading…</p>
+      ) : (
+        <ul>
+          {countries.map(country => (
+            <li key={country.code}>{country.emoji} {country.name}</li>
+          ))}
+        </ul>
+      )}
+    </main>
+  );
+}
diff --git a/examples/bundling/tree-shaking/rollup-ac2/src/index.js b/examples/bundling/tree-shaking/rollup-ac2/src/index.js
new file mode 100644
--- /dev/null
+++ b/examples/bundling/tree-shaking/rollup-ac2/src/index.js
@@ -0,0 +1,19 @@
+import React from "react";
+import { render } from "react-dom";
+import { ApolloClient } from "apollo-client";
+import { InMemoryCache } from "apollo-cache-inmemory";
+import { ApolloProvider } from "@apollo/react-hooks";
+
+import App from "./App";
+
+const client = new ApolloClient({
+  uri: 'https://countries.trevorblades.com/',
+  cache: new InMemoryCache(),
+});
+
+render(
+  <ApolloProvider client={client}>
+    <App />
+  </ApolloProvider>,
+  document.getElementById("root")
+);
diff --git a/examples/bundling/tree-shaking/rollup-ac3-no-react/rollup.config.js b/examples/bundling/tree-shaking/rollup-ac3-no-react/rollup.config.js
new file mode 100644
--- /dev/null
+++ b/examples/bundling/tree-shaking/rollup-ac3-no-react/rollup.config.js
@@ -0,0 +1,68 @@
+import node from 'rollup-plugin-node-resolve';
+import babel from 'rollup-plugin-babel';
+import cjs from 'rollup-plugin-commonjs';
+import replace from 'rollup-plugin-replace';
+import { terser } from 'rollup-plugin-terser';
+import gzipPlugin from 'rollup-plugin-gzip'
+
+function build({ outputPrefix, externals = [], minify = false, gzip = false }) {
+  return {
+    input: './src/index.js',
+    output: {
+      file: `./public/js/${outputPrefix}${minify ? '.min' : ''}.js`,
+      format: 'cjs',
+      sourcemap: true
+    },
+    external(id) {
+      return externals.indexOf(id) >= 0;
+    },
+    treeshake: true,
+    plugins: [
+      node(),
+      replace({
+        'process.env.NODE_ENV': JSON.stringify('production')
+      }),
+      babel({
+        exclude: 'node_modules/**'
+      }),
+      cjs({
+        namedExports: {
+          'react': [
+            'useRef',
+            'useContext',
+            'useReducer',
+            'useEffect',
+            'useState'
+          ]
+        }
+      }),
+      minify && terser({
+        mangle: {
+          toplevel: true
+        },
+        compress: {
+          dead_code: true,
+          global_defs: {
+            '@process.env.NODE_ENV': JSON.stringify('production')
+          }
+        }
+      }),
+      gzip && gzipPlugin()
+    ]
+  };
+}
+
+export default [
+  build({
+    externals: [],
+    outputPrefix: 'app',
+    minify: false,
+    gzip: false
+  }),
+  build({
+    externals: [],
+    outputPrefix: 'app',
+    minify: true,
+    gzip: true
+  })
+];
diff --git a/examples/bundling/tree-shaking/rollup-ac3-no-react/src/index.js b/examples/bundling/tree-shaking/rollup-ac3-no-react/src/index.js
new file mode 100644
--- /dev/null
+++ b/examples/bundling/tree-shaking/rollup-ac3-no-react/src/index.js
@@ -0,0 +1,39 @@
+import { ApolloClient, InMemoryCache, gql } from "@apollo/client";
+
+const ALL_COUNTRIES = gql`
+  query AllCountries {
+    countries @client {
+      code
+      name
+      emoji
+    }
+  }
+`;
+
+const client = new ApolloClient({
+  cache: new InMemoryCache(),
+  resolvers: {
+    Query: {
+      countries() {
+        return [
+          {
+            code: "AD",
+            emoji: "🇦🇩",
+            name: "Andorra",
+            __typename: "Country"
+          },
+          {
+            code: "AE",
+            emoji: "🇦🇪",
+            name: "United Arab Emirates",
+            __typename: "Country"
+          }
+        ];
+      }
+    }
+  }
+});
+
+client.query({ query: ALL_COUNTRIES }).then(response => {
+  console.log(response);
+});
diff --git a/examples/bundling/tree-shaking/rollup-ac3/rollup.config.js b/examples/bundling/tree-shaking/rollup-ac3/rollup.config.js
new file mode 100644
--- /dev/null
+++ b/examples/bundling/tree-shaking/rollup-ac3/rollup.config.js
@@ -0,0 +1,70 @@
+import node from 'rollup-plugin-node-resolve';
+import babel from 'rollup-plugin-babel';
+import cjs from 'rollup-plugin-commonjs';
+import replace from 'rollup-plugin-replace';
+import { terser as minify } from 'rollup-plugin-terser';
+import gzipPlugin from 'rollup-plugin-gzip'
+
+function build({ outputPrefix, externals = [], gzip = false }) {
+  return {
+    input: './src/index.js',
+    output: {
+      file: `./public/js/${outputPrefix}.min.js`,
+      format: 'cjs',
+      sourcemap: true
+    },
+    external(id) {
+      return externals.indexOf(id) >= 0;
+    },
+    treeshake: true,
+    plugins: [
+      node(),
+      replace({
+        'process.env.NODE_ENV': JSON.stringify('production')
+      }),
+      babel({
+        exclude: 'node_modules/**',
+        presets: [require('@babel/preset-react')]
+      }),
+      cjs({
+        namedExports: {
+          'react': [
+            'useRef',
+            'useContext',
+            'useReducer',
+            'useEffect',
+            'useState'
+          ],
+          'react-dom': [
+            'render',
+          ],
+        }
+      }),
+      minify({
+        mangle: {
+          toplevel: true
+        },
+        compress: {
+          dead_code: true,
+          global_defs: {
+            '@process.env.NODE_ENV': JSON.stringify('production')
+          }
+        }
+      }),
+      gzip && gzipPlugin()
+    ]
+  };
+}
+
+export default [
+  build({
+    externals: [],
+    outputPrefix: 'app',
+    gzip: true
+  }),
+  build({
+    externals: ['react', 'react-dom'],
+    outputPrefix: 'app-no-react',
+    gzip: true
+  })
+];
diff --git a/examples/bundling/tree-shaking/rollup-ac3/src/App.js b/examples/bundling/tree-shaking/rollup-ac3/src/App.js
new file mode 100644
--- /dev/null
+++ b/examples/bundling/tree-shaking/rollup-ac3/src/App.js
@@ -0,0 +1,34 @@
+import React from "react";
+import { gql, useQuery } from "@apollo/client";
+
+const ALL_COUNTRIES = gql`
+  query AllCountries {
+    countries {
+      code
+      name
+      emoji
+    }
+  }
+`;
+
+export default function App() {
+  const {
+    loading,
+    data: { countries } = {}
+  } = useQuery(ALL_COUNTRIES);
+
+  return (
+    <main>
+      <h1>Countries</h1>
+      {loading ? (
+        <p>Loading…</p>
+      ) : (
+        <ul>
+          {countries.map(country => (
+            <li key={country.code}>{country.emoji} {country.name}</li>
+          ))}
+        </ul>
+      )}
+    </main>
+  );
+}
diff --git a/examples/bundling/tree-shaking/rollup-ac3/src/index.js b/examples/bundling/tree-shaking/rollup-ac3/src/index.js
new file mode 100644
--- /dev/null
+++ b/examples/bundling/tree-shaking/rollup-ac3/src/index.js
@@ -0,0 +1,17 @@
+import React from "react";
+import { render } from "react-dom";
+import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
+
+import App from "./App";
+
+const client = new ApolloClient({
+  uri: 'https://countries.trevorblades.com/',
+  cache: new InMemoryCache(),
+});
+
+render(
+  <ApolloProvider client={client}>
+    <App />
+  </ApolloProvider>,
+  document.getElementById("root")
+);
diff --git a/examples/bundling/tree-shaking/webpack-ac3/src/App.js b/examples/bundling/tree-shaking/webpack-ac3/src/App.js
new file mode 100644
--- /dev/null
+++ b/examples/bundling/tree-shaking/webpack-ac3/src/App.js
@@ -0,0 +1,34 @@
+import React from "react";
+import { gql, useQuery } from "@apollo/client";
+
+const ALL_COUNTRIES = gql`
+  query AllCountries {
+    countries {
+      code
+      name
+      emoji
+    }
+  }
+`;
+
+export default function App() {
+  const {
+    loading,
+    data: { countries } = {}
+  } = useQuery(ALL_COUNTRIES);
+
+  return (
+    <main>
+      <h1>Countries</h1>
+      {loading ? (
+        <p>Loading…</p>
+      ) : (
+        <ul>
+          {countries.map(country => (
+            <li key={country.code}>{country.emoji} {country.name}</li>
+          ))}
+        </ul>
+      )}
+    </main>
+  );
+}
diff --git a/examples/bundling/tree-shaking/webpack-ac3/src/index.js b/examples/bundling/tree-shaking/webpack-ac3/src/index.js
new file mode 100644
--- /dev/null
+++ b/examples/bundling/tree-shaking/webpack-ac3/src/index.js
@@ -0,0 +1,17 @@
+import React from "react";
+import { render } from "react-dom";
+import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
+
+import App from "./App";
+
+const client = new ApolloClient({
+  uri: 'https://countries.trevorblades.com/',
+  cache: new InMemoryCache(),
+});
+
+render(
+  <ApolloProvider client={client}>
+    <App />
+  </ApolloProvider>,
+  document.getElementById("root")
+);
diff --git a/jest.config.js b/jest.config.js
deleted file mode 100644
--- a/jest.config.js
+++ /dev/null
@@ -1,4 +0,0 @@
-module.exports = {
-  rootDir: '.',
-  projects: ['<rootDir>/packages/*'],
-};
diff --git a/packages/apollo-boost/jest.config.js b/packages/apollo-boost/jest.config.js
deleted file mode 100644
--- a/packages/apollo-boost/jest.config.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
-  ...require('../../config/jest.config.settings'),
-};
diff --git a/packages/apollo-boost/rollup.config.js b/packages/apollo-boost/rollup.config.js
deleted file mode 100644
--- a/packages/apollo-boost/rollup.config.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { rollup } from '../../config/rollup.config';
-
-export default rollup({
-  name: 'apollo-boost',
-  extraGlobals: {
-    'apollo-cache-inmemory': 'apolloCacheInMemory',
-    'apollo-link': 'apolloLink.core',
-    'apollo-link-http': 'apolloLinkHttp',
-    'apollo-link-error': 'apolloLinkError',
-    'graphql-tag': 'graphqlTag',
-  },
-});
diff --git a/packages/apollo-boost/src/index.ts b/packages/apollo-boost/src/index.ts
deleted file mode 100644
--- a/packages/apollo-boost/src/index.ts
+++ /dev/null
@@ -1,202 +0,0 @@
-/* necessary for backward compat */
-export * from 'apollo-client';
-export * from 'apollo-link';
-export * from 'apollo-cache-inmemory';
-
-import { Operation, ApolloLink, Observable } from 'apollo-link';
-import { HttpLink, UriFunction } from 'apollo-link-http';
-import { onError, ErrorLink } from 'apollo-link-error';
-import { ApolloCache } from 'apollo-cache';
-import { InMemoryCache, CacheResolverMap } from 'apollo-cache-inmemory';
-import gql from 'graphql-tag';
-import ApolloClient, {
-  Resolvers,
-  LocalStateFragmentMatcher,
-} from 'apollo-client';
-import { DocumentNode } from 'graphql';
-import { invariant } from 'ts-invariant';
-
-export { gql, HttpLink };
-
-type ClientStateConfig = {
-  cache?: ApolloCache<any>;
-  defaults?: Record<string, any>;
-  resolvers?: Resolvers | Resolvers[];
-  typeDefs?: string | string[] | DocumentNode | DocumentNode[];
-  fragmentMatcher?: LocalStateFragmentMatcher;
-};
-
-export interface PresetConfig {
-  request?: (operation: Operation) => Promise<void> | void;
-  uri?: string | UriFunction;
-  credentials?: string;
-  headers?: any;
-  fetch?: WindowOrWorkerGlobalScope['fetch'];
-  fetchOptions?: HttpLink.Options;
-  clientState?: ClientStateConfig;
-  onError?: ErrorLink.ErrorHandler;
-  cacheRedirects?: CacheResolverMap;
-  cache?: ApolloCache<any>;
-  name?: string;
-  version?: string;
-  resolvers?: Resolvers | Resolvers[];
-  typeDefs?: string | string[] | DocumentNode | DocumentNode[];
-  fragmentMatcher?: LocalStateFragmentMatcher;
-  assumeImmutableResults?: boolean;
-}
-
-// Yes, these are the exact same as the `PresetConfig` interface. We're
-// defining these again so they can be used to verify that valid config
-// options are being used in the `DefaultClient` constructor, for clients
-// that aren't using Typescript. This duplication is unfortunate, and at
-// some point can likely be adjusted so these items are inferred from
-// the `PresetConfig` interface using a Typescript transform at compilation
-// time. Unfortunately, TS transforms with rollup don't appear to be quite
-// working properly, so this will have to be re-visited at some point.
-// For now, when updating the properties of the `PresetConfig` interface,
-// please also update this constant.
-const PRESET_CONFIG_KEYS = [
-  'request',
-  'uri',
-  'credentials',
-  'headers',
-  'fetch',
-  'fetchOptions',
-  'clientState',
-  'onError',
-  'cacheRedirects',
-  'cache',
-  'name',
-  'version',
-  'resolvers',
-  'typeDefs',
-  'fragmentMatcher',
-];
-
-export default class DefaultClient<TCache> extends ApolloClient<TCache> {
-  constructor(config: PresetConfig = {}) {
-    if (config) {
-      const diff = Object.keys(config).filter(
-        key => PRESET_CONFIG_KEYS.indexOf(key) === -1,
-      );
-
-      if (diff.length > 0) {
-        invariant.warn(
-          'ApolloBoost was initialized with unsupported options: ' +
-            `${diff.join(' ')}`,
-        );
-      }
-    }
-
-    const {
-      request,
-      uri,
-      credentials,
-      headers,
-      fetch,
-      fetchOptions,
-      clientState,
-      cacheRedirects,
-      onError: errorCallback,
-      name,
-      version,
-      resolvers,
-      typeDefs,
-      fragmentMatcher,
-    } = config;
-
-    let { cache } = config;
-
-    invariant(
-      !cache || !cacheRedirects,
-      'Incompatible cache configuration. When not providing `cache`, ' +
-        'configure the provided instance with `cacheRedirects` instead.',
-    );
-
-    if (!cache) {
-      cache = cacheRedirects
-        ? new InMemoryCache({ cacheRedirects })
-        : new InMemoryCache();
-    }
-
-    const errorLink = errorCallback
-      ? onError(errorCallback)
-      : onError(({ graphQLErrors, networkError }) => {
-          if (graphQLErrors) {
-            graphQLErrors.forEach(({ message, locations, path }) =>
-              // tslint:disable-next-line
-              invariant.warn(
-                `[GraphQL error]: Message: ${message}, Location: ` +
-                  `${locations}, Path: ${path}`,
-              ),
-            );
-          }
-          if (networkError) {
-            // tslint:disable-next-line
-            invariant.warn(`[Network error]: ${networkError}`);
-          }
-        });
-
-    const requestHandler = request
-      ? new ApolloLink(
-          (operation, forward) =>
-            new Observable(observer => {
-              let handle: any;
-              Promise.resolve(operation)
-                .then(oper => request(oper))
-                .then(() => {
-                  handle = forward(operation).subscribe({
-                    next: observer.next.bind(observer),
-                    error: observer.error.bind(observer),
-                    complete: observer.complete.bind(observer),
-                  });
-                })
-                .catch(observer.error.bind(observer));
-
-              return () => {
-                if (handle) {
-                  handle.unsubscribe();
-                }
-              };
-            }),
-        )
-      : false;
-
-    const httpLink = new HttpLink({
-      uri: uri || '/graphql',
-      fetch,
-      fetchOptions: fetchOptions || {},
-      credentials: credentials || 'same-origin',
-      headers: headers || {},
-    });
-
-    const link = ApolloLink.from([errorLink, requestHandler, httpLink].filter(
-      x => !!x,
-    ) as ApolloLink[]);
-
-    let activeResolvers = resolvers;
-    let activeTypeDefs = typeDefs;
-    let activeFragmentMatcher = fragmentMatcher;
-    if (clientState) {
-      if (clientState.defaults) {
-        cache.writeData({
-          data: clientState.defaults,
-        });
-      }
-      activeResolvers = clientState.resolvers;
-      activeTypeDefs = clientState.typeDefs;
-      activeFragmentMatcher = clientState.fragmentMatcher;
-    }
-
-    // super hacky, we will fix the types eventually
-    super({
-      cache,
-      link,
-      name,
-      version,
-      resolvers: activeResolvers,
-      typeDefs: activeTypeDefs,
-      fragmentMatcher: activeFragmentMatcher,
-    } as any);
-  }
-}
diff --git a/packages/apollo-cache-inmemory/jest.config.js b/packages/apollo-cache-inmemory/jest.config.js
deleted file mode 100644
--- a/packages/apollo-cache-inmemory/jest.config.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
-  ...require('../../config/jest.config.settings'),
-};
diff --git a/packages/apollo-cache-inmemory/rollup.config.js b/packages/apollo-cache-inmemory/rollup.config.js
deleted file mode 100644
--- a/packages/apollo-cache-inmemory/rollup.config.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import { rollup } from '../../config/rollup.config';
-
-export default rollup({
-  name: 'apollo-cache-inmemory',
-  extraGlobals: {
-    'graphql/language/printer': 'print',
-    optimism: 'optimism',
-    'graphql/language/visitor': 'visitor',
-  },
-});
diff --git a/packages/apollo-cache-inmemory/src/depTrackingCache.ts b/packages/apollo-cache-inmemory/src/depTrackingCache.ts
deleted file mode 100644
--- a/packages/apollo-cache-inmemory/src/depTrackingCache.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { NormalizedCache, NormalizedCacheObject, StoreObject } from './types';
-import { wrap, OptimisticWrapperFunction } from 'optimism';
-
-const hasOwn = Object.prototype.hasOwnProperty;
-
-export class DepTrackingCache implements NormalizedCache {
-  // Wrapper function produced by the optimism library, used to depend on
-  // dataId strings, for easy invalidation of specific IDs.
-  private depend: OptimisticWrapperFunction<[string], StoreObject | undefined>;
-
-  constructor(private data: NormalizedCacheObject = Object.create(null)) {
-    this.depend = wrap((dataId: string) => this.data[dataId], {
-      disposable: true,
-      makeCacheKey(dataId: string) {
-        return dataId;
-      },
-    });
-  }
-
-  public toObject(): NormalizedCacheObject {
-    return this.data;
-  }
-
-  public get(dataId: string): StoreObject {
-    this.depend(dataId);
-    return this.data[dataId]!;
-  }
-
-  public set(dataId: string, value?: StoreObject) {
-    const oldValue = this.data[dataId];
-    if (value !== oldValue) {
-      this.data[dataId] = value;
-      this.depend.dirty(dataId);
-    }
-  }
-
-  public delete(dataId: string): void {
-    if (hasOwn.call(this.data, dataId)) {
-      delete this.data[dataId];
-      this.depend.dirty(dataId);
-    }
-  }
-
-  public clear(): void {
-    this.replace(null);
-  }
-
-  public replace(newData: NormalizedCacheObject | null): void {
-    if (newData) {
-      Object.keys(newData).forEach(dataId => {
-        this.set(dataId, newData[dataId]);
-      });
-      Object.keys(this.data).forEach(dataId => {
-        if (!hasOwn.call(newData, dataId)) {
-          this.delete(dataId);
-        }
-      });
-    } else {
-      Object.keys(this.data).forEach(dataId => {
-        this.delete(dataId);
-      });
-    }
-  }
-}
-
-export function defaultNormalizedCacheFactory(
-  seed?: NormalizedCacheObject,
-): NormalizedCache {
-  return new DepTrackingCache(seed);
-}
diff --git a/packages/apollo-cache-inmemory/src/fragmentMatcher.ts b/packages/apollo-cache-inmemory/src/fragmentMatcher.ts
deleted file mode 100644
--- a/packages/apollo-cache-inmemory/src/fragmentMatcher.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import { isTest, IdValue } from 'apollo-utilities';
-import { invariant } from 'ts-invariant';
-
-import {
-  ReadStoreContext,
-  FragmentMatcherInterface,
-  PossibleTypesMap,
-  IntrospectionResultData,
-} from './types';
-
-let haveWarned = false;
-
-function shouldWarn() {
-  const answer = !haveWarned;
-  /* istanbul ignore if */
-  if (!isTest()) {
-    haveWarned = true;
-  }
-  return answer;
-}
-
-/**
- * This fragment matcher is very basic and unable to match union or interface type conditions
- */
-export class HeuristicFragmentMatcher implements FragmentMatcherInterface {
-  constructor() {
-    // do nothing
-  }
-
-  public ensureReady() {
-    return Promise.resolve();
-  }
-
-  public canBypassInit() {
-    return true; // we don't need to initialize this fragment matcher.
-  }
-
-  public match(
-    idValue: IdValue,
-    typeCondition: string,
-    context: ReadStoreContext,
-  ): boolean | 'heuristic' {
-    const obj = context.store.get(idValue.id);
-    const isRootQuery = idValue.id === 'ROOT_QUERY';
-
-    if (!obj) {
-      // https://github.com/apollographql/apollo-client/pull/3507
-      return isRootQuery;
-    }
-
-    const { __typename = isRootQuery && 'Query' } = obj;
-
-    if (!__typename) {
-      if (shouldWarn()) {
-        invariant.warn(`You're using fragments in your queries, but either don't have the addTypename:
-  true option set in Apollo Client, or you are trying to write a fragment to the store without the __typename.
-   Please turn on the addTypename option and include __typename when writing fragments so that Apollo Client
-   can accurately match fragments.`);
-        invariant.warn(
-          'Could not find __typename on Fragment ',
-          typeCondition,
-          obj,
-        );
-        invariant.warn(
-          `DEPRECATION WARNING: using fragments without __typename is unsupported behavior ` +
-            `and will be removed in future versions of Apollo client. You should fix this and set addTypename to true now.`,
-        );
-      }
-
-      return 'heuristic';
-    }
-
-    if (__typename === typeCondition) {
-      return true;
-    }
-
-    // At this point we don't know if this fragment should match or not. It's
-    // either:
-    //
-    // 1. (GOOD) A fragment on a matching interface or union.
-    // 2. (BAD) A fragment on a non-matching concrete type or interface or union.
-    //
-    // If it's 2, we don't want it to match. If it's 1, we want it to match. We
-    // can't tell the difference, so we warn the user, but still try to match
-    // it (for backwards compatibility reasons). This unfortunately means that
-    // using the `HeuristicFragmentMatcher` with unions and interfaces is
-    // very unreliable. This will be addressed in a future major version of
-    // Apollo Client, but for now the recommendation is to use the
-    // `IntrospectionFragmentMatcher` when working with unions/interfaces.
-
-    if (shouldWarn()) {
-      invariant.error(
-        'You are using the simple (heuristic) fragment matcher, but your ' +
-          'queries contain union or interface types. Apollo Client will not be ' +
-          'able to accurately map fragments. To make this error go away, use ' +
-          'the `IntrospectionFragmentMatcher` as described in the docs: ' +
-          'https://www.apollographql.com/docs/react/advanced/fragments.html#fragment-matcher',
-      );
-    }
-
-    return 'heuristic';
-  }
-}
-
-export class IntrospectionFragmentMatcher implements FragmentMatcherInterface {
-  private isReady: boolean;
-  private possibleTypesMap: PossibleTypesMap;
-
-  constructor(options?: {
-    introspectionQueryResultData?: IntrospectionResultData;
-  }) {
-    if (options && options.introspectionQueryResultData) {
-      this.possibleTypesMap = this.parseIntrospectionResult(
-        options.introspectionQueryResultData,
-      );
-      this.isReady = true;
-    } else {
-      this.isReady = false;
-    }
-
-    this.match = this.match.bind(this);
-  }
-
-  public match(
-    idValue: IdValue,
-    typeCondition: string,
-    context: ReadStoreContext,
-  ) {
-    invariant(
-      this.isReady,
-      'FragmentMatcher.match() was called before FragmentMatcher.init()',
-    );
-
-    const obj = context.store.get(idValue.id);
-    const isRootQuery = idValue.id === 'ROOT_QUERY';
-
-    if (!obj) {
-      // https://github.com/apollographql/apollo-client/pull/4620
-      return isRootQuery;
-    }
-
-    const { __typename = isRootQuery && 'Query' } = obj;
-
-    invariant(
-      __typename,
-      `Cannot match fragment because __typename property is missing: ${JSON.stringify(
-        obj,
-      )}`,
-    );
-
-    if (__typename === typeCondition) {
-      return true;
-    }
-
-    const implementingTypes = this.possibleTypesMap[typeCondition];
-    if (
-      __typename &&
-      implementingTypes &&
-      implementingTypes.indexOf(__typename) > -1
-    ) {
-      return true;
-    }
-
-    return false;
-  }
-
-  private parseIntrospectionResult(
-    introspectionResultData: IntrospectionResultData,
-  ): PossibleTypesMap {
-    const typeMap: PossibleTypesMap = {};
-    introspectionResultData.__schema.types.forEach(type => {
-      if (type.kind === 'UNION' || type.kind === 'INTERFACE') {
-        typeMap[type.name] = type.possibleTypes.map(
-          implementingType => implementingType.name,
-        );
-      }
-    });
-    return typeMap;
-  }
-}
diff --git a/packages/apollo-cache-inmemory/src/fragmentMatcherIntrospectionQuery.ts b/packages/apollo-cache-inmemory/src/fragmentMatcherIntrospectionQuery.ts
deleted file mode 100644
--- a/packages/apollo-cache-inmemory/src/fragmentMatcherIntrospectionQuery.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-const query: any = {
-  kind: 'Document',
-  definitions: [
-    {
-      kind: 'OperationDefinition',
-      operation: 'query',
-      name: null,
-      variableDefinitions: null,
-      directives: [],
-      selectionSet: {
-        kind: 'SelectionSet',
-        selections: [
-          {
-            kind: 'Field',
-            alias: null,
-            name: {
-              kind: 'Name',
-              value: '__schema',
-            },
-            arguments: [],
-            directives: [],
-            selectionSet: {
-              kind: 'SelectionSet',
-              selections: [
-                {
-                  kind: 'Field',
-                  alias: null,
-                  name: {
-                    kind: 'Name',
-                    value: 'types',
-                  },
-                  arguments: [],
-                  directives: [],
-                  selectionSet: {
-                    kind: 'SelectionSet',
-                    selections: [
-                      {
-                        kind: 'Field',
-                        alias: null,
-                        name: {
-                          kind: 'Name',
-                          value: 'kind',
-                        },
-                        arguments: [],
-                        directives: [],
-                        selectionSet: null,
-                      },
-                      {
-                        kind: 'Field',
-                        alias: null,
-                        name: {
-                          kind: 'Name',
-                          value: 'name',
-                        },
-                        arguments: [],
-                        directives: [],
-                        selectionSet: null,
-                      },
-                      {
-                        kind: 'Field',
-                        alias: null,
-                        name: {
-                          kind: 'Name',
-                          value: 'possibleTypes',
-                        },
-                        arguments: [],
-                        directives: [],
-                        selectionSet: {
-                          kind: 'SelectionSet',
-                          selections: [
-                            {
-                              kind: 'Field',
-                              alias: null,
-                              name: {
-                                kind: 'Name',
-                                value: 'name',
-                              },
-                              arguments: [],
-                              directives: [],
-                              selectionSet: null,
-                            },
-                          ],
-                        },
-                      },
-                    ],
-                  },
-                },
-              ],
-            },
-          },
-        ],
-      },
-    },
-  ],
-};
-
-export default query;
diff --git a/packages/apollo-cache-inmemory/src/inMemoryCache.ts b/packages/apollo-cache-inmemory/src/inMemoryCache.ts
deleted file mode 100644
--- a/packages/apollo-cache-inmemory/src/inMemoryCache.ts
+++ /dev/null
@@ -1,355 +0,0 @@
-// Make builtins like Map and Set safe to use with non-extensible objects.
-import './fixPolyfills';
-
-import { DocumentNode } from 'graphql';
-
-import { Cache, ApolloCache, Transaction } from 'apollo-cache';
-
-import { addTypenameToDocument, canUseWeakMap } from 'apollo-utilities';
-
-import { wrap } from 'optimism';
-
-import { invariant, InvariantError } from 'ts-invariant';
-
-import { HeuristicFragmentMatcher } from './fragmentMatcher';
-import {
-  ApolloReducerConfig,
-  NormalizedCache,
-  NormalizedCacheObject,
-} from './types';
-
-import { StoreReader } from './readFromStore';
-import { StoreWriter } from './writeToStore';
-import { DepTrackingCache } from './depTrackingCache';
-import { KeyTrie } from 'optimism';
-import { ObjectCache } from './objectCache';
-
-export interface InMemoryCacheConfig extends ApolloReducerConfig {
-  resultCaching?: boolean;
-  freezeResults?: boolean;
-}
-
-const defaultConfig: InMemoryCacheConfig = {
-  fragmentMatcher: new HeuristicFragmentMatcher(),
-  dataIdFromObject: defaultDataIdFromObject,
-  addTypename: true,
-  resultCaching: true,
-  freezeResults: false,
-};
-
-export function defaultDataIdFromObject(result: any): string | null {
-  if (result.__typename) {
-    if (result.id !== undefined) {
-      return `${result.__typename}:${result.id}`;
-    }
-    if (result._id !== undefined) {
-      return `${result.__typename}:${result._id}`;
-    }
-  }
-  return null;
-}
-
-const hasOwn = Object.prototype.hasOwnProperty;
-
-export class OptimisticCacheLayer extends ObjectCache {
-  constructor(
-    public readonly optimisticId: string,
-    // OptimisticCacheLayer objects always wrap some other parent cache, so
-    // this.parent should never be null.
-    public readonly parent: NormalizedCache,
-    public readonly transaction: Transaction<NormalizedCacheObject>,
-  ) {
-    super(Object.create(null));
-  }
-
-  public toObject(): NormalizedCacheObject {
-    return {
-      ...this.parent.toObject(),
-      ...this.data,
-    };
-  }
-
-  // All the other accessor methods of ObjectCache work without knowing about
-  // this.parent, but the get method needs to be overridden to implement the
-  // fallback this.parent.get(dataId) behavior.
-  public get(dataId: string) {
-    return hasOwn.call(this.data, dataId)
-      ? this.data[dataId]
-      : this.parent.get(dataId);
-  }
-}
-
-export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
-  private data: NormalizedCache;
-  private optimisticData: NormalizedCache;
-
-  protected config: InMemoryCacheConfig;
-  private watches = new Set<Cache.WatchOptions>();
-  private addTypename: boolean;
-  private typenameDocumentCache = new Map<DocumentNode, DocumentNode>();
-  private storeReader: StoreReader;
-  private storeWriter: StoreWriter;
-  private cacheKeyRoot = new KeyTrie<object>(canUseWeakMap);
-
-  // Set this while in a transaction to prevent broadcasts...
-  // don't forget to turn it back on!
-  private silenceBroadcast: boolean = false;
-
-  constructor(config: InMemoryCacheConfig = {}) {
-    super();
-    this.config = { ...defaultConfig, ...config };
-
-    // backwards compat
-    if ((this.config as any).customResolvers) {
-      invariant.warn(
-        'customResolvers have been renamed to cacheRedirects. Please update your config as we will be deprecating customResolvers in the next major version.',
-      );
-      this.config.cacheRedirects = (this.config as any).customResolvers;
-    }
-
-    if ((this.config as any).cacheResolvers) {
-      invariant.warn(
-        'cacheResolvers have been renamed to cacheRedirects. Please update your config as we will be deprecating cacheResolvers in the next major version.',
-      );
-      this.config.cacheRedirects = (this.config as any).cacheResolvers;
-    }
-
-    this.addTypename = !!this.config.addTypename;
-
-    // Passing { resultCaching: false } in the InMemoryCache constructor options
-    // will completely disable dependency tracking, which will improve memory
-    // usage but worsen the performance of repeated reads.
-    this.data = this.config.resultCaching
-      ? new DepTrackingCache()
-      : new ObjectCache();
-
-    // When no optimistic writes are currently active, cache.optimisticData ===
-    // cache.data, so there are no additional layers on top of the actual data.
-    // When an optimistic update happens, this.optimisticData will become a
-    // linked list of OptimisticCacheLayer objects that terminates with the
-    // original this.data cache object.
-    this.optimisticData = this.data;
-
-    this.storeWriter = new StoreWriter();
-    this.storeReader = new StoreReader({
-      cacheKeyRoot: this.cacheKeyRoot,
-      freezeResults: config.freezeResults,
-    });
-
-    const cache = this;
-    const { maybeBroadcastWatch } = cache;
-    this.maybeBroadcastWatch = wrap((c: Cache.WatchOptions) => {
-      return maybeBroadcastWatch.call(this, c);
-    }, {
-      makeCacheKey(c: Cache.WatchOptions) {
-        if (c.optimistic) {
-          // If we're reading optimistic data, it doesn't matter if this.data
-          // is a DepTrackingCache, since it will be ignored.
-          return;
-        }
-
-        if (c.previousResult) {
-          // If a previousResult was provided, assume the caller would prefer
-          // to compare the previous data to the new data to determine whether
-          // to broadcast, so we should disable caching by returning here, to
-          // give maybeBroadcastWatch a chance to do that comparison.
-          return;
-        }
-
-        if (cache.data instanceof DepTrackingCache) {
-          // Return a cache key (thus enabling caching) only if we're currently
-          // using a data store that can track cache dependencies.
-          return cache.cacheKeyRoot.lookup(
-            c.query,
-            JSON.stringify(c.variables),
-          );
-        }
-      }
-    });
-  }
-
-  public restore(data: NormalizedCacheObject): this {
-    if (data) this.data.replace(data);
-    return this;
-  }
-
-  public extract(optimistic: boolean = false): NormalizedCacheObject {
-    return (optimistic ? this.optimisticData : this.data).toObject();
-  }
-
-  public read<T>(options: Cache.ReadOptions): T | null {
-    if (typeof options.rootId === 'string' &&
-        typeof this.data.get(options.rootId) === 'undefined') {
-      return null;
-    }
-
-    const { fragmentMatcher } = this.config;
-    const fragmentMatcherFunction = fragmentMatcher && fragmentMatcher.match;
-
-    return this.storeReader.readQueryFromStore({
-      store: options.optimistic ? this.optimisticData : this.data,
-      query: this.transformDocument(options.query),
-      variables: options.variables,
-      rootId: options.rootId,
-      fragmentMatcherFunction,
-      previousResult: options.previousResult,
-      config: this.config,
-    }) || null;
-  }
-
-  public write(write: Cache.WriteOptions): void {
-    const { fragmentMatcher } = this.config;
-    const fragmentMatcherFunction = fragmentMatcher && fragmentMatcher.match;
-
-    this.storeWriter.writeResultToStore({
-      dataId: write.dataId,
-      result: write.result,
-      variables: write.variables,
-      document: this.transformDocument(write.query),
-      store: this.data,
-      dataIdFromObject: this.config.dataIdFromObject,
-      fragmentMatcherFunction,
-    });
-
-    this.broadcastWatches();
-  }
-
-  public diff<T>(query: Cache.DiffOptions): Cache.DiffResult<T> {
-    const { fragmentMatcher } = this.config;
-    const fragmentMatcherFunction = fragmentMatcher && fragmentMatcher.match;
-
-    return this.storeReader.diffQueryAgainstStore({
-      store: query.optimistic ? this.optimisticData : this.data,
-      query: this.transformDocument(query.query),
-      variables: query.variables,
-      returnPartialData: query.returnPartialData,
-      previousResult: query.previousResult,
-      fragmentMatcherFunction,
-      config: this.config,
-    });
-  }
-
-  public watch(watch: Cache.WatchOptions): () => void {
-    this.watches.add(watch);
-
-    return () => {
-      this.watches.delete(watch);
-    };
-  }
-
-  public evict(query: Cache.EvictOptions): Cache.EvictionResult {
-    throw new InvariantError(`eviction is not implemented on InMemory Cache`);
-  }
-
-  public reset(): Promise<void> {
-    this.data.clear();
-    this.broadcastWatches();
-
-    return Promise.resolve();
-  }
-
-  public removeOptimistic(idToRemove: string) {
-    const toReapply: OptimisticCacheLayer[] = [];
-    let removedCount = 0;
-    let layer = this.optimisticData;
-
-    while (layer instanceof OptimisticCacheLayer) {
-      if (layer.optimisticId === idToRemove) {
-        ++removedCount;
-      } else {
-        toReapply.push(layer);
-      }
-      layer = layer.parent;
-    }
-
-    if (removedCount > 0) {
-      // Reset this.optimisticData to the first non-OptimisticCacheLayer object,
-      // which is almost certainly this.data.
-      this.optimisticData = layer;
-
-      // Reapply the layers whose optimistic IDs do not match the removed ID.
-      while (toReapply.length > 0) {
-        const layer = toReapply.pop()!;
-        this.performTransaction(layer.transaction, layer.optimisticId);
-      }
-
-      this.broadcastWatches();
-    }
-  }
-
-  public performTransaction(
-    transaction: Transaction<NormalizedCacheObject>,
-    // This parameter is not part of the performTransaction signature inherited
-    // from the ApolloCache abstract class, but it's useful because it saves us
-    // from duplicating this implementation in recordOptimisticTransaction.
-    optimisticId?: string,
-  ) {
-    const { data, silenceBroadcast } = this;
-    this.silenceBroadcast = true;
-
-    if (typeof optimisticId === 'string') {
-      // Add a new optimistic layer and temporarily make this.data refer to
-      // that layer for the duration of the transaction.
-      this.data = this.optimisticData = new OptimisticCacheLayer(
-        // Note that there can be multiple layers with the same optimisticId.
-        // When removeOptimistic(id) is called for that id, all matching layers
-        // will be removed, and the remaining layers will be reapplied.
-        optimisticId,
-        this.optimisticData,
-        transaction,
-      );
-    }
-
-    try {
-      transaction(this);
-    } finally {
-      this.silenceBroadcast = silenceBroadcast;
-      this.data = data;
-    }
-
-    // This broadcast does nothing if this.silenceBroadcast is true.
-    this.broadcastWatches();
-  }
-
-  public recordOptimisticTransaction(
-    transaction: Transaction<NormalizedCacheObject>,
-    id: string,
-  ) {
-    return this.performTransaction(transaction, id);
-  }
-
-  public transformDocument(document: DocumentNode): DocumentNode {
-    if (this.addTypename) {
-      let result = this.typenameDocumentCache.get(document);
-      if (!result) {
-        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;
-    }
-    return document;
-  }
-
-  protected broadcastWatches() {
-    if (!this.silenceBroadcast) {
-      this.watches.forEach(c => this.maybeBroadcastWatch(c));
-    }
-  }
-
-  // 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) {
-    c.callback(
-      this.diff({
-        query: c.query,
-        variables: c.variables,
-        previousResult: c.previousResult && c.previousResult(),
-        optimistic: c.optimistic,
-      }),
-    );
-  }
-}
diff --git a/packages/apollo-cache-inmemory/src/index.ts b/packages/apollo-cache-inmemory/src/index.ts
deleted file mode 100644
--- a/packages/apollo-cache-inmemory/src/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export {
-  InMemoryCache,
-  InMemoryCacheConfig,
-  defaultDataIdFromObject,
-} from './inMemoryCache';
-
-export * from './readFromStore';
-export * from './writeToStore';
-export * from './fragmentMatcher';
-export * from './objectCache';
-export * from './types';
diff --git a/packages/apollo-cache-inmemory/src/mapCache.ts b/packages/apollo-cache-inmemory/src/mapCache.ts
deleted file mode 100644
--- a/packages/apollo-cache-inmemory/src/mapCache.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { NormalizedCache, NormalizedCacheObject, StoreObject } from './types';
-
-/**
- * A Map-based implementation of the NormalizedCache.
- * Note that you need a polyfill for Object.entries for this to work.
- */
-export class MapCache implements NormalizedCache {
-  private cache: Map<string, StoreObject | undefined>;
-
-  constructor(data: NormalizedCacheObject = {}) {
-    this.cache = new Map(Object.entries(data));
-  }
-
-  public get(dataId: string): StoreObject {
-    return this.cache.get(`${dataId}`)!;
-  }
-
-  public set(dataId: string, value: StoreObject): void {
-    this.cache.set(`${dataId}`, value);
-  }
-
-  public delete(dataId: string): void {
-    this.cache.delete(`${dataId}`);
-  }
-
-  public clear(): void {
-    return this.cache.clear();
-  }
-
-  public toObject(): NormalizedCacheObject {
-    const obj: NormalizedCacheObject = {};
-    this.cache.forEach((dataId, key) => {
-      obj[key] = dataId;
-    });
-    return obj;
-  }
-
-  public replace(newData: NormalizedCacheObject): void {
-    this.cache.clear();
-    Object.entries(newData).forEach(([dataId, value]) =>
-      this.cache.set(dataId, value),
-    );
-  }
-}
-
-export function mapNormalizedCacheFactory(
-  seed?: NormalizedCacheObject,
-): NormalizedCache {
-  return new MapCache(seed);
-}
diff --git a/packages/apollo-cache-inmemory/src/objectCache.ts b/packages/apollo-cache-inmemory/src/objectCache.ts
deleted file mode 100644
--- a/packages/apollo-cache-inmemory/src/objectCache.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { NormalizedCache, NormalizedCacheObject, StoreObject } from './types';
-
-export class ObjectCache implements NormalizedCache {
-  constructor(protected data: NormalizedCacheObject = Object.create(null)) {}
-
-  public toObject() {
-    return this.data;
-  }
-
-  public get(dataId: string) {
-    return this.data[dataId]!;
-  }
-
-  public set(dataId: string, value: StoreObject) {
-    this.data[dataId] = value;
-  }
-
-  public delete(dataId: string) {
-    this.data[dataId] = void 0;
-  }
-
-  public clear() {
-    this.data = Object.create(null);
-  }
-
-  public replace(newData: NormalizedCacheObject) {
-    this.data = newData || Object.create(null);
-  }
-}
-
-export function defaultNormalizedCacheFactory(
-  seed?: NormalizedCacheObject,
-): NormalizedCache {
-  return new ObjectCache(seed);
-}
diff --git a/packages/apollo-cache-inmemory/src/readFromStore.ts b/packages/apollo-cache-inmemory/src/readFromStore.ts
deleted file mode 100644
--- a/packages/apollo-cache-inmemory/src/readFromStore.ts
+++ /dev/null
@@ -1,638 +0,0 @@
-import {
-  argumentsObjectFromField,
-  assign,
-  canUseWeakMap,
-  createFragmentMap,
-  DirectiveInfo,
-  FragmentMap,
-  getDefaultValues,
-  getDirectiveInfoFromField,
-  getFragmentDefinitions,
-  getMainDefinition,
-  getQueryDefinition,
-  getStoreKeyName,
-  IdValue,
-  isEqual,
-  isField,
-  isIdValue,
-  isInlineFragment,
-  isJsonValue,
-  maybeDeepFreeze,
-  mergeDeepArray,
-  resultKeyNameFromField,
-  shouldInclude,
-  StoreValue,
-  toIdValue,
-} from 'apollo-utilities';
-
-import { Cache } from 'apollo-cache';
-
-import {
-  ReadStoreContext,
-  DiffQueryAgainstStoreOptions,
-  ReadQueryOptions,
-  StoreObject,
-} from './types';
-
-import {
-  DocumentNode,
-  FieldNode,
-  FragmentDefinitionNode,
-  InlineFragmentNode,
-  SelectionSetNode,
-} from 'graphql';
-
-import { wrap, KeyTrie } from 'optimism';
-import { DepTrackingCache } from './depTrackingCache';
-import { invariant, InvariantError } from 'ts-invariant';
-
-export type VariableMap = { [name: string]: any };
-
-export type FragmentMatcher = (
-  rootValue: any,
-  typeCondition: string,
-  context: ReadStoreContext,
-) => boolean | 'heuristic';
-
-type ExecContext = {
-  query: DocumentNode;
-  fragmentMap: FragmentMap;
-  contextValue: ReadStoreContext;
-  variableValues: VariableMap;
-  fragmentMatcher: FragmentMatcher;
-};
-
-type ExecInfo = {
-  resultKey: string;
-  directives: DirectiveInfo;
-};
-
-export type ExecResultMissingField = {
-  object: StoreObject;
-  fieldName: string;
-  tolerable: boolean;
-};
-
-export type ExecResult<R = any> = {
-  result: R;
-  // Empty array if no missing fields encountered while computing result.
-  missing?: ExecResultMissingField[];
-};
-
-type ExecStoreQueryOptions = {
-  query: DocumentNode;
-  rootValue: IdValue;
-  contextValue: ReadStoreContext;
-  variableValues: VariableMap;
-  // Default matcher always matches all fragments
-  fragmentMatcher?: FragmentMatcher;
-};
-
-type ExecSelectionSetOptions = {
-  selectionSet: SelectionSetNode;
-  rootValue: any;
-  execContext: ExecContext;
-};
-
-type ExecSubSelectedArrayOptions = {
-  field: FieldNode;
-  array: any[];
-  execContext: ExecContext;
-};
-
-export interface StoreReaderConfig {
-  cacheKeyRoot?: KeyTrie<object>;
-  freezeResults?: boolean;
-}
-
-export class StoreReader {
-  private freezeResults: boolean;
-
-  constructor({
-    cacheKeyRoot = new KeyTrie<object>(canUseWeakMap),
-    freezeResults = false,
-  }: StoreReaderConfig = {}) {
-    const {
-      executeStoreQuery,
-      executeSelectionSet,
-      executeSubSelectedArray,
-    } = this;
-
-    this.freezeResults = freezeResults;
-
-    this.executeStoreQuery = wrap((options: ExecStoreQueryOptions) => {
-      return executeStoreQuery.call(this, options);
-    }, {
-      makeCacheKey({
-        query,
-        rootValue,
-        contextValue,
-        variableValues,
-        fragmentMatcher,
-      }: ExecStoreQueryOptions) {
-        // The result of executeStoreQuery can be safely cached only if the
-        // underlying store is capable of tracking dependencies and invalidating
-        // the cache when relevant data have changed.
-        if (contextValue.store instanceof DepTrackingCache) {
-          return cacheKeyRoot.lookup(
-            contextValue.store,
-            query,
-            fragmentMatcher,
-            JSON.stringify(variableValues),
-            rootValue.id,
-          );
-        }
-      }
-    });
-
-    this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
-      return executeSelectionSet.call(this, options);
-    }, {
-      makeCacheKey({
-        selectionSet,
-        rootValue,
-        execContext,
-      }: ExecSelectionSetOptions) {
-        if (execContext.contextValue.store instanceof DepTrackingCache) {
-          return cacheKeyRoot.lookup(
-            execContext.contextValue.store,
-            selectionSet,
-            execContext.fragmentMatcher,
-            JSON.stringify(execContext.variableValues),
-            rootValue.id,
-          );
-        }
-      }
-    });
-
-    this.executeSubSelectedArray = wrap((options: ExecSubSelectedArrayOptions) => {
-      return executeSubSelectedArray.call(this, options);
-    }, {
-      makeCacheKey({ field, array, execContext }) {
-        if (execContext.contextValue.store instanceof DepTrackingCache) {
-          return cacheKeyRoot.lookup(
-            execContext.contextValue.store,
-            field,
-            array,
-            JSON.stringify(execContext.variableValues),
-          );
-        }
-      }
-    });
-  }
-
-  /**
-   * Resolves the result of a query solely from the store (i.e. never hits the server).
-   *
-   * @param {Store} store The {@link NormalizedCache} used by Apollo for the `data` portion of the
-   * store.
-   *
-   * @param {DocumentNode} query The query document to resolve from the data available in the store.
-   *
-   * @param {Object} [variables] A map from the name of a variable to its value. These variables can
-   * be referenced by the query document.
-   *
-   * @param {any} previousResult The previous result returned by this function for the same query.
-   * If nothing in the store changed since that previous result then values from the previous result
-   * will be returned to preserve referential equality.
-   */
-  public readQueryFromStore<QueryType>(
-    options: ReadQueryOptions,
-  ): QueryType | undefined {
-    return this.diffQueryAgainstStore<QueryType>({
-      ...options,
-      returnPartialData: false,
-    }).result;
-  }
-
-  /**
-   * Given a store and a query, return as much of the result as possible and
-   * identify if any data was missing from the store.
-   * @param  {DocumentNode} query A parsed GraphQL query document
-   * @param  {Store} store The Apollo Client store object
-   * @param  {any} previousResult The previous result returned by this function for the same query
-   * @return {result: Object, complete: [boolean]}
-   */
-  public diffQueryAgainstStore<T>({
-    store,
-    query,
-    variables,
-    previousResult,
-    returnPartialData = true,
-    rootId = 'ROOT_QUERY',
-    fragmentMatcherFunction,
-    config,
-  }: DiffQueryAgainstStoreOptions): Cache.DiffResult<T> {
-    // Throw the right validation error by trying to find a query in the document
-    const queryDefinition = getQueryDefinition(query);
-
-    variables = assign({}, getDefaultValues(queryDefinition), variables);
-
-    const context: ReadStoreContext = {
-      // Global settings
-      store,
-      dataIdFromObject: config && config.dataIdFromObject,
-      cacheRedirects: (config && config.cacheRedirects) || {},
-    };
-
-    const execResult = this.executeStoreQuery({
-      query,
-      rootValue: {
-        type: 'id',
-        id: rootId,
-        generated: true,
-        typename: 'Query',
-      },
-      contextValue: context,
-      variableValues: variables,
-      fragmentMatcher: fragmentMatcherFunction,
-    });
-
-    const hasMissingFields =
-      execResult.missing && execResult.missing.length > 0;
-
-    if (hasMissingFields && ! returnPartialData) {
-      execResult.missing!.forEach(info => {
-        if (info.tolerable) return;
-        throw new InvariantError(
-          `Can't find field ${info.fieldName} on object ${JSON.stringify(
-            info.object,
-            null,
-            2,
-          )}.`,
-        );
-      });
-    }
-
-    if (previousResult) {
-      if (isEqual(previousResult, execResult.result)) {
-        execResult.result = previousResult;
-      }
-    }
-
-    return {
-      result: execResult.result,
-      complete: !hasMissingFields,
-    };
-  }
-
-  /**
-   * Based on graphql function from graphql-js:
-   *
-   * graphql(
-   *   schema: GraphQLSchema,
-   *   requestString: string,
-   *   rootValue?: ?any,
-   *   contextValue?: ?any,
-   *   variableValues?: ?{[key: string]: any},
-   *   operationName?: ?string
-   * ): Promise<GraphQLResult>
-   *
-   * The default export as of graphql-anywhere is sync as of 4.0,
-   * but below is an exported alternative that is async.
-   * In the 5.0 version, this will be the only export again
-   * and it will be async
-   *
-   */
-  private executeStoreQuery({
-    query,
-    rootValue,
-    contextValue,
-    variableValues,
-    // Default matcher always matches all fragments
-    fragmentMatcher = defaultFragmentMatcher,
-  }: ExecStoreQueryOptions): ExecResult {
-    const mainDefinition = getMainDefinition(query);
-    const fragments = getFragmentDefinitions(query);
-    const fragmentMap = createFragmentMap(fragments);
-    const execContext: ExecContext = {
-      query,
-      fragmentMap,
-      contextValue,
-      variableValues,
-      fragmentMatcher,
-    };
-
-    return this.executeSelectionSet({
-      selectionSet: mainDefinition.selectionSet,
-      rootValue,
-      execContext,
-    });
-  }
-
-  private executeSelectionSet({
-    selectionSet,
-    rootValue,
-    execContext,
-  }: ExecSelectionSetOptions): ExecResult {
-    const { fragmentMap, contextValue, variableValues: variables } = execContext;
-    const finalResult: ExecResult = { result: null };
-
-    const objectsToMerge: { [key: string]: any }[] = [];
-
-    const object: StoreObject = contextValue.store.get(rootValue.id);
-
-    const typename =
-      (object && object.__typename) ||
-      (rootValue.id === 'ROOT_QUERY' && 'Query') ||
-      void 0;
-
-    function handleMissing<T>(result: ExecResult<T>): T {
-      if (result.missing) {
-        finalResult.missing = finalResult.missing || [];
-        finalResult.missing.push(...result.missing);
-      }
-      return result.result;
-    }
-
-    selectionSet.selections.forEach(selection => {
-      if (!shouldInclude(selection, variables)) {
-        // Skip this entirely
-        return;
-      }
-
-      if (isField(selection)) {
-        const fieldResult = handleMissing(
-          this.executeField(object, typename, selection, execContext),
-        );
-
-        if (typeof fieldResult !== 'undefined') {
-          objectsToMerge.push({
-            [resultKeyNameFromField(selection)]: fieldResult,
-          });
-        }
-
-      } else {
-        let fragment: InlineFragmentNode | FragmentDefinitionNode;
-
-        if (isInlineFragment(selection)) {
-          fragment = selection;
-        } else {
-          // This is a named fragment
-          fragment = fragmentMap[selection.name.value];
-
-          if (!fragment) {
-            throw new InvariantError(`No fragment named ${selection.name.value}`);
-          }
-        }
-
-        const typeCondition =
-          fragment.typeCondition && fragment.typeCondition.name.value;
-
-        const match =
-          !typeCondition ||
-          execContext.fragmentMatcher(rootValue, typeCondition, contextValue);
-
-        if (match) {
-          let fragmentExecResult = this.executeSelectionSet({
-            selectionSet: fragment.selectionSet,
-            rootValue,
-            execContext,
-          });
-
-          if (match === 'heuristic' && fragmentExecResult.missing) {
-            fragmentExecResult = {
-              ...fragmentExecResult,
-              missing: fragmentExecResult.missing.map(info => {
-                return { ...info, tolerable: true };
-              }),
-            };
-          }
-
-          objectsToMerge.push(handleMissing(fragmentExecResult));
-        }
-      }
-    });
-
-    // Perform a single merge at the end so that we can avoid making more
-    // defensive shallow copies than necessary.
-    finalResult.result = mergeDeepArray(objectsToMerge);
-
-    if (this.freezeResults && process.env.NODE_ENV !== 'production') {
-      Object.freeze(finalResult.result);
-    }
-
-    return finalResult;
-  }
-
-  private executeField(
-    object: StoreObject,
-    typename: string | void,
-    field: FieldNode,
-    execContext: ExecContext,
-  ): ExecResult {
-    const { variableValues: variables, contextValue } = execContext;
-    const fieldName = field.name.value;
-    const args = argumentsObjectFromField(field, variables);
-
-    const info: ExecInfo = {
-      resultKey: resultKeyNameFromField(field),
-      directives: getDirectiveInfoFromField(field, variables),
-    };
-
-    const readStoreResult = readStoreResolver(
-      object,
-      typename,
-      fieldName,
-      args,
-      contextValue,
-      info,
-    );
-
-    if (Array.isArray(readStoreResult.result)) {
-      return this.combineExecResults(
-        readStoreResult,
-        this.executeSubSelectedArray({
-          field,
-          array: readStoreResult.result,
-          execContext,
-        }),
-      );
-    }
-
-    // Handle all scalar types here
-    if (!field.selectionSet) {
-      assertSelectionSetForIdValue(field, readStoreResult.result);
-      if (this.freezeResults && process.env.NODE_ENV !== 'production') {
-        maybeDeepFreeze(readStoreResult);
-      }
-      return readStoreResult;
-    }
-
-    // From here down, the field has a selection set, which means it's trying to
-    // query a GraphQLObjectType
-    if (readStoreResult.result == null) {
-      // Basically any field in a GraphQL response can be null, or missing
-      return readStoreResult;
-    }
-
-    // Returned value is an object, and the query has a sub-selection. Recurse.
-    return this.combineExecResults(
-      readStoreResult,
-      this.executeSelectionSet({
-        selectionSet: field.selectionSet,
-        rootValue: readStoreResult.result,
-        execContext,
-      }),
-    );
-  }
-
-  private combineExecResults<T>(
-    ...execResults: ExecResult<T>[]
-  ): ExecResult<T> {
-    let missing: ExecResultMissingField[] | undefined;
-    execResults.forEach(execResult => {
-      if (execResult.missing) {
-        missing = missing || [];
-        missing.push(...execResult.missing);
-      }
-    });
-    return {
-      result: execResults.pop()!.result,
-      missing,
-    };
-  }
-
-  private executeSubSelectedArray({
-    field,
-    array,
-    execContext,
-  }: ExecSubSelectedArrayOptions): ExecResult {
-    let missing: ExecResultMissingField[] | undefined;
-
-    function handleMissing<T>(childResult: ExecResult<T>): T {
-      if (childResult.missing) {
-        missing = missing || [];
-        missing.push(...childResult.missing);
-      }
-
-      return childResult.result;
-    }
-
-    array = array.map(item => {
-      // null value in array
-      if (item === null) {
-        return null;
-      }
-
-      // This is a nested array, recurse
-      if (Array.isArray(item)) {
-        return handleMissing(this.executeSubSelectedArray({
-          field,
-          array: item,
-          execContext,
-        }));
-      }
-
-      // This is an object, run the selection set on it
-      if (field.selectionSet) {
-        return handleMissing(this.executeSelectionSet({
-          selectionSet: field.selectionSet,
-          rootValue: item,
-          execContext,
-        }));
-      }
-
-      assertSelectionSetForIdValue(field, item);
-
-      return item;
-    });
-
-    if (this.freezeResults && process.env.NODE_ENV !== 'production') {
-      Object.freeze(array);
-    }
-
-    return { result: array, missing };
-  }
-}
-
-function assertSelectionSetForIdValue(
-  field: FieldNode,
-  value: any,
-) {
-  if (!field.selectionSet && isIdValue(value)) {
-    throw new InvariantError(
-      `Missing selection set for object of type ${
-        value.typename
-      } returned for query field ${field.name.value}`
-    );
-  }
-}
-
-function defaultFragmentMatcher() {
-  return true;
-}
-
-export function assertIdValue(idValue: IdValue) {
-  invariant(isIdValue(idValue), `\
-Encountered a sub-selection on the query, but the store doesn't have \
-an object reference. This should never happen during normal use unless you have custom code \
-that is directly manipulating the store; please file an issue.`);
-}
-
-function readStoreResolver(
-  object: StoreObject,
-  typename: string | void,
-  fieldName: string,
-  args: any,
-  context: ReadStoreContext,
-  { resultKey, directives }: ExecInfo,
-): ExecResult<StoreValue> {
-  let storeKeyName = fieldName;
-  if (args || directives) {
-    // We happen to know here that getStoreKeyName returns its first
-    // argument unmodified if there are no args or directives, so we can
-    // avoid calling the function at all in that case, as a small but
-    // important optimization to this frequently executed code.
-    storeKeyName = getStoreKeyName(storeKeyName, args, directives);
-  }
-
-  let fieldValue: StoreValue | void = void 0;
-
-  if (object) {
-    fieldValue = object[storeKeyName];
-
-    if (
-      typeof fieldValue === 'undefined' &&
-      context.cacheRedirects &&
-      typeof typename === 'string'
-    ) {
-      // Look for the type in the custom resolver map
-      const type = context.cacheRedirects[typename];
-      if (type) {
-        // Look for the field in the custom resolver map
-        const resolver = type[fieldName];
-        if (resolver) {
-          fieldValue = resolver(object, args, {
-            getCacheKey(storeObj: StoreObject) {
-              const id = context.dataIdFromObject!(storeObj);
-              return id && toIdValue({
-                id,
-                typename: storeObj.__typename,
-              });
-            },
-          });
-        }
-      }
-    }
-  }
-
-  if (typeof fieldValue === 'undefined') {
-    return {
-      result: fieldValue,
-      missing: [{
-        object,
-        fieldName: storeKeyName,
-        tolerable: false,
-      }],
-    };
-  }
-
-  if (isJsonValue(fieldValue)) {
-    fieldValue = fieldValue.json;
-  }
-
-  return {
-    result: fieldValue,
-  };
-}
diff --git a/packages/apollo-cache-inmemory/src/writeToStore.ts b/packages/apollo-cache-inmemory/src/writeToStore.ts
deleted file mode 100644
--- a/packages/apollo-cache-inmemory/src/writeToStore.ts
+++ /dev/null
@@ -1,520 +0,0 @@
-import {
-  SelectionSetNode,
-  FieldNode,
-  DocumentNode,
-  InlineFragmentNode,
-  FragmentDefinitionNode,
-} from 'graphql';
-import { FragmentMatcher } from './readFromStore';
-
-import {
-  assign,
-  createFragmentMap,
-  FragmentMap,
-  getDefaultValues,
-  getFragmentDefinitions,
-  getOperationDefinition,
-  IdValue,
-  isField,
-  isIdValue,
-  isInlineFragment,
-  isProduction,
-  resultKeyNameFromField,
-  shouldInclude,
-  storeKeyNameFromField,
-  StoreValue,
-  toIdValue,
-  isEqual,
-} from 'apollo-utilities';
-
-import { invariant } from 'ts-invariant';
-
-import { ObjectCache } from './objectCache';
-import { defaultNormalizedCacheFactory } from './depTrackingCache';
-
-import {
-  IdGetter,
-  NormalizedCache,
-  ReadStoreContext,
-  StoreObject,
-} from './types';
-
-export class WriteError extends Error {
-  public type = 'WriteError';
-}
-
-export function enhanceErrorWithDocument(error: Error, document: DocumentNode) {
-  // XXX A bit hacky maybe ...
-  const enhancedError = new WriteError(
-    `Error writing result to store for query:\n ${JSON.stringify(document)}`,
-  );
-  enhancedError.message += '\n' + error.message;
-  enhancedError.stack = error.stack;
-  return enhancedError;
-}
-
-export type WriteContext = {
-  readonly store: NormalizedCache;
-  readonly processedData?: { [x: string]: FieldNode[] };
-  readonly variables?: any;
-  readonly dataIdFromObject?: IdGetter;
-  readonly fragmentMap?: FragmentMap;
-  readonly fragmentMatcherFunction?: FragmentMatcher;
-};
-
-export class StoreWriter {
-  /**
-   * Writes the result of a query to the store.
-   *
-   * @param result The result object returned for the query document.
-   *
-   * @param query The query document whose result we are writing to the store.
-   *
-   * @param store The {@link NormalizedCache} used by Apollo for the `data` portion of the store.
-   *
-   * @param variables A map from the name of a variable to its value. These variables can be
-   * referenced by the query document.
-   *
-   * @param dataIdFromObject A function that returns an object identifier given a particular result
-   * object. See the store documentation for details and an example of this function.
-   *
-   * @param fragmentMatcherFunction A function to use for matching fragment conditions in GraphQL documents
-   */
-  public writeQueryToStore({
-    query,
-    result,
-    store = defaultNormalizedCacheFactory(),
-    variables,
-    dataIdFromObject,
-    fragmentMatcherFunction,
-  }: {
-    query: DocumentNode;
-    result: Object;
-    store?: NormalizedCache;
-    variables?: Object;
-    dataIdFromObject?: IdGetter;
-    fragmentMatcherFunction?: FragmentMatcher;
-  }): NormalizedCache {
-    return this.writeResultToStore({
-      dataId: 'ROOT_QUERY',
-      result,
-      document: query,
-      store,
-      variables,
-      dataIdFromObject,
-      fragmentMatcherFunction,
-    });
-  }
-
-  public writeResultToStore({
-    dataId,
-    result,
-    document,
-    store = defaultNormalizedCacheFactory(),
-    variables,
-    dataIdFromObject,
-    fragmentMatcherFunction,
-  }: {
-    dataId: string;
-    result: any;
-    document: DocumentNode;
-    store?: NormalizedCache;
-    variables?: Object;
-    dataIdFromObject?: IdGetter;
-    fragmentMatcherFunction?: FragmentMatcher;
-  }): NormalizedCache {
-    // XXX TODO REFACTOR: this is a temporary workaround until query normalization is made to work with documents.
-    const operationDefinition = getOperationDefinition(document)!;
-
-    try {
-      return this.writeSelectionSetToStore({
-        result,
-        dataId,
-        selectionSet: operationDefinition.selectionSet,
-        context: {
-          store,
-          processedData: {},
-          variables: assign(
-            {},
-            getDefaultValues(operationDefinition),
-            variables,
-          ),
-          dataIdFromObject,
-          fragmentMap: createFragmentMap(getFragmentDefinitions(document)),
-          fragmentMatcherFunction,
-        },
-      });
-    } catch (e) {
-      throw enhanceErrorWithDocument(e, document);
-    }
-  }
-
-  public writeSelectionSetToStore({
-    result,
-    dataId,
-    selectionSet,
-    context,
-  }: {
-    dataId: string;
-    result: any;
-    selectionSet: SelectionSetNode;
-    context: WriteContext;
-  }): NormalizedCache {
-    const { variables, store, fragmentMap } = context;
-
-    selectionSet.selections.forEach(selection => {
-      if (!shouldInclude(selection, variables)) {
-        return;
-      }
-
-      if (isField(selection)) {
-        const resultFieldKey: string = resultKeyNameFromField(selection);
-        const value: any = result[resultFieldKey];
-
-        if (typeof value !== 'undefined') {
-          this.writeFieldToStore({
-            dataId,
-            value,
-            field: selection,
-            context,
-          });
-        } else {
-          let isDefered = false;
-          let isClient = false;
-          if (selection.directives && selection.directives.length) {
-            // If this is a defered field we don't need to throw / warn.
-            isDefered = selection.directives.some(
-              directive => directive.name && directive.name.value === 'defer',
-            );
-
-            // When using the @client directive, it might be desirable in
-            // some cases to want to write a selection set to the store,
-            // without having all of the selection set values available.
-            // This is because the @client field values might have already
-            // been written to the cache separately (e.g. via Apollo
-            // Cache's `writeData` capabilities). Because of this, we'll
-            // skip the missing field warning for fields with @client
-            // directives.
-            isClient = selection.directives.some(
-              directive => directive.name && directive.name.value === 'client',
-            );
-          }
-
-          if (!isDefered && !isClient && context.fragmentMatcherFunction) {
-            // XXX We'd like to throw an error, but for backwards compatibility's sake
-            // we just print a warning for the time being.
-            //throw new WriteError(`Missing field ${resultFieldKey} in ${JSON.stringify(result, null, 2).substring(0, 100)}`);
-            invariant.warn(
-              `Missing field ${resultFieldKey} in ${JSON.stringify(
-                result,
-                null,
-                2,
-              ).substring(0, 100)}`,
-            );
-          }
-        }
-      } else {
-        // This is not a field, so it must be a fragment, either inline or named
-        let fragment: InlineFragmentNode | FragmentDefinitionNode;
-
-        if (isInlineFragment(selection)) {
-          fragment = selection;
-        } else {
-          // Named fragment
-          fragment = (fragmentMap || {})[selection.name.value];
-          invariant(fragment, `No fragment named ${selection.name.value}.`);
-        }
-
-        let matches = true;
-        if (context.fragmentMatcherFunction && fragment.typeCondition) {
-          // TODO we need to rewrite the fragment matchers for this to work properly and efficiently
-          // Right now we have to pretend that we're passing in an idValue and that there's a store
-          // on the context.
-          const id = dataId || 'self';
-          const idValue = toIdValue({ id, typename: undefined });
-          const fakeContext: ReadStoreContext = {
-            // NOTE: fakeContext always uses ObjectCache
-            // since this is only to ensure the return value of 'matches'
-            store: new ObjectCache({ [id]: result }),
-            cacheRedirects: {},
-          };
-          const match = context.fragmentMatcherFunction(
-            idValue,
-            fragment.typeCondition.name.value,
-            fakeContext,
-          );
-          if (!isProduction() && match === 'heuristic') {
-            invariant.error('WARNING: heuristic fragment matching going on!');
-          }
-          matches = !!match;
-        }
-
-        if (matches) {
-          this.writeSelectionSetToStore({
-            result,
-            selectionSet: fragment.selectionSet,
-            dataId,
-            context,
-          });
-        }
-      }
-    });
-
-    return store;
-  }
-
-  private writeFieldToStore({
-    field,
-    value,
-    dataId,
-    context,
-  }: {
-    field: FieldNode;
-    value: any;
-    dataId: string;
-    context: WriteContext;
-  }) {
-    const { variables, dataIdFromObject, store } = context;
-
-    let storeValue: StoreValue;
-    let storeObject: StoreObject;
-
-    const storeFieldName: string = storeKeyNameFromField(field, variables);
-
-    // If this is a scalar value...
-    if (!field.selectionSet || value === null) {
-      storeValue =
-        value != null && typeof value === 'object'
-          ? // If the scalar value is a JSON blob, we have to "escape" it so it can’t pretend to be
-            // an id.
-            { type: 'json', json: value }
-          : // Otherwise, just store the scalar directly in the store.
-            value;
-    } else if (Array.isArray(value)) {
-      const generatedId = `${dataId}.${storeFieldName}`;
-
-      storeValue = this.processArrayValue(
-        value,
-        generatedId,
-        field.selectionSet,
-        context,
-      );
-    } else {
-      // It's an object
-      let valueDataId = `${dataId}.${storeFieldName}`;
-      let generated = true;
-
-      // We only prepend the '$' if the valueDataId isn't already a generated
-      // id.
-      if (!isGeneratedId(valueDataId)) {
-        valueDataId = '$' + valueDataId;
-      }
-
-      if (dataIdFromObject) {
-        const semanticId = dataIdFromObject(value);
-
-        // We throw an error if the first character of the id is '$. This is
-        // because we use that character to designate an Apollo-generated id
-        // and we use the distinction between user-desiginated and application-provided
-        // ids when managing overwrites.
-        invariant(
-          !semanticId || !isGeneratedId(semanticId),
-          'IDs returned by dataIdFromObject cannot begin with the "$" character.',
-        );
-
-        if (
-          semanticId ||
-          (typeof semanticId === 'number' && semanticId === 0)
-        ) {
-          valueDataId = semanticId;
-          generated = false;
-        }
-      }
-
-      if (!isDataProcessed(valueDataId, field, context.processedData)) {
-        this.writeSelectionSetToStore({
-          dataId: valueDataId,
-          result: value,
-          selectionSet: field.selectionSet,
-          context,
-        });
-      }
-
-      // We take the id and escape it (i.e. wrap it with an enclosing object).
-      // This allows us to distinguish IDs from normal scalars.
-      const typename = value.__typename;
-      storeValue = toIdValue({ id: valueDataId, typename }, generated);
-
-      // check if there was a generated id at the location where we're
-      // about to place this new id. If there was, we have to merge the
-      // data from that id with the data we're about to write in the store.
-      storeObject = store.get(dataId);
-      const escapedId =
-        storeObject && (storeObject[storeFieldName] as IdValue | undefined);
-      if (escapedId !== storeValue && isIdValue(escapedId)) {
-        const hadTypename = escapedId.typename !== undefined;
-        const hasTypename = typename !== undefined;
-        const typenameChanged =
-          hadTypename && hasTypename && escapedId.typename !== typename;
-
-        // If there is already a real id in the store and the current id we
-        // are dealing with is generated, we throw an error.
-        // One exception we allow is when the typename has changed, which occurs
-        // when schema defines a union, both with and without an ID in the same place.
-        // checks if we "lost" the read id
-        invariant(
-          !generated || escapedId.generated || typenameChanged,
-          `Store error: the application attempted to write an object with no provided id but the store already contains an id of ${
-            escapedId.id
-          } for this object. The selectionSet that was trying to be written is:\n${
-            JSON.stringify(field)
-          }`,
-        );
-
-        // checks if we "lost" the typename
-        invariant(
-          !hadTypename || hasTypename,
-          `Store error: the application attempted to write an object with no provided typename but the store already contains an object with typename of ${
-            escapedId.typename
-          } for the object of id ${escapedId.id}. The selectionSet that was trying to be written is:\n${
-            JSON.stringify(field)
-          }`,
-        );
-
-        if (escapedId.generated) {
-          // We should only merge if it's an object of the same type,
-          // otherwise we should delete the generated object
-          if (typenameChanged) {
-            // Only delete the generated object when the old object was
-            // inlined, and the new object is not. This is indicated by
-            // the old id being generated, and the new id being real.
-            if (!generated) {
-              store.delete(escapedId.id);
-            }
-          } else {
-            mergeWithGenerated(escapedId.id, (storeValue as IdValue).id, store);
-          }
-        }
-      }
-    }
-
-    storeObject = store.get(dataId);
-    if (!storeObject || !isEqual(storeValue, storeObject[storeFieldName])) {
-      store.set(dataId, {
-        ...storeObject,
-        [storeFieldName]: storeValue,
-      });
-    }
-  }
-
-  private processArrayValue(
-    value: any[],
-    generatedId: string,
-    selectionSet: SelectionSetNode,
-    context: WriteContext,
-  ): any[] {
-    return value.map((item: any, index: any) => {
-      if (item === null) {
-        return null;
-      }
-
-      let itemDataId = `${generatedId}.${index}`;
-
-      if (Array.isArray(item)) {
-        return this.processArrayValue(item, itemDataId, selectionSet, context);
-      }
-
-      let generated = true;
-
-      if (context.dataIdFromObject) {
-        const semanticId = context.dataIdFromObject(item);
-
-        if (semanticId) {
-          itemDataId = semanticId;
-          generated = false;
-        }
-      }
-
-      if (!isDataProcessed(itemDataId, selectionSet, context.processedData)) {
-        this.writeSelectionSetToStore({
-          dataId: itemDataId,
-          result: item,
-          selectionSet,
-          context,
-        });
-      }
-
-      return toIdValue(
-        { id: itemDataId, typename: item.__typename },
-        generated,
-      );
-    });
-  }
-}
-
-// Checks if the id given is an id that was generated by Apollo
-// rather than by dataIdFromObject.
-function isGeneratedId(id: string): boolean {
-  return id[0] === '$';
-}
-
-function mergeWithGenerated(
-  generatedKey: string,
-  realKey: string,
-  cache: NormalizedCache,
-): boolean {
-  if (generatedKey === realKey) {
-    return false;
-  }
-
-  const generated = cache.get(generatedKey);
-  const real = cache.get(realKey);
-  let madeChanges = false;
-
-  Object.keys(generated).forEach(key => {
-    const value = generated[key];
-    const realValue = real[key];
-
-    if (
-      isIdValue(value) &&
-      isGeneratedId(value.id) &&
-      isIdValue(realValue) &&
-      !isEqual(value, realValue) &&
-      mergeWithGenerated(value.id, realValue.id, cache)
-    ) {
-      madeChanges = true;
-    }
-  });
-
-  cache.delete(generatedKey);
-  const newRealValue = { ...generated, ...real };
-
-  if (isEqual(newRealValue, real)) {
-    return madeChanges;
-  }
-
-  cache.set(realKey, newRealValue);
-  return true;
-}
-
-function isDataProcessed(
-  dataId: string,
-  field: FieldNode | SelectionSetNode,
-  processedData?: { [x: string]: (FieldNode | SelectionSetNode)[] },
-): boolean {
-  if (!processedData) {
-    return false;
-  }
-
-  if (processedData[dataId]) {
-    if (processedData[dataId].indexOf(field) >= 0) {
-      return true;
-    } else {
-      processedData[dataId].push(field);
-    }
-  } else {
-    processedData[dataId] = [field];
-  }
-
-  return false;
-}
diff --git a/packages/apollo-cache/jest.config.js b/packages/apollo-cache/jest.config.js
deleted file mode 100644
--- a/packages/apollo-cache/jest.config.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
-  ...require('../../config/jest.config.settings'),
-};
diff --git a/packages/apollo-cache/rollup.config.js b/packages/apollo-cache/rollup.config.js
deleted file mode 100644
--- a/packages/apollo-cache/rollup.config.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { rollup } from '../../config/rollup.config';
-
-export default rollup({
-  name: 'apollo-cache',
-});
diff --git a/packages/apollo-cache/src/index.ts b/packages/apollo-cache/src/index.ts
deleted file mode 100644
--- a/packages/apollo-cache/src/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './cache';
-export * from './types';
diff --git a/packages/apollo-cache/src/types/index.ts b/packages/apollo-cache/src/types/index.ts
deleted file mode 100644
--- a/packages/apollo-cache/src/types/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './DataProxy';
-export * from './Cache';
diff --git a/packages/apollo-client/analyze/src/index.js b/packages/apollo-client/analyze/src/index.js
deleted file mode 100644
--- a/packages/apollo-client/analyze/src/index.js
+++ /dev/null
@@ -1 +0,0 @@
-import ApolloClient from '../../lib/src';
diff --git a/packages/apollo-client/analyze/webpack.config.js b/packages/apollo-client/analyze/webpack.config.js
deleted file mode 100644
--- a/packages/apollo-client/analyze/webpack.config.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const path = require('path');
-const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
-  .BundleAnalyzerPlugin;
-
-module.exports = {
-  context: __dirname,
-  entry: './src/index.js',
-  output: {
-    path: path.resolve(__dirname, 'dist'),
-    filename: 'bundle.js',
-  },
-  plugins: [new BundleAnalyzerPlugin()],
-};
diff --git a/packages/apollo-client/benchmark/github-reporter.ts b/packages/apollo-client/benchmark/github-reporter.ts
deleted file mode 100644
--- a/packages/apollo-client/benchmark/github-reporter.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-import GithubAPI from '@octokit/rest';
-import { bsuite, groupPromises, log } from './util';
-import { thresholds } from './thresholds';
-
-export function collectAndReportBenchmarks(uploadToGithub: Boolean) {
-  const github = eval('new require("@octokit/rest")()') as GithubAPI;
-  const commitSHA =
-    process.env.TRAVIS_PULL_REQUEST_SHA || process.env.TRAVIS_COMMIT || '';
-
-  if (uploadToGithub) {
-    github.authenticate({
-      type: 'oauth',
-      token: process.env.DANGER_GITHUB_API_TOKEN || '',
-    });
-
-    github.repos.createStatus({
-      owner: 'apollographql',
-      repo: 'apollo-client',
-      sha: commitSHA,
-      context: 'Benchmark',
-      description: 'Evaluation is in progress!',
-      state: 'pending',
-    });
-  }
-
-  Promise.all(groupPromises)
-    .then(() => {
-      log('Running benchmarks.');
-      return new Promise<{
-        [name: string]: { mean: number; moe: number };
-      }>(resolve => {
-        const retMap: { [name: string]: { mean: number; moe: number } } = {};
-
-        bsuite
-          .on('error', (error: any) => {
-            log('Error: ', error);
-          })
-          .on('cycle', (event: any) => {
-            retMap[event.target.name] = {
-              mean: event.target.stats.mean * 1000,
-              moe: event.target.stats.moe * 1000,
-            };
-            log('Mean time in ms: ', event.target.stats.mean * 1000);
-            log(String(event.target));
-            log('');
-          })
-          .on('complete', (_: any) => {
-            resolve(retMap);
-          })
-          .run({ async: false });
-      });
-    })
-    .then(res => {
-      let message = '';
-      let pass = false;
-      Object.keys(res).forEach(element => {
-        if (element != 'baseline') {
-          if (!thresholds[element]) {
-            console.error(`Threshold not defined for "${element}"`);
-            if (message === '') {
-              message = `Threshold not defined for "${element}"`;
-              pass = false;
-            }
-          } else {
-            const normalizedMean = res[element].mean / res['baseline'].mean;
-            if (normalizedMean > thresholds[element]) {
-              const perfDropMessage = `Performance drop detected for benchmark: "${element}", ${
-                res[element].mean
-              } / ${res['baseline'].mean} = ${normalizedMean} > ${
-                thresholds[element]
-              }`;
-              console.error(perfDropMessage);
-              if (message === '') {
-                message = `Performance drop detected for benchmark: "${element}"`;
-                pass = false;
-              }
-            } else {
-              console.log(
-                `No performance drop detected for benchmark: "${element}", ${
-                  res[element].mean
-                } / ${res['baseline'].mean} = ${normalizedMean} <= ${
-                  thresholds[element]
-                }`,
-              );
-            }
-          }
-        }
-      });
-
-      if (message === '') {
-        message = 'All benchmarks are under the defined thresholds!';
-        pass = true;
-      }
-
-      console.log('Reporting benchmarks to GitHub status...');
-
-      if (uploadToGithub) {
-        return github.repos
-          .createStatus({
-            owner: 'apollographql',
-            repo: 'apollo-client',
-            sha: commitSHA,
-            context: 'Benchmark',
-            description: message,
-            state: pass ? 'success' : 'error',
-          })
-          .then(() => {
-            console.log('Published benchmark results to GitHub status');
-          });
-      } else {
-        return;
-      }
-    });
-}
diff --git a/packages/apollo-client/benchmark/index.ts b/packages/apollo-client/benchmark/index.ts
deleted file mode 100644
--- a/packages/apollo-client/benchmark/index.ts
+++ /dev/null
@@ -1,428 +0,0 @@
-// This file implements some of the basic benchmarks around
-// Apollo Client.
-
-import gql from 'graphql-tag';
-
-import {
-  group,
-  benchmark,
-  afterEach,
-  DescriptionObject,
-  dataIdFromObject,
-} from './util';
-
-import { ApolloClient, ApolloQueryResult } from '../src/index';
-
-import { times, cloneDeep } from 'lodash';
-
-import { InMemoryCache } from 'apollo-cache-inmemory';
-
-import { Operation, ApolloLink, FetchResult, Observable } from 'apollo-link';
-
-import { print } from 'graphql/language/printer';
-
-import { collectAndReportBenchmarks } from './github-reporter';
-
-interface MockedResponse {
-  request: Operation;
-  result?: FetchResult;
-  error?: Error;
-  delay?: number;
-}
-
-function mockSingleLink(...mockedResponses: MockedResponse[]): ApolloLink {
-  return new MockLink(mockedResponses);
-}
-
-function requestToKey(request: Operation): string {
-  const queryString = request.query && print(request.query);
-
-  return JSON.stringify({
-    variables: request.variables || {},
-    query: queryString,
-  });
-}
-
-class MockLink extends ApolloLink {
-  private mockedResponsesByKey: { [key: string]: MockedResponse[] } = {};
-
-  constructor(mockedResponses: MockedResponse[]) {
-    super();
-    mockedResponses.forEach(mockedResponse => {
-      this.addMockedResponse(mockedResponse);
-    });
-  }
-
-  public addMockedResponse(mockedResponse: MockedResponse) {
-    const key = requestToKey(mockedResponse.request);
-    let mockedResponses = this.mockedResponsesByKey[key];
-    if (!mockedResponses) {
-      mockedResponses = [];
-      this.mockedResponsesByKey[key] = mockedResponses;
-    }
-    mockedResponses.push(mockedResponse);
-  }
-
-  public request(operation: Operation) {
-    const key = requestToKey(operation);
-    const responses = this.mockedResponsesByKey[key];
-    if (!responses || responses.length === 0) {
-      throw new Error(
-        `No more mocked responses for the query: ${print(
-          operation.query,
-        )}, variables: ${JSON.stringify(operation.variables)}`,
-      );
-    }
-
-    const { result, error, delay } = responses.shift()!;
-    if (!result && !error) {
-      throw new Error(
-        `Mocked response should contain either result or error: ${key}`,
-      );
-    }
-
-    return new Observable<FetchResult>(observer => {
-      let timer = setTimeout(() => {
-        if (error) {
-          observer.error(error);
-        } else {
-          if (result) observer.next(result);
-          observer.complete();
-        }
-      }, delay ? delay : 0);
-
-      return () => {
-        clearTimeout(timer);
-      };
-    });
-  }
-}
-
-const simpleQuery = gql`
-  query {
-    author {
-      firstName
-      lastName
-    }
-  }
-`;
-
-const simpleResult = {
-  data: {
-    author: {
-      firstName: 'John',
-      lastName: 'Smith',
-    },
-  },
-};
-
-const getClientInstance = () => {
-  const link = mockSingleLink({
-    request: { query: simpleQuery } as Operation,
-    result: simpleResult,
-  });
-
-  return new ApolloClient({
-    link,
-    cache: new InMemoryCache({ addTypename: false }),
-  });
-};
-
-const createReservations = (count: number) => {
-  const reservations: {
-    name: string;
-    id: string;
-  }[] = [];
-  times(count, reservationIndex => {
-    reservations.push({
-      name: 'Fake Reservation',
-      id: reservationIndex.toString(),
-    });
-  });
-  return reservations;
-};
-
-group(end => {
-  benchmark('baseline', done => {
-    let arr = Array.from({ length: 100 }, () => Math.random());
-    arr.sort();
-    done();
-  });
-  end();
-});
-
-group(end => {
-  const link = mockSingleLink({
-    request: { query: simpleQuery } as Operation,
-    result: simpleResult,
-  });
-
-  const cache = new InMemoryCache();
-
-  benchmark('constructing an instance', done => {
-    new ApolloClient({ link, cache });
-    done();
-  });
-  end();
-});
-
-group(end => {
-  benchmark('fetching a query result from mocked server', done => {
-    const client = getClientInstance();
-    client.query({ query: simpleQuery }).then(_ => {
-      done();
-    });
-  });
-
-  end();
-});
-
-group(end => {
-  benchmark('write data and receive update from the cache', done => {
-    const client = getClientInstance();
-    const observable = client.watchQuery({
-      query: simpleQuery,
-      fetchPolicy: 'cache-only',
-    });
-    observable.subscribe({
-      next(res: ApolloQueryResult<Object>) {
-        if (Object.keys(res.data).length > 0) {
-          done();
-        }
-      },
-      error(_: Error) {
-        console.warn('Error occurred in observable.');
-      },
-    });
-    client.query({ query: simpleQuery });
-  });
-
-  end();
-});
-
-group(end => {
-  // This benchmark is supposed to check whether the time
-  // taken to deliver updates is linear in the number of subscribers or not.
-  // (Should be linear). When plotting the results from this benchmark,
-  // the `meanTimes` structure can be used.
-  const meanTimes: { [subscriberCount: string]: number } = {};
-
-  times(4, countR => {
-    const count = 5 * Math.pow(4, countR);
-    benchmark(
-      {
-        name: `write data and deliver update to ${count} subscribers`,
-        count,
-      },
-      done => {
-        const promises: Promise<void>[] = [];
-        const client = getClientInstance();
-
-        times(count, () => {
-          promises.push(
-            new Promise<void>((resolve, _) => {
-              client
-                .watchQuery({
-                  query: simpleQuery,
-                  fetchPolicy: 'cache-only',
-                })
-                .subscribe({
-                  next(res: ApolloQueryResult<Object>) {
-                    if (Object.keys(res.data).length > 0) {
-                      resolve();
-                    }
-                  },
-                });
-            }),
-          );
-        });
-
-        client.query({ query: simpleQuery });
-        Promise.all(promises).then(() => {
-          done();
-        });
-      },
-    );
-
-    afterEach((description: DescriptionObject, event: any) => {
-      const iterCount = description['count'] as number;
-      meanTimes[iterCount.toString()] = event.target.stats.mean * 1000;
-    });
-  });
-
-  end();
-});
-
-times(4, (countR: number) => {
-  const count = 5 * Math.pow(4, countR);
-  const query = gql`
-    query($id: String) {
-      author(id: $id) {
-        name
-        id
-        __typename
-      }
-    }
-  `;
-  const originalResult = {
-    data: {
-      author: {
-        name: 'John Smith',
-        id: 1,
-        __typename: 'Author',
-      },
-    },
-  };
-
-  group(end => {
-    const cache = new InMemoryCache({
-      dataIdFromObject: (obj: any) => {
-        if (obj.id && obj.__typename) {
-          return obj.__typename + obj.id;
-        }
-        return null;
-      },
-    });
-
-    // insert a bunch of stuff into the cache
-    times(count, index => {
-      const result = cloneDeep(originalResult);
-      result.data.author.id = index;
-
-      return cache.writeQuery({
-        query,
-        variables: { id: index },
-        data: result.data as any,
-      });
-    });
-
-    benchmark(
-      {
-        name: `read single item from cache with ${count} items in cache`,
-        count,
-      },
-      done => {
-        const randomIndex = Math.floor(Math.random() * count);
-        cache.readQuery({
-          query,
-          variables: { id: randomIndex },
-        });
-        done();
-      },
-    );
-
-    end();
-  });
-});
-
-// Measure the amount of time it takes to read a bunch of
-// objects from the cache.
-times(4, index => {
-  group(end => {
-    const cache = new InMemoryCache({
-      dataIdFromObject,
-      addTypename: false,
-    });
-
-    const query = gql`
-      query($id: String) {
-        house(id: $id) {
-          reservations {
-            name
-            id
-          }
-        }
-      }
-    `;
-    const houseId = '12';
-    const reservationCount = 5 * Math.pow(4, index);
-    const reservations = createReservations(reservationCount);
-
-    const variables = { id: houseId };
-
-    cache.writeQuery({
-      query,
-      variables,
-      data: {
-        house: {
-          reservations,
-        },
-      },
-    });
-
-    benchmark(
-      `read result with ${reservationCount} items associated with the result`,
-      done => {
-        cache.readQuery({
-          query,
-          variables,
-        });
-        done();
-      },
-    );
-
-    end();
-  });
-});
-
-// Measure only the amount of time it takes to diff a query against the store
-//
-// This test allows us to differentiate between the fixed cost of .query() and the fixed cost
-// of actually reading from the store.
-times(4, index => {
-  group(end => {
-    const reservationCount = 5 * Math.pow(4, index);
-
-    // Prime the cache.
-    const query = gql`
-      query($id: String) {
-        house(id: $id) {
-          reservations {
-            name
-            id
-          }
-        }
-      }
-    `;
-    const variables = { id: '7' };
-    const reservations = createReservations(reservationCount);
-    const result = {
-      house: { reservations },
-    };
-
-    const cache = new InMemoryCache({
-      dataIdFromObject,
-      addTypename: false,
-    });
-
-    cache.write({
-      dataId: 'ROOT_QUERY',
-      query,
-      variables,
-      result,
-    });
-
-    // We only keep track of the results so that V8 doesn't decide to just throw
-    // away our cache read code.
-    let results: any = null;
-    benchmark(
-      `diff query against store with ${reservationCount} items`,
-      done => {
-        results = cache.diff({
-          query,
-          variables,
-          optimistic: false,
-        });
-        done();
-      },
-    );
-
-    end();
-  });
-});
-
-if (process.env.DANGER_GITHUB_API_TOKEN) {
-  collectAndReportBenchmarks(true);
-} else {
-  collectAndReportBenchmarks(false);
-}
diff --git a/packages/apollo-client/benchmark/thresholds.ts b/packages/apollo-client/benchmark/thresholds.ts
deleted file mode 100644
--- a/packages/apollo-client/benchmark/thresholds.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-export const thresholds: { [name: string]: number } = {
-  'constructing an instance': 0.1,
-
-  'fetching a query result from mocked server': 14,
-
-  'write data and receive update from the cache': 14,
-  'write data and deliver update to 5 subscribers': 20,
-  'write data and deliver update to 10 subscribers': 25,
-  'write data and deliver update to 20 subscribers': 40,
-  'write data and deliver update to 40 subscribers': 62,
-  'write data and deliver update to 80 subscribers': 100,
-  'write data and deliver update to 160 subscribers': 165,
-  'write data and deliver update to 320 subscribers': 430,
-
-  'read single item from cache with 5 items in cache': 0.65 / 0.062,
-  'read single item from cache with 10 items in cache': 0.65 / 0.062,
-  'read single item from cache with 20 items in cache': 0.65 / 0.062,
-  'read single item from cache with 40 items in cache': 0.65 / 0.062,
-  'read single item from cache with 80 items in cache': 0.65 / 0.062,
-  'read single item from cache with 160 items in cache': 0.65 / 0.062,
-  'read single item from cache with 320 items in cache': 0.65 / 0.062,
-
-  'read result with 5 items associated with the result': 0.8 / 0.062,
-  'read result with 10 items associated with the result': 0.8 / 0.062,
-  'read result with 20 items associated with the result': 0.8 / 0.062,
-  'read result with 40 items associated with the result': 1 / 0.062,
-  'read result with 80 items associated with the result': 1.3 / 0.062,
-  'read result with 160 items associated with the result': 2.1 / 0.062,
-  'read result with 320 items associated with the result': 3.6 / 0.062,
-
-  'diff query against store with 5 items': 0.06 / 0.062,
-  'diff query against store with 10 items': 0.07 / 0.062,
-  'diff query against store with 20 items': 0.07 / 0.062,
-  'diff query against store with 40 items': 0.11 / 0.062,
-  'diff query against store with 80 items': 0.2 / 0.062,
-  'diff query against store with 160 items': 0.36 / 0.062,
-  'diff query against store with 320 items': 0.75 / 0.062,
-};
diff --git a/packages/apollo-client/benchmark/util.ts b/packages/apollo-client/benchmark/util.ts
deleted file mode 100644
--- a/packages/apollo-client/benchmark/util.ts
+++ /dev/null
@@ -1,173 +0,0 @@
-import Benchmark from 'benchmark';
-
-// This file implements utilities around benchmark.js that make it
-// easier to use for our benchmarking needs.
-
-// Specifically, it provides `group` and `benchmark`, examples of which
-// can be seen within the benchmarks.The functions allow you to manage scope and async
-// code more easily than benchmark.js typically allows.
-//
-// `group` is meant to provide a way to execute code that sets up the scope variables for your
-// benchmark. It is only run once before the benchmark, not on every call of the code to
-// be benchmarked. The `benchmark` function is similar to the `it` function within mocha;
-// it allows you to define a particular block of code to be benchmarked.
-
-Benchmark.options.minSamples = 150;
-export const bsuite = new Benchmark.Suite();
-export type DoneFunction = () => void;
-
-export interface DescriptionObject {
-  name: string;
-  [other: string]: any;
-}
-
-export type Nullable<T> = T | undefined;
-export type Description = DescriptionObject | string;
-export type CycleFunction = (doneFn: DoneFunction) => void;
-export type BenchmarkFunction = (
-  description: Description,
-  cycleFn: CycleFunction,
-) => void;
-export type GroupFunction = (done: DoneFunction) => void;
-export type AfterEachCallbackFunction = (
-  descr: Description,
-  event: any,
-) => void;
-export type AfterEachFunction = (
-  afterEachFnArg: AfterEachCallbackFunction,
-) => void;
-export type AfterAllCallbackFunction = () => void;
-export type AfterAllFunction = (afterAllFn: AfterAllCallbackFunction) => void;
-
-export let benchmark: BenchmarkFunction;
-export let afterEach: AfterEachFunction;
-export let afterAll: AfterAllFunction;
-
-// Used to log stuff within benchmarks without pissing off tslint.
-export function log(logString: string, ...args: any[]) {
-  // tslint:disable-next-line
-  console.log(logString, ...args);
-}
-
-// A reasonable implementation of dataIdFromObject that we use within
-// the benchmarks.
-export const dataIdFromObject = (object: any) => {
-  if (object.__typename && object.id) {
-    return object.__typename + '__' + object.id;
-  }
-  return null;
-};
-
-interface Scope {
-  benchmark?: BenchmarkFunction;
-  afterEach?: AfterEachFunction;
-  afterAll?: AfterAllFunction;
-}
-
-// Internal function that returns the current exposed functions
-// benchmark, setup, etc.
-function currentScope() {
-  return {
-    benchmark,
-    afterEach,
-    afterAll,
-  };
-}
-
-// Internal function that lets us set benchmark, setup, afterEach, etc.
-// in a reasonable fashion.
-function setScope(scope: Scope) {
-  benchmark = scope.benchmark as BenchmarkFunction;
-  afterEach = scope.afterEach as AfterEachFunction;
-  afterAll = scope.afterAll as AfterAllFunction;
-}
-
-export const groupPromises: Promise<void>[] = [];
-
-export const group = (groupFn: GroupFunction) => {
-  const oldScope = currentScope();
-  const scope: {
-    benchmark?: BenchmarkFunction;
-    afterEach?: AfterEachFunction;
-    afterAll?: AfterAllFunction;
-  } = {};
-
-  let afterEachFn: Nullable<AfterEachCallbackFunction> = undefined;
-  scope.afterEach = (afterEachFnArg: AfterEachCallbackFunction) => {
-    afterEachFn = afterEachFnArg;
-  };
-
-  let afterAllFn: Nullable<AfterAllCallbackFunction> = undefined;
-  scope.afterAll = (afterAllFnArg: AfterAllCallbackFunction) => {
-    afterAllFn = afterAllFnArg;
-  };
-
-  const benchmarkPromises: Promise<void>[] = [];
-
-  scope.benchmark = (
-    description: string | Description,
-    benchmarkFn: CycleFunction,
-  ) => {
-    const name =
-      (description as DescriptionObject).name || (description as string);
-    log('Adding benchmark: ', name);
-
-    // const scopes: Object[] = [];
-    let cycleCount = 0;
-    benchmarkPromises.push(
-      new Promise<void>((resolve, _) => {
-        bsuite.add(name, {
-          defer: true,
-          fn: (deferred: any) => {
-            const done = () => {
-              cycleCount++;
-              deferred.resolve();
-            };
-
-            benchmarkFn(done);
-          },
-
-          onComplete: (event: any) => {
-            if (afterEachFn) {
-              afterEachFn(description, event);
-            }
-            resolve();
-          },
-        });
-      }),
-    );
-  };
-
-  groupPromises.push(
-    new Promise<void>((resolve, _) => {
-      const groupDone = () => {
-        Promise.all(benchmarkPromises).then(() => {
-          if (afterAllFn) {
-            afterAllFn();
-          }
-        });
-        resolve();
-      };
-
-      setScope(scope);
-      groupFn(groupDone);
-      setScope(oldScope);
-    }),
-  );
-};
-
-export function runBenchmarks() {
-  Promise.all(groupPromises).then(() => {
-    log('Running benchmarks.');
-    bsuite
-      .on('error', (error: any) => {
-        log('Error: ', error);
-      })
-      .on('cycle', (event: any) => {
-        log('Mean time in ms: ', event.target.stats.mean * 1000);
-        log(String(event.target));
-        log('');
-      })
-      .run({ async: false });
-  });
-}
diff --git a/packages/apollo-client/jest.config.js b/packages/apollo-client/jest.config.js
deleted file mode 100644
--- a/packages/apollo-client/jest.config.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = {
-  ...require('../../config/jest.config.settings'),
-  // must be inside the src dir
-  setupFiles: ['<rootDir>/src/config/jest/setup.ts'],
-};
diff --git a/packages/apollo-client/rollup.config.js b/packages/apollo-client/rollup.config.js
deleted file mode 100644
--- a/packages/apollo-client/rollup.config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { rollup } from '../../config/rollup.config';
-
-export default rollup({
-  name: 'apollo-client',
-  extraGlobals: {
-    'symbol-observable': '$$observable',
-  },
-});
diff --git a/packages/apollo-client/src/__mocks__/mockLinks.ts b/packages/apollo-client/src/__mocks__/mockLinks.ts
deleted file mode 100644
--- a/packages/apollo-client/src/__mocks__/mockLinks.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-import {
-  Operation,
-  ApolloLink,
-  FetchResult,
-  Observable,
-  GraphQLRequest,
-} from 'apollo-link';
-
-import { print } from 'graphql/language/printer';
-
-interface MockApolloLink extends ApolloLink {
-  operation?: Operation;
-}
-
-// Pass in multiple mocked responses, so that you can test flows that end up
-// making multiple queries to the server
-export function mockSingleLink(
-  ...mockedResponses: MockedResponse[]
-): MockApolloLink {
-  return new MockLink(mockedResponses);
-}
-
-export function mockObservableLink(): MockSubscriptionLink {
-  return new MockSubscriptionLink();
-}
-
-export interface MockedResponse {
-  request: GraphQLRequest;
-  result?: FetchResult;
-  error?: Error;
-  delay?: number;
-}
-
-export interface MockedSubscriptionResult {
-  result?: FetchResult;
-  error?: Error;
-  delay?: number;
-}
-
-export interface MockedSubscription {
-  request: Operation;
-}
-
-export class MockLink extends ApolloLink {
-  public operation: Operation;
-  private mockedResponsesByKey: { [key: string]: MockedResponse[] } = {};
-
-  constructor(mockedResponses: MockedResponse[]) {
-    super();
-    mockedResponses.forEach(mockedResponse => {
-      this.addMockedResponse(mockedResponse);
-    });
-  }
-
-  public addMockedResponse(mockedResponse: MockedResponse) {
-    const key = requestToKey(mockedResponse.request);
-    let mockedResponses = this.mockedResponsesByKey[key];
-    if (!mockedResponses) {
-      mockedResponses = [];
-      this.mockedResponsesByKey[key] = mockedResponses;
-    }
-    mockedResponses.push(mockedResponse);
-  }
-
-  public request(operation: Operation) {
-    this.operation = operation;
-    const key = requestToKey(operation);
-    const responses = this.mockedResponsesByKey[key];
-    if (!responses || responses.length === 0) {
-      throw new Error(
-        `No more mocked responses for the query: ${print(
-          operation.query,
-        )}, variables: ${JSON.stringify(operation.variables)}`,
-      );
-    }
-
-    const { result, error, delay } = responses.shift()!;
-    if (!result && !error) {
-      throw new Error(
-        `Mocked response should contain either result or error: ${key}`,
-      );
-    }
-
-    return new Observable<FetchResult>(observer => {
-      let timer = setTimeout(
-        () => {
-          if (error) {
-            observer.error(error);
-          } else {
-            if (result) observer.next(result);
-            observer.complete();
-          }
-        },
-        delay ? delay : 0,
-      );
-
-      return () => {
-        clearTimeout(timer);
-      };
-    });
-  }
-}
-
-export class MockSubscriptionLink extends ApolloLink {
-  // private observer: Observer<any>;
-  private observer: any;
-  public unsubscribers: any[] = [];
-  public setups: any[] = [];
-
-  constructor() {
-    super();
-  }
-
-  public request() {
-    return new Observable<FetchResult>(observer => {
-      this.setups.forEach(x => x());
-      this.observer = observer;
-      return {
-        unsubscribe: () => {
-          this.unsubscribers.forEach(x => x());
-        },
-        closed: false,
-      };
-    });
-  }
-
-  public simulateResult(result: MockedSubscriptionResult) {
-    setTimeout(() => {
-      const { observer } = this;
-      if (!observer) throw new Error('subscription torn down');
-      if (result.result && observer.next) observer.next(result.result);
-      if (result.error && observer.error) observer.error(result.error);
-    }, result.delay || 0);
-  }
-
-  public simulateComplete() {
-    const { observer } = this;
-    if (!observer) throw new Error('subscription torn down');
-    if (observer.complete) observer.complete();
-  }
-
-  public onSetup(listener: any): void {
-    this.setups = this.setups.concat([listener]);
-  }
-
-  public onUnsubscribe(listener: any): void {
-    this.unsubscribers = this.unsubscribers.concat([listener]);
-  }
-}
-
-function requestToKey(request: GraphQLRequest): string {
-  const queryString = request.query && print(request.query);
-
-  return JSON.stringify({
-    variables: request.variables || {},
-    query: queryString,
-  });
-}
diff --git a/packages/apollo-client/src/__mocks__/mockQueryManager.ts b/packages/apollo-client/src/__mocks__/mockQueryManager.ts
deleted file mode 100644
--- a/packages/apollo-client/src/__mocks__/mockQueryManager.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { QueryManager } from '../core/QueryManager';
-
-import { mockSingleLink, MockedResponse } from './mockLinks';
-
-import { DataStore } from '../data/store';
-import { InMemoryCache } from 'apollo-cache-inmemory';
-
-// Helper method for the tests that construct a query manager out of a
-// a list of mocked responses for a mocked network interface.
-export default (...mockedResponses: MockedResponse[]) => {
-  return new QueryManager({
-    link: mockSingleLink(...mockedResponses),
-    store: new DataStore(new InMemoryCache({ addTypename: false })),
-  });
-};
diff --git a/packages/apollo-client/src/data/store.ts b/packages/apollo-client/src/data/store.ts
deleted file mode 100644
--- a/packages/apollo-client/src/data/store.ts
+++ /dev/null
@@ -1,205 +0,0 @@
-import { ExecutionResult, DocumentNode } from 'graphql';
-import { ApolloCache, Cache, DataProxy } from 'apollo-cache';
-
-import { QueryStoreValue } from '../data/queries';
-import {
-  getOperationName,
-  tryFunctionOrLogError,
-  graphQLResultHasError,
-} from 'apollo-utilities';
-import { MutationQueryReducer } from '../core/types';
-
-export type QueryWithUpdater = {
-  updater: MutationQueryReducer<Object>;
-  query: QueryStoreValue;
-};
-
-export interface DataWrite {
-  rootId: string;
-  result: any;
-  document: DocumentNode;
-  operationName: string | null;
-  variables: Object;
-}
-
-export class DataStore<TSerialized> {
-  private cache: ApolloCache<TSerialized>;
-
-  constructor(initialCache: ApolloCache<TSerialized>) {
-    this.cache = initialCache;
-  }
-
-  public getCache(): ApolloCache<TSerialized> {
-    return this.cache;
-  }
-
-  public markQueryResult(
-    result: ExecutionResult,
-    document: DocumentNode,
-    variables: any,
-    fetchMoreForQueryId: string | undefined,
-    ignoreErrors: boolean = false,
-  ) {
-    let writeWithErrors = !graphQLResultHasError(result);
-    if (ignoreErrors && graphQLResultHasError(result) && result.data) {
-      writeWithErrors = true;
-    }
-    if (!fetchMoreForQueryId && writeWithErrors) {
-      this.cache.write({
-        result: result.data,
-        dataId: 'ROOT_QUERY',
-        query: document,
-        variables: variables,
-      });
-    }
-  }
-
-  public markSubscriptionResult(
-    result: ExecutionResult,
-    document: DocumentNode,
-    variables: any,
-  ) {
-    // the subscription interface should handle not sending us results we no longer subscribe to.
-    // XXX I don't think we ever send in an object with errors, but we might in the future...
-    if (!graphQLResultHasError(result)) {
-      this.cache.write({
-        result: result.data,
-        dataId: 'ROOT_SUBSCRIPTION',
-        query: document,
-        variables: variables,
-      });
-    }
-  }
-
-  public markMutationInit(mutation: {
-    mutationId: string;
-    document: DocumentNode;
-    variables: any;
-    updateQueries: { [queryId: string]: QueryWithUpdater };
-    update: ((proxy: DataProxy, mutationResult: Object) => void) | undefined;
-    optimisticResponse: Object | Function | undefined;
-  }) {
-    if (mutation.optimisticResponse) {
-      let optimistic: Object;
-      if (typeof mutation.optimisticResponse === 'function') {
-        optimistic = mutation.optimisticResponse(mutation.variables);
-      } else {
-        optimistic = mutation.optimisticResponse;
-      }
-
-      this.cache.recordOptimisticTransaction(c => {
-        const orig = this.cache;
-        this.cache = c;
-
-        try {
-          this.markMutationResult({
-            mutationId: mutation.mutationId,
-            result: { data: optimistic },
-            document: mutation.document,
-            variables: mutation.variables,
-            updateQueries: mutation.updateQueries,
-            update: mutation.update,
-          });
-        } finally {
-          this.cache = orig;
-        }
-      }, mutation.mutationId);
-    }
-  }
-
-  public markMutationResult(mutation: {
-    mutationId: string;
-    result: ExecutionResult;
-    document: DocumentNode;
-    variables: any;
-    updateQueries: { [queryId: string]: QueryWithUpdater };
-    update: ((proxy: DataProxy, mutationResult: Object) => void) | undefined;
-  }) {
-    // Incorporate the result from this mutation into the store
-    if (!graphQLResultHasError(mutation.result)) {
-      const cacheWrites: Cache.WriteOptions[] = [{
-        result: mutation.result.data,
-        dataId: 'ROOT_MUTATION',
-        query: mutation.document,
-        variables: mutation.variables,
-      }];
-
-      const { updateQueries } = mutation;
-      if (updateQueries) {
-        Object.keys(updateQueries).forEach(id => {
-          const { query, updater } = updateQueries[id];
-
-          // Read the current query result from the store.
-          const { result: currentQueryResult, complete } = this.cache.diff({
-            query: query.document,
-            variables: query.variables,
-            returnPartialData: true,
-            optimistic: false,
-          });
-
-          if (complete) {
-            // Run our reducer using the current query result and the mutation result.
-            const nextQueryResult = tryFunctionOrLogError(() =>
-              updater(currentQueryResult, {
-                mutationResult: mutation.result,
-                queryName: getOperationName(query.document) || undefined,
-                queryVariables: query.variables,
-              }),
-            );
-
-            // Write the modified result back into the store if we got a new result.
-            if (nextQueryResult) {
-              cacheWrites.push({
-                result: nextQueryResult,
-                dataId: 'ROOT_QUERY',
-                query: query.document,
-                variables: query.variables,
-              });
-            }
-          }
-        });
-      }
-
-      this.cache.performTransaction(c => {
-        cacheWrites.forEach(write => c.write(write));
-
-        // If the mutation has some writes associated with it then we need to
-        // apply those writes to the store by running this reducer again with a
-        // write action.
-        const { update } = mutation;
-        if (update) {
-          tryFunctionOrLogError(() => update(c, mutation.result));
-        }
-      });
-    }
-  }
-
-  public markMutationComplete({
-    mutationId,
-    optimisticResponse,
-  }: {
-    mutationId: string;
-    optimisticResponse?: any;
-  }) {
-    if (optimisticResponse) {
-      this.cache.removeOptimistic(mutationId);
-    }
-  }
-
-  public markUpdateQueryResult(
-    document: DocumentNode,
-    variables: any,
-    newResult: any,
-  ) {
-    this.cache.write({
-      result: newResult,
-      dataId: 'ROOT_QUERY',
-      variables,
-      query: document,
-    });
-  }
-
-  public reset(): Promise<void> {
-    return this.cache.reset();
-  }
-}
diff --git a/packages/apollo-client/src/index.ts b/packages/apollo-client/src/index.ts
deleted file mode 100644
--- a/packages/apollo-client/src/index.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-export {
-  ObservableQuery,
-  FetchMoreOptions,
-  UpdateQueryOptions,
-  ApolloCurrentResult,
-  ApolloCurrentQueryResult,
-} from './core/ObservableQuery';
-export {
-  QueryBaseOptions,
-  QueryOptions,
-  WatchQueryOptions,
-  MutationOptions,
-  SubscriptionOptions,
-  FetchPolicy,
-  WatchQueryFetchPolicy,
-  ErrorPolicy,
-  FetchMoreQueryOptions,
-  SubscribeToMoreOptions,
-  MutationUpdaterFn,
-} from './core/watchQueryOptions';
-export { NetworkStatus } from './core/networkStatus';
-export * from './core/types';
-export {
-  Resolver,
-  FragmentMatcher as LocalStateFragmentMatcher,
-} from './core/LocalState';
-
-export { isApolloError, ApolloError } from './errors/ApolloError';
-
-import ApolloClient, {
-  ApolloClientOptions,
-  DefaultOptions,
-} from './ApolloClient';
-export { ApolloClientOptions, DefaultOptions };
-
-// Export the client as both default and named.
-export { ApolloClient };
-export default ApolloClient;
diff --git a/packages/apollo-client/src/util/Observable.ts b/packages/apollo-client/src/util/Observable.ts
deleted file mode 100644
--- a/packages/apollo-client/src/util/Observable.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-// This simplified polyfill attempts to follow the ECMAScript Observable proposal.
-// See https://github.com/zenparsing/es-observable
-import { Observable as LinkObservable } from 'apollo-link';
-
-export type Subscription = ZenObservable.Subscription;
-export type Observer<T> = ZenObservable.Observer<T>;
-
-import $$observable from 'symbol-observable';
-
-// rxjs interopt
-export class Observable<T> extends LinkObservable<T> {
-  public [$$observable]() {
-    return this;
-  }
-
-  public ['@@observable' as any]() {
-    return this;
-  }
-}
diff --git a/packages/apollo-client/src/util/capitalizeFirstLetter.ts b/packages/apollo-client/src/util/capitalizeFirstLetter.ts
deleted file mode 100644
--- a/packages/apollo-client/src/util/capitalizeFirstLetter.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function capitalizeFirstLetter(str: string) {
-  return str.charAt(0).toUpperCase() + str.slice(1);
-}
diff --git a/packages/apollo-utilities/jest.config.js b/packages/apollo-utilities/jest.config.js
deleted file mode 100644
--- a/packages/apollo-utilities/jest.config.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
-  ...require('../../config/jest.config.settings'),
-};
diff --git a/packages/apollo-utilities/rollup.config.js b/packages/apollo-utilities/rollup.config.js
deleted file mode 100644
--- a/packages/apollo-utilities/rollup.config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { rollup } from '../../config/rollup.config';
-
-export default rollup({
-  name: 'apollo-utilities',
-  extraGlobals: {
-    'fast-json-stable-stringify': 'stringify',
-    '@wry/equality': 'wryEquality',
-  },
-});
diff --git a/packages/apollo-utilities/src/declarations.d.ts b/packages/apollo-utilities/src/declarations.d.ts
deleted file mode 100644
--- a/packages/apollo-utilities/src/declarations.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-declare module 'fast-json-stable-stringify';
diff --git a/packages/apollo-utilities/src/index.ts b/packages/apollo-utilities/src/index.ts
deleted file mode 100644
--- a/packages/apollo-utilities/src/index.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-export * from './directives';
-export * from './fragments';
-export * from './getFromAST';
-export * from './transform';
-export * from './storeUtils';
-export * from './util/assign';
-export * from './util/canUse';
-export * from './util/cloneDeep';
-export * from './util/environment';
-export * from './util/errorHandling';
-export * from './util/isEqual';
-export * from './util/maybeDeepFreeze';
-export * from './util/mergeDeep';
-export * from './util/warnOnce';
-export * from './util/stripSymbols';
-export * from './util/mergeDeep';
diff --git a/packages/apollo-utilities/src/util/isEqual.ts b/packages/apollo-utilities/src/util/isEqual.ts
deleted file mode 100644
--- a/packages/apollo-utilities/src/util/isEqual.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { equal as isEqual } from '@wry/equality';
diff --git a/packages/apollo-utilities/src/util/maybeDeepFreeze.ts b/packages/apollo-utilities/src/util/maybeDeepFreeze.ts
deleted file mode 100644
--- a/packages/apollo-utilities/src/util/maybeDeepFreeze.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { isDevelopment, isTest } from './environment';
-
-// Taken (mostly) from https://github.com/substack/deep-freeze to avoid
-// import hassles with rollup.
-function deepFreeze(o: any) {
-  Object.freeze(o);
-
-  Object.getOwnPropertyNames(o).forEach(function(prop) {
-    if (
-      o[prop] !== null &&
-      (typeof o[prop] === 'object' || typeof o[prop] === 'function') &&
-      !Object.isFrozen(o[prop])
-    ) {
-      deepFreeze(o[prop]);
-    }
-  });
-
-  return o;
-}
-
-export function maybeDeepFreeze(obj: any) {
-  if (isDevelopment() || isTest()) {
-    // Polyfilled Symbols potentially cause infinite / very deep recursion while deep freezing
-    // which is known to crash IE11 (https://github.com/apollographql/apollo-client/issues/3043).
-    const symbolIsPolyfilled =
-      typeof Symbol === 'function' && typeof Symbol('') === 'string';
-
-    if (!symbolIsPolyfilled) {
-      return deepFreeze(obj);
-    }
-  }
-  return obj;
-}
diff --git a/packages/apollo-utilities/src/util/mergeDeep.ts b/packages/apollo-utilities/src/util/mergeDeep.ts
deleted file mode 100644
--- a/packages/apollo-utilities/src/util/mergeDeep.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-const { hasOwnProperty } = Object.prototype;
-
-// These mergeDeep and mergeDeepArray utilities merge any number of objects
-// together, sharing as much memory as possible with the source objects, while
-// remaining careful to avoid modifying any source objects.
-
-// Logically, the return type of mergeDeep should be the intersection of
-// all the argument types. The binary call signature is by far the most
-// common, but we support 0- through 5-ary as well. After that, the
-// resulting type is just the inferred array element type. Note to nerds:
-// there is a more clever way of doing this that converts the tuple type
-// first to a union type (easy enough: T[number]) and then converts the
-// union to an intersection type using distributive conditional type
-// inference, but that approach has several fatal flaws (boolean becomes
-// true & false, and the inferred type ends up as unknown in many cases),
-// in addition to being nearly impossible to explain/understand.
-export type TupleToIntersection<T extends any[]> =
-  T extends [infer A] ? A :
-  T extends [infer A, infer B] ? A & B :
-  T extends [infer A, infer B, infer C] ? A & B & C :
-  T extends [infer A, infer B, infer C, infer D] ? A & B & C & D :
-  T extends [infer A, infer B, infer C, infer D, infer E] ? A & B & C & D & E :
-  T extends (infer U)[] ? U : any;
-
-export function mergeDeep<T extends any[]>(
-  ...sources: T
-): TupleToIntersection<T> {
-  return mergeDeepArray(sources);
-}
-
-// In almost any situation where you could succeed in getting the
-// TypeScript compiler to infer a tuple type for the sources array, you
-// could just use mergeDeep instead of mergeDeepArray, so instead of
-// trying to convert T[] to an intersection type we just infer the array
-// element type, which works perfectly when the sources array has a
-// consistent element type.
-export function mergeDeepArray<T>(sources: T[]): T {
-  let target = sources[0] || {} as T;
-  const count = sources.length;
-  if (count > 1) {
-    const pastCopies: any[] = [];
-    target = shallowCopyForMerge(target, pastCopies);
-    for (let i = 1; i < count; ++i) {
-      target = mergeHelper(target, sources[i], pastCopies);
-    }
-  }
-  return target;
-}
-
-function isObject(obj: any): obj is Record<string | number, any> {
-  return obj !== null && typeof obj === 'object';
-}
-
-function mergeHelper(
-  target: any,
-  source: any,
-  pastCopies: any[],
-) {
-  if (isObject(source) && isObject(target)) {
-    // In case the target has been frozen, make an extensible copy so that
-    // we can merge properties into the copy.
-    if (Object.isExtensible && !Object.isExtensible(target)) {
-      target = shallowCopyForMerge(target, pastCopies);
-    }
-
-    Object.keys(source).forEach(sourceKey => {
-      const sourceValue = source[sourceKey];
-      if (hasOwnProperty.call(target, sourceKey)) {
-        const targetValue = target[sourceKey];
-        if (sourceValue !== targetValue) {
-          // When there is a key collision, we need to make a shallow copy of
-          // target[sourceKey] so the merge does not modify any source objects.
-          // To avoid making unnecessary copies, we use a simple array to track
-          // past copies, since it's safe to modify copies created earlier in
-          // the merge. We use an array for pastCopies instead of a Map or Set,
-          // since the number of copies should be relatively small, and some
-          // Map/Set polyfills modify their keys.
-          target[sourceKey] = mergeHelper(
-            shallowCopyForMerge(targetValue, pastCopies),
-            sourceValue,
-            pastCopies,
-          );
-        }
-      } else {
-        // If there is no collision, the target can safely share memory with
-        // the source, and the recursion can terminate here.
-        target[sourceKey] = sourceValue;
-      }
-    });
-
-    return target;
-  }
-
-  // If source (or target) is not an object, let source replace target.
-  return source;
-}
-
-function shallowCopyForMerge<T>(value: T, pastCopies: any[]): T {
-  if (
-    value !== null &&
-    typeof value === 'object' &&
-    pastCopies.indexOf(value) < 0
-  ) {
-    if (Array.isArray(value)) {
-      value = (value as any).slice(0);
-    } else {
-      value = {
-        __proto__: Object.getPrototypeOf(value),
-        ...value,
-      };
-    }
-    pastCopies.push(value);
-  }
-  return value;
-}
diff --git a/packages/apollo-utilities/src/util/warnOnce.ts b/packages/apollo-utilities/src/util/warnOnce.ts
deleted file mode 100644
--- a/packages/apollo-utilities/src/util/warnOnce.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { isProduction, isTest } from './environment';
-
-const haveWarned = Object.create({});
-
-/**
- * Print a warning only once in development.
- * In production no warnings are printed.
- * In test all warnings are printed.
- *
- * @param msg The warning message
- * @param type warn or error (will call console.warn or console.error)
- */
-export function warnOnceInDevelopment(msg: string, type = 'warn') {
-  if (!isProduction() && !haveWarned[msg]) {
-    if (!isTest()) {
-      haveWarned[msg] = true;
-    }
-    if (type === 'error') {
-      console.error(msg);
-    } else {
-      console.warn(msg);
-    }
-  }
-}
diff --git a/packages/graphql-anywhere/jest.config.js b/packages/graphql-anywhere/jest.config.js
deleted file mode 100644
--- a/packages/graphql-anywhere/jest.config.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
-  ...require('../../config/jest.config.settings'),
-};
diff --git a/packages/graphql-anywhere/rollup.config.js b/packages/graphql-anywhere/rollup.config.js
deleted file mode 100644
--- a/packages/graphql-anywhere/rollup.config.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { rollup } from '../../config/rollup.config';
-
-export default [
-  ...rollup({ name: 'graphql-anywhere' }),
-];
diff --git a/packages/graphql-anywhere/src/async.ts b/packages/graphql-anywhere/src/async.ts
deleted file mode 100644
--- a/packages/graphql-anywhere/src/async.ts
+++ /dev/null
@@ -1,202 +0,0 @@
-import {
-  DocumentNode,
-  SelectionSetNode,
-  FieldNode,
-  FragmentDefinitionNode,
-  InlineFragmentNode,
-} from 'graphql';
-
-import {
-  getMainDefinition,
-  getFragmentDefinitions,
-  createFragmentMap,
-  shouldInclude,
-  getDirectiveInfoFromField,
-  isField,
-  isInlineFragment,
-  resultKeyNameFromField,
-  argumentsObjectFromField,
-} from 'apollo-utilities';
-
-import {
-  merge,
-  Resolver,
-  VariableMap,
-  ExecContext,
-  ExecInfo,
-  ExecOptions,
-} from './graphql';
-
-/* Based on graphql function from graphql-js:
- *
- * graphql(
- *   schema: GraphQLSchema,
- *   requestString: string,
- *   rootValue?: ?any,
- *   contextValue?: ?any,
- *   variableValues?: ?{[key: string]: any},
- *   operationName?: ?string
- * ): Promise<GraphQLResult>
- *
- * The default export as of graphql-anywhere is sync as of 4.0,
- * but below is an exported alternative that is async.
- * In the 5.0 version, this will be the only export again
- * and it will be async
- */
-export function graphql(
-  resolver: Resolver,
-  document: DocumentNode,
-  rootValue?: any,
-  contextValue?: any,
-  variableValues?: VariableMap,
-  execOptions: ExecOptions = {},
-): Promise<null | Object> {
-  const mainDefinition = getMainDefinition(document);
-
-  const fragments = getFragmentDefinitions(document);
-  const fragmentMap = createFragmentMap(fragments);
-
-  const resultMapper = execOptions.resultMapper;
-
-  // Default matcher always matches all fragments
-  const fragmentMatcher = execOptions.fragmentMatcher || (() => true);
-
-  const execContext: ExecContext = {
-    fragmentMap,
-    contextValue,
-    variableValues,
-    resultMapper,
-    resolver,
-    fragmentMatcher,
-  };
-
-  return executeSelectionSet(
-    mainDefinition.selectionSet,
-    rootValue,
-    execContext,
-  );
-}
-
-async function executeSelectionSet(
-  selectionSet: SelectionSetNode,
-  rootValue: any,
-  execContext: ExecContext,
-) {
-  const { fragmentMap, contextValue, variableValues: variables } = execContext;
-
-  const result = {};
-
-  const execute = async selection => {
-    if (!shouldInclude(selection, variables)) {
-      // Skip this entirely
-      return;
-    }
-
-    if (isField(selection)) {
-      const fieldResult = await executeField(selection, rootValue, execContext);
-
-      const resultFieldKey = resultKeyNameFromField(selection);
-
-      if (fieldResult !== undefined) {
-        if (result[resultFieldKey] === undefined) {
-          result[resultFieldKey] = fieldResult;
-        } else {
-          merge(result[resultFieldKey], fieldResult);
-        }
-      }
-
-      return;
-    }
-
-    let fragment: InlineFragmentNode | FragmentDefinitionNode;
-
-    if (isInlineFragment(selection)) {
-      fragment = selection;
-    } else {
-      // This is a named fragment
-      fragment = fragmentMap[selection.name.value];
-
-      if (!fragment) {
-        throw new Error(`No fragment named ${selection.name.value}`);
-      }
-    }
-
-    const typeCondition = fragment.typeCondition.name.value;
-
-    if (execContext.fragmentMatcher(rootValue, typeCondition, contextValue)) {
-      const fragmentResult = await executeSelectionSet(
-        fragment.selectionSet,
-        rootValue,
-        execContext,
-      );
-
-      merge(result, fragmentResult);
-    }
-  };
-
-  await Promise.all(selectionSet.selections.map(execute));
-
-  if (execContext.resultMapper) {
-    return execContext.resultMapper(result, rootValue);
-  }
-
-  return result;
-}
-
-async function executeField(
-  field: FieldNode,
-  rootValue: any,
-  execContext: ExecContext,
-): Promise<null | Object> {
-  const { variableValues: variables, contextValue, resolver } = execContext;
-
-  const fieldName = field.name.value;
-  const args = argumentsObjectFromField(field, variables);
-
-  const info: ExecInfo = {
-    isLeaf: !field.selectionSet,
-    resultKey: resultKeyNameFromField(field),
-    directives: getDirectiveInfoFromField(field, variables),
-    field,
-  };
-
-  const result = await resolver(fieldName, rootValue, args, contextValue, info);
-
-  // Handle all scalar types here
-  if (!field.selectionSet) {
-    return result;
-  }
-
-  // From here down, the field has a selection set, which means it's trying to
-  // query a GraphQLObjectType
-  if (result == null) {
-    // Basically any field in a GraphQL response can be null, or missing
-    return result;
-  }
-
-  if (Array.isArray(result)) {
-    return executeSubSelectedArray(field, result, execContext);
-  }
-
-  // Returned value is an object, and the query has a sub-selection. Recurse.
-  return executeSelectionSet(field.selectionSet, result, execContext);
-}
-
-function executeSubSelectedArray(field, result, execContext) {
-  return Promise.all(
-    result.map(item => {
-      // null value in array
-      if (item === null) {
-        return null;
-      }
-
-      // This is a nested array, recurse
-      if (Array.isArray(item)) {
-        return executeSubSelectedArray(field, item, execContext);
-      }
-
-      // This is an object, run the selection set on it
-      return executeSelectionSet(field.selectionSet, item, execContext);
-    }),
-  );
-}
diff --git a/packages/graphql-anywhere/src/graphql.ts b/packages/graphql-anywhere/src/graphql.ts
deleted file mode 100644
--- a/packages/graphql-anywhere/src/graphql.ts
+++ /dev/null
@@ -1,245 +0,0 @@
-import {
-  DocumentNode,
-  SelectionSetNode,
-  FieldNode,
-  FragmentDefinitionNode,
-  InlineFragmentNode,
-} from 'graphql';
-
-import {
-  getMainDefinition,
-  getFragmentDefinitions,
-  createFragmentMap,
-  FragmentMap,
-  DirectiveInfo,
-  shouldInclude,
-  getDirectiveInfoFromField,
-  isField,
-  isInlineFragment,
-  resultKeyNameFromField,
-  argumentsObjectFromField,
-} from 'apollo-utilities';
-
-export type Resolver = (
-  fieldName: string,
-  rootValue: any,
-  args: any,
-  context: any,
-  info: ExecInfo,
-) => any;
-
-export type VariableMap = { [name: string]: any };
-
-export type ResultMapper = (
-  values: { [fieldName: string]: any },
-  rootValue: any,
-) => any;
-export type FragmentMatcher = (
-  rootValue: any,
-  typeCondition: string,
-  context: any,
-) => boolean;
-
-export type ExecContext = {
-  fragmentMap: FragmentMap;
-  contextValue: any;
-  variableValues: VariableMap;
-  resultMapper: ResultMapper;
-  resolver: Resolver;
-  fragmentMatcher: FragmentMatcher;
-};
-
-export type ExecInfo = {
-  isLeaf: boolean;
-  resultKey: string;
-  directives: DirectiveInfo;
-  field: FieldNode;
-};
-
-export type ExecOptions = {
-  resultMapper?: ResultMapper;
-  fragmentMatcher?: FragmentMatcher;
-};
-
-/* Based on graphql function from graphql-js:
- *
- * graphql(
- *   schema: GraphQLSchema,
- *   requestString: string,
- *   rootValue?: ?any,
- *   contextValue?: ?any,
- *   variableValues?: ?{[key: string]: any},
- *   operationName?: ?string
- * ): Promise<GraphQLResult>
- *
- * The default export as of graphql-anywhere is sync as of 4.0,
- * but below is an exported alternative that is async.
- * In the 5.0 version, this will be the only export again
- * and it will be async
- */
-export function graphql(
-  resolver: Resolver,
-  document: DocumentNode,
-  rootValue?: any,
-  contextValue?: any,
-  variableValues: VariableMap = {},
-  execOptions: ExecOptions = {},
-) {
-  const mainDefinition = getMainDefinition(document);
-
-  const fragments = getFragmentDefinitions(document);
-  const fragmentMap = createFragmentMap(fragments);
-
-  const resultMapper = execOptions.resultMapper;
-
-  // Default matcher always matches all fragments
-  const fragmentMatcher = execOptions.fragmentMatcher || (() => true);
-
-  const execContext: ExecContext = {
-    fragmentMap,
-    contextValue,
-    variableValues,
-    resultMapper,
-    resolver,
-    fragmentMatcher,
-  };
-
-  return executeSelectionSet(
-    mainDefinition.selectionSet,
-    rootValue,
-    execContext,
-  );
-}
-
-function executeSelectionSet(
-  selectionSet: SelectionSetNode,
-  rootValue: any,
-  execContext: ExecContext,
-) {
-  const { fragmentMap, contextValue, variableValues: variables } = execContext;
-
-  const result = {};
-
-  selectionSet.selections.forEach(selection => {
-    if (variables && !shouldInclude(selection, variables)) {
-      // Skip selection sets which we're able to determine should not be run
-      return;
-    }
-
-    if (isField(selection)) {
-      const fieldResult = executeField(selection, rootValue, execContext);
-
-      const resultFieldKey = resultKeyNameFromField(selection);
-
-      if (fieldResult !== undefined) {
-        if (result[resultFieldKey] === undefined) {
-          result[resultFieldKey] = fieldResult;
-        } else {
-          merge(result[resultFieldKey], fieldResult);
-        }
-      }
-    } else {
-      let fragment: InlineFragmentNode | FragmentDefinitionNode;
-
-      if (isInlineFragment(selection)) {
-        fragment = selection;
-      } else {
-        // This is a named fragment
-        fragment = fragmentMap[selection.name.value];
-
-        if (!fragment) {
-          throw new Error(`No fragment named ${selection.name.value}`);
-        }
-      }
-
-      const typeCondition = fragment.typeCondition.name.value;
-
-      if (execContext.fragmentMatcher(rootValue, typeCondition, contextValue)) {
-        const fragmentResult = executeSelectionSet(
-          fragment.selectionSet,
-          rootValue,
-          execContext,
-        );
-
-        merge(result, fragmentResult);
-      }
-    }
-  });
-
-  if (execContext.resultMapper) {
-    return execContext.resultMapper(result, rootValue);
-  }
-
-  return result;
-}
-
-function executeField(
-  field: FieldNode,
-  rootValue: any,
-  execContext: ExecContext,
-): any {
-  const { variableValues: variables, contextValue, resolver } = execContext;
-
-  const fieldName = field.name.value;
-  const args = argumentsObjectFromField(field, variables);
-
-  const info: ExecInfo = {
-    isLeaf: !field.selectionSet,
-    resultKey: resultKeyNameFromField(field),
-    directives: getDirectiveInfoFromField(field, variables),
-    field,
-  };
-
-  const result = resolver(fieldName, rootValue, args, contextValue, info);
-
-  // Handle all scalar types here
-  if (!field.selectionSet) {
-    return result;
-  }
-
-  // From here down, the field has a selection set, which means it's trying to
-  // query a GraphQLObjectType
-  if (result == null) {
-    // Basically any field in a GraphQL response can be null, or missing
-    return result;
-  }
-
-  if (Array.isArray(result)) {
-    return executeSubSelectedArray(field, result, execContext);
-  }
-
-  // Returned value is an object, and the query has a sub-selection. Recurse.
-  return executeSelectionSet(field.selectionSet, result, execContext);
-}
-
-function executeSubSelectedArray(field, result, execContext) {
-  return result.map(item => {
-    // null value in array
-    if (item === null) {
-      return null;
-    }
-
-    // This is a nested array, recurse
-    if (Array.isArray(item)) {
-      return executeSubSelectedArray(field, item, execContext);
-    }
-
-    // This is an object, run the selection set on it
-    return executeSelectionSet(field.selectionSet, item, execContext);
-  });
-}
-
-const hasOwn = Object.prototype.hasOwnProperty;
-
-export function merge(dest, src) {
-  if (src !== null && typeof src === 'object') {
-    Object.keys(src).forEach(key => {
-      const srcVal = src[key];
-      if (!hasOwn.call(dest, key)) {
-        dest[key] = srcVal;
-      } else {
-        merge(dest[key], srcVal);
-      }
-    });
-  }
-}
diff --git a/packages/graphql-anywhere/src/index.ts b/packages/graphql-anywhere/src/index.ts
deleted file mode 100644
--- a/packages/graphql-anywhere/src/index.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-export { filter, check, propType } from './utilities';
-
-export {
-  Resolver,
-  VariableMap,
-  ResultMapper,
-  FragmentMatcher,
-  ExecContext,
-  ExecInfo,
-  ExecOptions,
-} from './graphql';
-
-import { graphql } from './graphql';
-export default graphql;
diff --git a/packages/graphql-anywhere/src/utilities.ts b/packages/graphql-anywhere/src/utilities.ts
deleted file mode 100644
--- a/packages/graphql-anywhere/src/utilities.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-import { DocumentNode, DirectiveNode } from 'graphql';
-
-import { getInclusionDirectives } from 'apollo-utilities';
-
-import { graphql, VariableMap, ExecInfo, ExecContext } from './graphql';
-
-import { invariant } from 'ts-invariant';
-
-const { hasOwnProperty } = Object.prototype;
-
-export function filter<FD = any, D extends FD = any>(
-  doc: DocumentNode,
-  data: D,
-  variableValues: VariableMap = {},
-): FD {
-  if (data === null) return data;
-
-  const resolver = (
-    fieldName: string,
-    root: any,
-    args: Object,
-    context: ExecContext,
-    info: ExecInfo,
-  ) => {
-    return root[info.resultKey];
-  };
-
-  return Array.isArray(data)
-    ? data.map(dataObj => graphql(resolver, doc, dataObj, null, variableValues))
-    : graphql(resolver, doc, data, null, variableValues);
-}
-
-// TODO: we should probably make check call propType and then throw,
-// rather than the other way round, to avoid constructing stack traces
-// for things like oneOf uses in React. At this stage I doubt many people
-// are using this like that, but in the future, who knows?
-export function check(
-  doc: DocumentNode,
-  data: any,
-  variables: VariableMap = {},
-): void {
-  const resolver = (
-    fieldName: string,
-    root: any,
-    args: any,
-    context: any,
-    info: any,
-  ) => {
-    // When variables is null, fields with @include/skip directives that
-    // reference variables are considered optional.
-    invariant(
-      hasOwnProperty.call(root, info.resultKey) ||
-        (!variables && hasVariableInclusions(info.field.directives)),
-      `${info.resultKey} missing on ${JSON.stringify(root)}`,
-    );
-    return root[info.resultKey];
-  };
-
-  graphql(resolver, doc, data, {}, variables, {
-    fragmentMatcher: () => false,
-  });
-}
-
-function hasVariableInclusions(
-  directives: ReadonlyArray<DirectiveNode>,
-): boolean {
-  return getInclusionDirectives(directives).some(
-    ({ ifArgument }) =>
-      ifArgument.value && ifArgument.value.kind === 'Variable',
-  );
-}
-
-// Lifted/adapted from
-//   https://github.com/facebook/react/blob/master/src/isomorphic/classic/types/ReactPropTypes.js
-const ANONYMOUS = '<<anonymous>>';
-function PropTypeError(message) {
-  this.message = message;
-  this.stack = '';
-}
-// Make `instanceof Error` still work for returned errors.
-PropTypeError.prototype = Error.prototype;
-
-const reactPropTypeLocationNames = {
-  prop: 'prop',
-  context: 'context',
-  childContext: 'child context',
-};
-
-function createChainableTypeChecker(validate) {
-  function checkType(
-    isRequired,
-    props,
-    propName,
-    componentName,
-    location,
-    propFullName,
-  ) {
-    componentName = componentName || ANONYMOUS;
-    propFullName = propFullName || propName;
-    if (props[propName] == null) {
-      const locationName = reactPropTypeLocationNames[location];
-      if (isRequired) {
-        if (props[propName] === null) {
-          return new PropTypeError(
-            `The ${locationName} \`${propFullName}\` is marked as required ` +
-              `in \`${componentName}\`, but its value is \`null\`.`,
-          );
-        }
-        return new PropTypeError(
-          `The ${locationName} \`${propFullName}\` is marked as required in ` +
-            `\`${componentName}\`, but its value is \`undefined\`.`,
-        );
-      }
-      return null;
-    } else {
-      return validate(props, propName, componentName, location, propFullName);
-    }
-  }
-
-  const chainedCheckType = checkType.bind(null, false);
-  chainedCheckType.isRequired = checkType.bind(null, true);
-
-  return chainedCheckType;
-}
-
-export function propType(
-  doc: DocumentNode,
-  mapPropsToVariables = props => null,
-) {
-  return createChainableTypeChecker((props, propName) => {
-    const prop = props[propName];
-    try {
-      if (!prop.loading) {
-        check(doc, prop, mapPropsToVariables(props));
-      }
-      return null;
-    } catch (e) {
-      // Need a much better error.
-      // Also we aren't checking for extra fields
-      return e;
-    }
-  });
-}
diff --git a/packages/apollo-client/src/ApolloClient.ts b/src/ApolloClient.ts
similarity index 93%
rename from packages/apollo-client/src/ApolloClient.ts
rename to src/ApolloClient.ts
--- a/packages/apollo-client/src/ApolloClient.ts
+++ b/src/ApolloClient.ts
@@ -1,14 +1,11 @@
-import {
-  ApolloLink,
-  FetchResult,
-  GraphQLRequest,
-  execute,
-} from 'apollo-link';
 import { ExecutionResult, DocumentNode } from 'graphql';
-import { ApolloCache, DataProxy } from 'apollo-cache';
-
 import { invariant, InvariantError } from 'ts-invariant';
 
+import { ApolloLink } from './link/core/ApolloLink';
+import { FetchResult, GraphQLRequest } from './link/core/types';
+import { execute } from './link/core/execute';
+import { ApolloCache } from './cache/core/cache';
+import { DataProxy } from './cache/core/types/DataProxy';
 import { QueryManager } from './core/QueryManager';
 import {
   ApolloQueryResult,
@@ -17,8 +14,7 @@ import {
 } from './core/types';
 import { ObservableQuery } from './core/ObservableQuery';
 import { LocalState, FragmentMatcher } from './core/LocalState';
-import { Observable } from './util/Observable';
-
+import { Observable } from './utilities/observables/Observable';
 import {
   QueryOptions,
   WatchQueryOptions,
@@ -26,10 +22,9 @@ import {
   MutationOptions,
   WatchQueryFetchPolicy,
 } from './core/watchQueryOptions';
-
-import { DataStore } from './data/store';
-
 import { version } from './version';
+import { HttpLink } from './link/http/HttpLink';
+import { UriFunction } from './link/http/selectHttpOptionsAndBody';
 
 export interface DefaultOptions {
   watchQuery?: Partial<WatchQueryOptions>;
@@ -40,6 +35,9 @@ export interface DefaultOptions {
 let hasSuggestedDevtools = false;
 
 export type ApolloClientOptions<TCacheShape> = {
+  uri?: string | UriFunction;
+  credentials?: string;
+  headers?: Record<string, string>;
   link?: ApolloLink;
   cache: ApolloCache<TCacheShape>;
   ssrForceFetchDelay?: number;
@@ -61,17 +59,16 @@ export type ApolloClientOptions<TCacheShape> = {
  * receive results from the server and cache the results in a store. It also delivers updates
  * to GraphQL queries through {@link Observable} instances.
  */
-export default class ApolloClient<TCacheShape> implements DataProxy {
+export class ApolloClient<TCacheShape> implements DataProxy {
   public link: ApolloLink;
-  public store: DataStore<TCacheShape>;
   public cache: ApolloCache<TCacheShape>;
-  public readonly queryManager: QueryManager<TCacheShape>;
   public disableNetworkFetches: boolean;
   public version: string;
   public queryDeduplication: boolean;
   public defaultOptions: DefaultOptions = {};
   public readonly typeDefs: ApolloClientOptions<TCacheShape>['typeDefs'];
 
+  private queryManager: QueryManager<TCacheShape>;
   private devToolsHookCb: Function;
   private resetStoreCallbacks: Array<() => Promise<any>> = [];
   private clearStoreCallbacks: Array<() => Promise<any>> = [];
@@ -80,6 +77,8 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
   /**
    * Constructs an instance of {@link ApolloClient}.
    *
+   * @param uri The GraphQL endpoint that Apollo Client will connect to. If
+   *            `link` is configured, this option is ignored.
    * @param link The {@link ApolloLink} over which GraphQL documents will be resolved into a response.
    *
    * @param cache The initial cache to use in the data store.
@@ -113,6 +112,9 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
    */
   constructor(options: ApolloClientOptions<TCacheShape>) {
     const {
+      uri,
+      credentials,
+      headers,
       cache,
       ssrMode = false,
       ssrForceFetchDelay = 0,
@@ -129,24 +131,25 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
 
     let { link } = options;
 
-    // If a link hasn't been defined, but local state resolvers have been set,
-    // setup a default empty link.
-    if (!link && resolvers) {
-      link = ApolloLink.empty();
+    if (!link) {
+      if (uri) {
+        link = new HttpLink({ uri, credentials, headers });
+      } else if (resolvers) {
+        link = ApolloLink.empty();
+      }
     }
 
     if (!link || !cache) {
       throw new InvariantError(
-        "In order to initialize Apollo Client, you must specify 'link' and 'cache' properties in the options object.\n" +
-        "These options are part of the upgrade requirements when migrating from Apollo Client 1.x to Apollo Client 2.x.\n" +
-        "For more information, please visit: https://www.apollographql.com/docs/tutorial/client.html#apollo-client-setup"
+        "To initialize Apollo Client, you must specify 'uri' or 'link' and " +
+        "'cache' properties in the options object. \n" +
+        "For more information, please visit: " +
+        "https://www.apollographql.com/docs/react/"
       );
     }
 
-    // remove apollo-client supported directives
     this.link = link;
     this.cache = cache;
-    this.store = new DataStore(cache);
     this.disableNetworkFetches = ssrMode || ssrForceFetchDelay > 0;
     this.queryDeduplication = queryDeduplication;
     this.defaultOptions = defaultOptions || {};
@@ -221,8 +224,8 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
     });
 
     this.queryManager = new QueryManager({
+      cache: this.cache,
       link: this.link,
-      store: this.store,
       queryDeduplication,
       ssrMode,
       clientAwareness: {
@@ -453,17 +456,6 @@ export default class ApolloClient<TCacheShape> implements DataProxy {
     return execute(this.link, payload);
   }
 
-  /**
-   * This initializes the query manager that tracks queries and the cache
-   */
-  public initQueryManager(): QueryManager<TCacheShape> {
-    invariant.warn(
-      'Calling the initQueryManager method is no longer necessary, ' +
-        'and it will be removed from ApolloClient in version 3.0.',
-    );
-    return this.queryManager;
-  }
-
   /**
    * Resets your entire store by clearing out your cache and then re-executing
    * all of your active queries. This makes it so that you may guarantee that
diff --git a/packages/apollo-cache/src/cache.ts b/src/cache/core/cache.ts
similarity index 90%
rename from packages/apollo-cache/src/cache.ts
rename to src/cache/core/cache.ts
--- a/packages/apollo-cache/src/cache.ts
+++ b/src/cache/core/cache.ts
@@ -1,7 +1,8 @@
 import { DocumentNode } from 'graphql';
-import { getFragmentQueryDocument } from 'apollo-utilities';
 
-import { DataProxy, Cache } from './types';
+import { getFragmentQueryDocument } from '../../utilities/graphql/fragments';
+import { DataProxy } from './types/DataProxy';
+import { Cache } from './types/Cache';
 import { justTypenameQuery, queryFromPojo, fragmentFromPojo } from './utils';
 
 export type Transaction<T> = (c: ApolloCache<T>) => void;
@@ -17,11 +18,13 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
   ): void;
   public abstract diff<T>(query: Cache.DiffOptions): Cache.DiffResult<T>;
   public abstract watch(watch: Cache.WatchOptions): () => void;
-  public abstract evict<TVariables = any>(
-    query: Cache.EvictOptions<TVariables>,
-  ): Cache.EvictionResult;
   public abstract reset(): Promise<void>;
 
+  // If called with only one argument, removes the entire entity
+  // identified by dataId. If called with a fieldName as well, removes all
+  // fields of the identified entity whose store names match fieldName.
+  public abstract evict(dataId: string, fieldName?: string): boolean;
+
   // intializer / offline / ssr API
   /**
    * Replaces existing state in the cache (if any) with the values expressed by
diff --git a/packages/apollo-cache/src/types/Cache.ts b/src/cache/core/types/Cache.ts
similarity index 83%
rename from packages/apollo-cache/src/types/Cache.ts
rename to src/cache/core/types/Cache.ts
--- a/packages/apollo-cache/src/types/Cache.ts
+++ b/src/cache/core/types/Cache.ts
@@ -2,9 +2,6 @@ import { DataProxy } from './DataProxy';
 
 export namespace Cache {
   export type WatchCallback = (newData: any) => void;
-  export interface EvictionResult {
-    success: Boolean;
-  }
 
   export interface ReadOptions<TVariables = any>
     extends DataProxy.Query<TVariables> {
@@ -27,11 +24,6 @@ export namespace Cache {
     callback: WatchCallback;
   }
 
-  export interface EvictOptions<TVariables = any>
-    extends DataProxy.Query<TVariables> {
-    rootId?: string;
-  }
-
   export import DiffResult = DataProxy.DiffResult;
   export import WriteQueryOptions = DataProxy.WriteQueryOptions;
   export import WriteFragmentOptions = DataProxy.WriteFragmentOptions;
diff --git a/packages/apollo-cache/src/types/DataProxy.ts b/src/cache/core/types/DataProxy.ts
similarity index 100%
rename from packages/apollo-cache/src/types/DataProxy.ts
rename to src/cache/core/types/DataProxy.ts
diff --git a/packages/apollo-cache/src/utils.ts b/src/cache/core/utils.ts
similarity index 100%
rename from packages/apollo-cache/src/utils.ts
rename to src/cache/core/utils.ts
diff --git a/src/cache/index.ts b/src/cache/index.ts
new file mode 100644
--- /dev/null
+++ b/src/cache/index.ts
@@ -0,0 +1,10 @@
+export { Transaction, ApolloCache } from './core/cache';
+export { Cache } from './core/types/Cache';
+export { DataProxy } from './core/types/DataProxy';
+
+export {
+  InMemoryCache,
+  InMemoryCacheConfig,
+} from './inmemory/inMemoryCache';
+export { defaultDataIdFromObject } from './inmemory/policies';
+export * from './inmemory/types';
diff --git a/src/cache/inmemory/entityStore.ts b/src/cache/inmemory/entityStore.ts
new file mode 100644
--- /dev/null
+++ b/src/cache/inmemory/entityStore.ts
@@ -0,0 +1,506 @@
+import { dep, OptimisticDependencyFunction, KeyTrie } from 'optimism';
+import { invariant } from 'ts-invariant';
+import { equal } from '@wry/equality';
+
+import { isReference, StoreValue } from '../../utilities/graphql/storeUtils';
+import {
+  DeepMerger,
+  ReconcilerFunction,
+} from '../../utilities/common/mergeDeep';
+import { canUseWeakMap } from '../../utilities/common/canUse';
+import { NormalizedCache, NormalizedCacheObject, StoreObject } from './types';
+import {
+  getTypenameFromStoreObject,
+  fieldNameFromStoreName,
+} from './helpers';
+
+const hasOwn = Object.prototype.hasOwnProperty;
+
+export abstract class EntityStore implements NormalizedCache {
+  protected data: NormalizedCacheObject = Object.create(null);
+
+  public readonly group: CacheGroup;
+
+  public abstract addLayer(
+    layerId: string,
+    replay: (layer: EntityStore) => any,
+  ): EntityStore;
+
+  public abstract removeLayer(layerId: string): EntityStore;
+
+  // Although the EntityStore class is abstract, it contains concrete
+  // implementations of the various NormalizedCache interface methods that
+  // are inherited by the Root and Layer subclasses.
+
+  public toObject(): NormalizedCacheObject {
+    return { ...this.data };
+  }
+
+  public has(dataId: string): boolean {
+    return this.get(dataId) !== void 0;
+  }
+
+  public get(dataId: string): StoreObject {
+    this.group.depend(dataId);
+    return this.data[dataId];
+  }
+
+  public getFieldValue(dataId: string, storeFieldName: string): StoreValue {
+    this.group.depend(dataId, storeFieldName);
+    const storeObject = this.data[dataId];
+    return storeObject && storeObject[storeFieldName];
+  }
+
+  public merge(dataId: string, incoming: StoreObject): void {
+    const existing = this.get(dataId);
+    const merged = new DeepMerger(storeObjectReconciler)
+      .merge(existing, incoming, this);
+    if (merged !== existing) {
+      this.data[dataId] = merged;
+      delete this.refs[dataId];
+      if (this.group.caching) {
+        // First, invalidate any dependents that called get rather than
+        // getFieldValue.
+        this.group.dirty(dataId);
+        // Now invalidate dependents who called getFieldValue for any
+        // fields that are changing as a result of this merge.
+        Object.keys(incoming).forEach(storeFieldName => {
+          if (!existing || incoming[storeFieldName] !== existing[storeFieldName]) {
+            this.group.dirty(dataId, storeFieldName);
+          }
+        });
+      }
+    }
+  }
+
+  // If called with only one argument, removes the entire entity
+  // identified by dataId. If called with a fieldName as well, removes all
+  // fields of that entity whose names match fieldName, according to the
+  // fieldNameFromStoreName helper function.
+  public delete(dataId: string, fieldName?: string) {
+    const storeObject = this.get(dataId);
+
+    if (storeObject) {
+      // In case someone passes in a storeFieldName (field.name.value +
+      // arguments key), normalize it down to just the field name.
+      fieldName = fieldName && fieldNameFromStoreName(fieldName);
+
+      const storeNamesToDelete: string[] = [];
+      Object.keys(storeObject).forEach(storeFieldName => {
+        // If the field value has already been set to undefined, we do not
+        // need to delete it again.
+        if (storeObject[storeFieldName] !== void 0 &&
+            // If no fieldName provided, delete all fields from storeObject.
+            // If provided, delete all fields matching fieldName.
+            (!fieldName || fieldName === fieldNameFromStoreName(storeFieldName))) {
+          storeNamesToDelete.push(storeFieldName);
+        }
+      });
+
+      if (storeNamesToDelete.length) {
+        // If we only have to worry about the Root layer of the store,
+        // then we can safely delete fields within entities, or whole
+        // entities by ID. If this instanceof EntityStore.Layer, however,
+        // then we need to set the "deleted" values to undefined instead
+        // of actually deleting them, so the deletion does not un-shadow
+        // values inherited from lower layers of the store.
+        const canDelete = this instanceof EntityStore.Root;
+        const remove = (obj: Record<string, any>, key: string) => {
+          if (canDelete) {
+            delete obj[key];
+          } else {
+            obj[key] = void 0;
+          }
+        };
+
+        // Note that we do not delete the this.rootIds[dataId] retainment
+        // count for this ID, since an object with the same ID could appear in
+        // the store again, and should not have to be retained again.
+        // delete this.rootIds[dataId];
+        delete this.refs[dataId];
+
+        const fieldsToDirty: Record<string, true> = Object.create(null);
+
+        if (fieldName) {
+          // If we have a fieldName and it matches more than zero fields,
+          // then we need to make a copy of this.data[dataId] without the
+          // fields that are getting deleted.
+          const cleaned = this.data[dataId] = { ...storeObject };
+          storeNamesToDelete.forEach(storeFieldName => {
+            remove(cleaned, storeFieldName);
+          });
+          // Although it would be logically correct to dirty each
+          // storeFieldName in the loop above, we know that they all have
+          // the same name, according to fieldNameFromStoreName.
+          fieldsToDirty[fieldName] = true;
+        } else {
+          // If no fieldName was provided, then we delete the whole entity
+          // from the cache.
+          remove(this.data, dataId);
+          storeNamesToDelete.forEach(storeFieldName => {
+            const fieldName = fieldNameFromStoreName(storeFieldName);
+            fieldsToDirty[fieldName] = true;
+          });
+        }
+
+        if (this.group.caching) {
+          this.group.dirty(dataId);
+          Object.keys(fieldsToDirty).forEach(fieldName => {
+            this.group.dirty(dataId, fieldName);
+          });
+        }
+      }
+    }
+  }
+
+  public clear(): void {
+    this.replace(null);
+  }
+
+  public replace(newData: NormalizedCacheObject | null): void {
+    Object.keys(this.data).forEach(dataId => {
+      if (!(newData && hasOwn.call(newData, dataId))) {
+        this.delete(dataId);
+      }
+    });
+    if (newData) {
+      Object.keys(newData).forEach(dataId => {
+        this.merge(dataId, newData[dataId]);
+      });
+    }
+  }
+
+  // Maps root entity IDs to the number of times they have been retained, minus
+  // the number of times they have been released. Retained entities keep other
+  // entities they reference (even indirectly) from being garbage collected.
+  private rootIds: {
+    [rootId: string]: number;
+  } = Object.create(null);
+
+  public retain(rootId: string): number {
+    return this.rootIds[rootId] = (this.rootIds[rootId] || 0) + 1;
+  }
+
+  public release(rootId: string): number {
+    if (this.rootIds[rootId] > 0) {
+      const count = --this.rootIds[rootId];
+      if (!count) delete this.rootIds[rootId];
+      return count;
+    }
+    return 0;
+  }
+
+  // This method will be overridden in the Layer class to merge root IDs for all
+  // layers (including the root).
+  public getRootIdSet() {
+    return new Set(Object.keys(this.rootIds));
+  }
+
+  // The goal of garbage collection is to remove IDs from the Root layer of the
+  // store that are no longer reachable starting from any IDs that have been
+  // explicitly retained (see retain and release, above). Returns an array of
+  // dataId strings that were removed from the store.
+  public gc() {
+    const ids = this.getRootIdSet();
+    const snapshot = this.toObject();
+    ids.forEach(id => {
+      if (hasOwn.call(snapshot, id)) {
+        // Because we are iterating over an ECMAScript Set, the IDs we add here
+        // will be visited in later iterations of the forEach loop only if they
+        // were not previously contained by the Set.
+        Object.keys(this.findChildRefIds(id)).forEach(ids.add, ids);
+        // By removing IDs from the snapshot object here, we protect them from
+        // getting removed from the root store layer below.
+        delete snapshot[id];
+      }
+    });
+    const idsToRemove = Object.keys(snapshot);
+    if (idsToRemove.length) {
+      let root: EntityStore = this;
+      while (root instanceof Layer) root = root.parent;
+      idsToRemove.forEach(id => root.delete(id));
+    }
+    return idsToRemove;
+  }
+
+  // Lazily tracks { __ref: <dataId> } strings contained by this.data[dataId].
+  private refs: {
+    [dataId: string]: Record<string, true>;
+  } = Object.create(null);
+
+  public findChildRefIds(dataId: string): Record<string, true> {
+    if (!hasOwn.call(this.refs, dataId)) {
+      const found = this.refs[dataId] = Object.create(null);
+      const workSet = new Set([this.data[dataId]]);
+      // Within the store, only arrays and objects can contain child entity
+      // references, so we can prune the traversal using this predicate:
+      const canTraverse = (obj: any) => obj !== null && typeof obj === 'object';
+      workSet.forEach(obj => {
+        if (isReference(obj)) {
+          found[obj.__ref] = true;
+        } else if (canTraverse(obj)) {
+          Object.values(obj)
+            // No need to add primitive values to the workSet, since they cannot
+            // contain reference objects.
+            .filter(canTraverse)
+            .forEach(workSet.add, workSet);
+        }
+      });
+    }
+    return this.refs[dataId];
+  }
+
+  // Used to compute cache keys specific to this.group.
+  public makeCacheKey(...args: any[]) {
+    return this.group.keyMaker.lookupArray(args);
+  }
+}
+
+// A single CacheGroup represents a set of one or more EntityStore objects,
+// typically the Root store in a CacheGroup by itself, and all active Layer
+// stores in a group together. A single EntityStore object belongs to only
+// one CacheGroup, store.group. The CacheGroup is responsible for tracking
+// dependencies, so store.group is helpful for generating unique keys for
+// cached results that need to be invalidated when/if those dependencies
+// change. If we used the EntityStore objects themselves as cache keys (that
+// is, store rather than store.group), the cache would become unnecessarily
+// fragmented by all the different Layer objects. Instead, the CacheGroup
+// approach allows all optimistic Layer objects in the same linked list to
+// belong to one CacheGroup, with the non-optimistic Root object belonging
+// to another CacheGroup, allowing resultCaching dependencies to be tracked
+// separately for optimistic and non-optimistic entity data.
+class CacheGroup {
+  private d: OptimisticDependencyFunction<string> | null = null;
+
+  constructor(public readonly caching: boolean) {
+    this.d = caching ? dep<string>() : null;
+  }
+
+  public depend(dataId: string, storeFieldName?: string) {
+    if (this.d) {
+      this.d(makeDepKey(dataId, storeFieldName));
+    }
+  }
+
+  public dirty(dataId: string, storeFieldName?: string) {
+    if (this.d) {
+      this.d.dirty(
+        typeof storeFieldName === "string"
+          ? makeDepKey(dataId, storeFieldName)
+          : makeDepKey(dataId),
+      );
+    }
+  }
+
+  // Used by the EntityStore#makeCacheKey method to compute cache keys
+  // specific to this CacheGroup.
+  public readonly keyMaker = new KeyTrie<object>(canUseWeakMap);
+}
+
+function makeDepKey(dataId: string, storeFieldName?: string) {
+  const parts = [dataId];
+  if (typeof storeFieldName === "string") {
+    parts.push(fieldNameFromStoreName(storeFieldName));
+  }
+  return JSON.stringify(parts);
+}
+
+export namespace EntityStore {
+  // Refer to this class as EntityStore.Root outside this namespace.
+  export class Root extends EntityStore {
+    // Although each Root instance gets its own unique CacheGroup object,
+    // any Layer instances created by calling addLayer need to share a
+    // single distinct CacheGroup object. Since this shared object must
+    // outlast the Layer instances themselves, it needs to be created and
+    // owned by the Root instance.
+    private sharedLayerGroup: CacheGroup = null;
+
+    constructor({
+      resultCaching = true,
+      seed,
+    }: {
+      resultCaching?: boolean;
+      seed?: NormalizedCacheObject;
+    }) {
+      super();
+      (this.group as any) = new CacheGroup(resultCaching);
+      this.sharedLayerGroup = new CacheGroup(resultCaching);
+      if (seed) this.replace(seed);
+    }
+
+    public addLayer(
+      layerId: string,
+      replay: (layer: EntityStore) => any,
+    ): EntityStore {
+      // The replay function will be called in the Layer constructor.
+      return new Layer(layerId, this, replay, this.sharedLayerGroup);
+    }
+
+    public removeLayer(layerId: string): Root {
+      // Never remove the root layer.
+      return this;
+    }
+  }
+}
+
+// Not exported, since all Layer instances are created by the addLayer method
+// of the EntityStore.Root class.
+class Layer extends EntityStore {
+  constructor(
+    public readonly id: string,
+    public readonly parent: Layer | EntityStore.Root,
+    public readonly replay: (layer: EntityStore) => any,
+    public readonly group: CacheGroup,
+  ) {
+    super();
+    replay(this);
+  }
+
+  public addLayer(
+    layerId: string,
+    replay: (layer: EntityStore) => any,
+  ): EntityStore {
+    return new Layer(layerId, this, replay, this.group);
+  }
+
+  public removeLayer(layerId: string): EntityStore {
+    // Remove all instances of the given id, not just the first one.
+    const parent = this.parent.removeLayer(layerId);
+
+    if (layerId === this.id) {
+      // Dirty every ID we're removing.
+      // TODO Some of these IDs could escape dirtying if value unchanged.
+      if (this.group.caching) {
+        Object.keys(this.data).forEach(dataId => this.delete(dataId));
+      }
+      return parent;
+    }
+
+    // No changes are necessary if the parent chain remains identical.
+    if (parent === this.parent) return this;
+
+    // Recreate this layer on top of the new parent.
+    return parent.addLayer(this.id, this.replay);
+  }
+
+  public toObject(): NormalizedCacheObject {
+    return {
+      ...this.parent.toObject(),
+      ...this.data,
+    };
+  }
+
+  public get(dataId: string): StoreObject {
+    if (hasOwn.call(this.data, dataId)) {
+      return super.get(dataId);
+    }
+
+    // If this layer has a this.depend function and it's not the one
+    // this.parent is using, we need to depend on the given dataId using
+    // this.depend before delegating to the parent. This check saves us
+    // from calling this.depend for every optimistic layer we examine, but
+    // ensures we call this.depend in the last optimistic layer before we
+    // reach the root layer.
+
+    if (this.group.caching && this.group !== this.parent.group) {
+      this.group.depend(dataId);
+    }
+
+    return this.parent.get(dataId);
+  }
+
+  public getFieldValue(dataId: string, storeFieldName: string): StoreValue {
+    if (hasOwn.call(this.data, dataId)) {
+      const storeObject = this.data[dataId];
+      if (storeObject && hasOwn.call(storeObject, storeFieldName)) {
+        return super.getFieldValue(dataId, storeFieldName);
+      }
+    }
+
+    if (this.group.caching && this.group !== this.parent.group) {
+      this.group.depend(dataId, storeFieldName);
+    }
+
+    return this.parent.getFieldValue(dataId, storeFieldName);
+  }
+
+  // Return a Set<string> of all the ID strings that have been retained by this
+  // Layer *and* any layers/roots beneath it.
+  public getRootIdSet(): Set<string> {
+    const ids = this.parent.getRootIdSet();
+    super.getRootIdSet().forEach(ids.add, ids);
+    return ids;
+  }
+
+  public findChildRefIds(dataId: string): Record<string, true> {
+    const fromParent = this.parent.findChildRefIds(dataId);
+    return hasOwn.call(this.data, dataId) ? {
+      ...fromParent,
+      ...super.findChildRefIds(dataId),
+    } : fromParent;
+  }
+}
+
+const storeObjectReconciler: ReconcilerFunction<[EntityStore]> = function (
+  existingObject,
+  incomingObject,
+  property,
+  // This parameter comes from the additional argument we pass to the
+  // merge method in context.mergeStoreObjects (see writeQueryToStore).
+  store,
+) {
+  // In the future, reconciliation logic may depend on the type of the parent
+  // StoreObject, not just the values of the given property.
+  const existing = existingObject[property];
+  const incoming = incomingObject[property];
+
+  if (
+    existing !== incoming &&
+    // The DeepMerger class has various helpful utilities that we might as
+    // well reuse here.
+    this.isObject(existing) &&
+    this.isObject(incoming)
+  ) {
+    const eType = getTypenameFromStoreObject(store, existing);
+    const iType = getTypenameFromStoreObject(store, incoming);
+    // If both objects have a typename and the typename is different, let the
+    // incoming object win. The typename can change when a different subtype
+    // of a union or interface is written to the store.
+    if (
+      typeof eType === 'string' &&
+      typeof iType === 'string' &&
+      eType !== iType
+    ) {
+      return incoming;
+    }
+
+    invariant(
+      !isReference(existing) || isReference(incoming),
+      `Store error: the application attempted to write an object with no provided id but the store already contains an id of ${existing.__ref} for this object.`,
+    );
+
+    // It's worth checking deep equality here (even though blindly
+    // returning incoming would be logically correct) because preserving
+    // the referential identity of existing data can prevent needless
+    // rereading and rerendering.
+    if (equal(existing, incoming)) {
+      return existing;
+    }
+  }
+
+  // In all other cases, incoming replaces existing without any effort to
+  // merge them deeply, since custom merge functions have already been
+  // applied to the incoming data by walkWithMergeOverrides.
+  return incoming;
+}
+
+export function supportsResultCaching(store: any): store is EntityStore {
+  // When result caching is disabled, store.depend will be null.
+  return !!(store instanceof EntityStore && store.group.caching);
+}
+
+export function defaultNormalizedCacheFactory(
+  seed?: NormalizedCacheObject,
+): NormalizedCache {
+  return new EntityStore.Root({ resultCaching: true, seed });
+}
diff --git a/packages/apollo-cache-inmemory/src/fixPolyfills.ts b/src/cache/inmemory/fixPolyfills.ts
similarity index 100%
rename from packages/apollo-cache-inmemory/src/fixPolyfills.ts
rename to src/cache/inmemory/fixPolyfills.ts
diff --git a/src/cache/inmemory/helpers.ts b/src/cache/inmemory/helpers.ts
new file mode 100644
--- /dev/null
+++ b/src/cache/inmemory/helpers.ts
@@ -0,0 +1,75 @@
+import { FieldNode } from 'graphql';
+import { NormalizedCache, StoreObject } from './types';
+import { Reference, isReference, StoreValue, isField } from '../../utilities/graphql/storeUtils';
+import { DeepMerger, ReconcilerFunction } from '../../utilities/common/mergeDeep';
+
+export function getTypenameFromStoreObject(
+  store: NormalizedCache,
+  objectOrReference: StoreObject | Reference,
+): string | undefined {
+  return isReference(objectOrReference)
+    ? store.getFieldValue(objectOrReference.__ref, "__typename") as string
+    : objectOrReference && objectOrReference.__typename;
+}
+
+const FieldNamePattern = /^[_A-Za-z0-9]+/;
+export function fieldNameFromStoreName(storeFieldName: string) {
+  const match = storeFieldName.match(FieldNamePattern);
+  return match && match[0];
+}
+
+// Invoking merge functions needs to happen after processSelectionSet has
+// finished, but requires information that is more readily available
+// during processSelectionSet, so processSelectionSet embeds special
+// objects of the following shape within its result tree, which then must
+// be removed by calling Policies#applyMerges.
+export interface FieldValueToBeMerged {
+  __field: FieldNode;
+  __typename: string;
+  __value: StoreValue;
+}
+
+export function isFieldValueToBeMerged(
+  value: any,
+): value is FieldValueToBeMerged {
+  const field = value && value.__field;
+  return field && isField(field);
+}
+
+export function makeProcessedFieldsMerger() {
+  // A DeepMerger that merges arrays and objects structurally, but otherwise
+  // prefers incoming scalar values over existing values. Provides special
+  // treatment for FieldValueToBeMerged objects. Used to accumulate fields
+  // when processing a single selection set.
+  return new DeepMerger(reconcileProcessedFields);
+}
+
+const reconcileProcessedFields: ReconcilerFunction<[]> = function (
+  existingObject,
+  incomingObject,
+  property,
+) {
+  const existing = existingObject[property];
+  const incoming = incomingObject[property];
+
+  if (isFieldValueToBeMerged(existing)) {
+    existing.__value = this.merge(
+      existing.__value,
+      isFieldValueToBeMerged(incoming)
+        // TODO Check compatibility of __field and __typename properties?
+        ? incoming.__value
+        : incoming,
+    );
+    return existing;
+  }
+
+  if (isFieldValueToBeMerged(incoming)) {
+    incoming.__value = this.merge(
+      existing,
+      incoming.__value,
+    );
+    return incoming;
+  }
+
+  return this.merge(existing, incoming);
+}
diff --git a/src/cache/inmemory/inMemoryCache.ts b/src/cache/inmemory/inMemoryCache.ts
new file mode 100644
--- /dev/null
+++ b/src/cache/inmemory/inMemoryCache.ts
@@ -0,0 +1,307 @@
+// Make builtins like Map and Set safe to use with non-extensible objects.
+import './fixPolyfills';
+
+import { DocumentNode } from 'graphql';
+import { wrap } from 'optimism';
+
+import { ApolloCache, Transaction } from '../core/cache';
+import { Cache } from '../core/types/Cache';
+import { addTypenameToDocument } from '../../utilities/graphql/transform';
+import {
+  ApolloReducerConfig,
+  NormalizedCacheObject,
+  StoreObject,
+} from './types';
+import { StoreReader } from './readFromStore';
+import { StoreWriter } from './writeToStore';
+import { EntityStore, supportsResultCaching } from './entityStore';
+import {
+  defaultDataIdFromObject,
+  PossibleTypesMap,
+  Policies,
+  TypePolicies,
+} from './policies';
+
+export interface InMemoryCacheConfig extends ApolloReducerConfig {
+  resultCaching?: boolean;
+  possibleTypes?: PossibleTypesMap;
+  typePolicies?: TypePolicies;
+}
+
+const defaultConfig: InMemoryCacheConfig = {
+  dataIdFromObject: defaultDataIdFromObject,
+  addTypename: true,
+  resultCaching: true,
+  typePolicies: {},
+};
+
+export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
+  private data: EntityStore;
+  private optimisticData: EntityStore;
+
+  protected config: InMemoryCacheConfig;
+  private watches = new Set<Cache.WatchOptions>();
+  private addTypename: boolean;
+  private policies: Policies;
+
+  private typenameDocumentCache = new Map<DocumentNode, DocumentNode>();
+  private storeReader: StoreReader;
+  private storeWriter: StoreWriter;
+
+  // Set this while in a transaction to prevent broadcasts...
+  // don't forget to turn it back on!
+  private silenceBroadcast: boolean = false;
+
+  constructor(config: InMemoryCacheConfig = {}) {
+    super();
+    this.config = { ...defaultConfig, ...config };
+    this.addTypename = !!this.config.addTypename;
+
+    this.policies = new Policies({
+      dataIdFromObject: this.config.dataIdFromObject,
+      possibleTypes: this.config.possibleTypes,
+      typePolicies: this.config.typePolicies,
+    });
+
+    // Passing { resultCaching: false } in the InMemoryCache constructor options
+    // will completely disable dependency tracking, which will improve memory
+    // usage but worsen the performance of repeated reads.
+    this.data = new EntityStore.Root({
+      resultCaching: this.config.resultCaching,
+    });
+
+    // When no optimistic writes are currently active, cache.optimisticData ===
+    // cache.data, so there are no additional layers on top of the actual data.
+    // When an optimistic update happens, this.optimisticData will become a
+    // linked list of OptimisticCacheLayer objects that terminates with the
+    // original this.data cache object.
+    this.optimisticData = this.data;
+
+    this.storeWriter = new StoreWriter({
+      policies: this.policies,
+    });
+
+    this.storeReader = new StoreReader({
+      addTypename: this.addTypename,
+      policies: this.policies,
+    });
+
+    const cache = this;
+    const { maybeBroadcastWatch } = cache;
+    this.maybeBroadcastWatch = wrap((c: Cache.WatchOptions) => {
+      return maybeBroadcastWatch.call(this, c);
+    }, {
+      makeCacheKey(c: Cache.WatchOptions) {
+        // Return a cache key (thus enabling result caching) only if we're
+        // currently using a data store that can track cache dependencies.
+        const store = c.optimistic ? cache.optimisticData : cache.data;
+        if (supportsResultCaching(store)) {
+          const { optimistic, rootId, variables } = c;
+          return store.makeCacheKey(
+            c.query,
+            // Different watches can have the same query, optimistic
+            // status, rootId, and variables, but if their callbacks are
+            // different, the (identical) result needs to be delivered to
+            // each distinct callback. The easiest way to achieve that
+            // separation is to include c.callback in the cache key for
+            // maybeBroadcastWatch calls. See issue #5733.
+            c.callback,
+            JSON.stringify({ optimistic, rootId, variables }),
+          );
+        }
+      }
+    });
+  }
+
+  public restore(data: NormalizedCacheObject): this {
+    if (data) this.data.replace(data);
+    return this;
+  }
+
+  public extract(optimistic: boolean = false): NormalizedCacheObject {
+    return (optimistic ? this.optimisticData : this.data).toObject();
+  }
+
+  public read<T>(options: Cache.ReadOptions): T | null {
+    if (typeof options.rootId === 'string' &&
+        !this.data.has(options.rootId)) {
+      return null;
+    }
+
+    return this.storeReader.readQueryFromStore({
+      store: options.optimistic ? this.optimisticData : this.data,
+      query: options.query,
+      variables: options.variables,
+      rootId: options.rootId,
+      config: this.config,
+    }) || null;
+  }
+
+  public write(options: Cache.WriteOptions): void {
+    this.storeWriter.writeQueryToStore({
+      store: this.data,
+      query: options.query,
+      result: options.result,
+      dataId: options.dataId,
+      variables: options.variables,
+    });
+
+    this.broadcastWatches();
+  }
+
+  public diff<T>(options: Cache.DiffOptions): Cache.DiffResult<T> {
+    return this.storeReader.diffQueryAgainstStore({
+      store: options.optimistic ? this.optimisticData : this.data,
+      query: options.query,
+      variables: options.variables,
+      returnPartialData: options.returnPartialData,
+      config: this.config,
+    });
+  }
+
+  public watch(watch: Cache.WatchOptions): () => void {
+    this.watches.add(watch);
+
+    return () => {
+      this.watches.delete(watch);
+    };
+  }
+
+  // Request garbage collection of unreachable normalized entities.
+  public gc() {
+    return this.optimisticData.gc();
+  }
+
+  // Call this method to ensure the given root ID remains in the cache after
+  // garbage collection, along with its transitive child entities. Note that
+  // the cache automatically retains all directly written entities. By default,
+  // the retainment persists after optimistic updates are removed. Pass true
+  // for the optimistic argument if you would prefer for the retainment to be
+  // discarded when the top-most optimistic layer is removed. Returns the
+  // resulting (non-negative) retainment count.
+  public retain(rootId: string, optimistic?: boolean): number {
+    return (optimistic ? this.optimisticData : this.data).retain(rootId);
+  }
+
+  // Call this method to undo the effect of the retain method, above. Once the
+  // retainment count falls to zero, the given ID will no longer be preserved
+  // during garbage collection, though it may still be preserved by other safe
+  // entities that refer to it. Returns the resulting (non-negative) retainment
+  // count, in case that's useful.
+  public release(rootId: string, optimistic?: boolean): number {
+    return (optimistic ? this.optimisticData : this.data).release(rootId);
+  }
+
+  // Returns the canonical ID for a given StoreObject, obeying typePolicies
+  // and keyFields (and dataIdFromObject, if you still use that). At minimum,
+  // the object must contain a __typename and any primary key fields required
+  // to identify entities of that type. If you pass a query result object, be
+  // sure that none of the primary key fields have been renamed by aliasing.
+  public identify(object: StoreObject): string | null {
+    return this.policies.identify(object);
+  }
+
+  public evict(dataId: string, fieldName?: string): boolean {
+    if (this.optimisticData.has(dataId)) {
+      // Note that this deletion does not trigger a garbage collection, which
+      // is convenient in cases where you want to evict multiple entities before
+      // performing a single garbage collection.
+      this.optimisticData.delete(dataId, fieldName);
+      this.broadcastWatches();
+      return !this.optimisticData.has(dataId);
+    }
+    return false;
+  }
+
+  public reset(): Promise<void> {
+    this.data.clear();
+    this.optimisticData = this.data;
+    this.broadcastWatches();
+    return Promise.resolve();
+  }
+
+  public removeOptimistic(idToRemove: string) {
+    const newOptimisticData = this.optimisticData.removeLayer(idToRemove);
+    if (newOptimisticData !== this.optimisticData) {
+      this.optimisticData = newOptimisticData;
+      this.broadcastWatches();
+    }
+  }
+
+  public performTransaction(
+    transaction: (proxy: InMemoryCache) => any,
+    // This parameter is not part of the performTransaction signature inherited
+    // from the ApolloCache abstract class, but it's useful because it saves us
+    // from duplicating this implementation in recordOptimisticTransaction.
+    optimisticId?: string,
+  ) {
+    const perform = (layer?: EntityStore) => {
+      const proxy: InMemoryCache = Object.create(this);
+      proxy.silenceBroadcast = true;
+      if (layer) {
+        // The proxy object is just like this except that silenceBroadcast
+        // is set to true, and proxy.data and proxy.optimisticData both
+        // point to the same layer.
+        proxy.data = proxy.optimisticData = layer;
+      }
+      // Because the proxy object can simply be forgotten, we do not need
+      // to wrap this call with a try-finally block.
+      return transaction(proxy);
+    };
+
+    if (typeof optimisticId === 'string') {
+      // Note that there can be multiple layers with the same optimisticId.
+      // When removeOptimistic(id) is called for that id, all matching layers
+      // will be removed, and the remaining layers will be reapplied.
+      this.optimisticData = this.optimisticData.addLayer(optimisticId, perform);
+    } else {
+      // If we don't have an optimisticId, perform the transaction anyway. Note
+      // that this.optimisticData.addLayer calls perform, too.
+      perform();
+    }
+
+    // This broadcast does nothing if this.silenceBroadcast is true.
+    this.broadcastWatches();
+  }
+
+  public recordOptimisticTransaction(
+    transaction: Transaction<NormalizedCacheObject>,
+    id: string,
+  ) {
+    return this.performTransaction(transaction, id);
+  }
+
+  public transformDocument(document: DocumentNode): DocumentNode {
+    if (this.addTypename) {
+      let result = this.typenameDocumentCache.get(document);
+      if (!result) {
+        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;
+    }
+    return document;
+  }
+
+  protected broadcastWatches() {
+    if (!this.silenceBroadcast) {
+      this.watches.forEach(c => this.maybeBroadcastWatch(c));
+    }
+  }
+
+  // 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) {
+    c.callback(
+      this.diff({
+        query: c.query,
+        variables: c.variables,
+        optimistic: c.optimistic,
+      }),
+    );
+  }
+}
diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts
new file mode 100644
--- /dev/null
+++ b/src/cache/inmemory/policies.ts
@@ -0,0 +1,811 @@
+import {
+  InlineFragmentNode,
+  FragmentDefinitionNode,
+  SelectionSetNode,
+  FieldNode,
+} from "graphql";
+
+import { dep, KeyTrie } from 'optimism';
+import invariant from 'ts-invariant';
+
+import {
+  FragmentMap,
+  getFragmentFromSelection,
+} from '../../utilities/graphql/fragments';
+
+import {
+  isField,
+  getTypenameFromResult,
+  valueToObjectRepresentation,
+  storeKeyNameFromField,
+  StoreValue,
+  argumentsObjectFromField,
+  Reference,
+  makeReference,
+  isReference,
+} from '../../utilities/graphql/storeUtils';
+
+import { maybeDeepFreeze } from '../../utilities/common/maybeDeepFreeze';
+import { canUseWeakMap } from '../../utilities/common/canUse';
+
+import {
+  IdGetter,
+  StoreObject,
+  NormalizedCache,
+} from "./types";
+
+import {
+  fieldNameFromStoreName,
+  FieldValueToBeMerged,
+  isFieldValueToBeMerged,
+} from './helpers';
+
+const hasOwn = Object.prototype.hasOwnProperty;
+
+export type TypePolicies = {
+  [__typename: string]: TypePolicy;
+}
+
+// TypeScript 3.7 will allow recursive type aliases, so this should work:
+// type KeySpecifier = (string | KeySpecifier)[]
+type KeySpecifier = (string | any[])[];
+
+type KeyFieldsFunction = (
+  object: Readonly<StoreObject>,
+  context: {
+    typename: string;
+    selectionSet?: SelectionSetNode;
+    fragmentMap?: FragmentMap;
+    policies: Policies;
+  },
+) => ReturnType<IdGetter>;
+
+type TypePolicy = {
+  // Allows defining the primary key fields for this type, either using an
+  // array of field names or a function that returns an arbitrary string.
+  keyFields?: KeySpecifier | KeyFieldsFunction | false;
+
+  // In the rare event that your schema happens to use a different
+  // __typename for the root Query, Mutation, and/or Schema types, you can
+  // express your deviant preferences by enabling one of these options.
+  queryType?: true,
+  mutationType?: true,
+  subscriptionType?: true,
+
+  fields?: {
+    [fieldName: string]:
+      | FieldPolicy<StoreValue>
+      | FieldReadFunction<StoreValue>;
+  }
+};
+
+type KeyArgsFunction = (
+  field: FieldNode,
+  context: {
+    typename: string;
+    variables: Record<string, any>;
+    policies: Policies;
+  },
+) => ReturnType<IdGetter>;
+
+export type FieldPolicy<TValue> = {
+  keyArgs?: KeySpecifier | KeyArgsFunction | false;
+  read?: FieldReadFunction<TValue>;
+  merge?: FieldMergeFunction<TValue>;
+};
+
+export type FieldValueGetter =
+  ReturnType<Policies["makeFieldValueGetter"]>;
+
+type StorageType = Record<string, any>;
+
+interface FieldFunctionOptions {
+  args: Record<string, any> | null;
+
+  // The name of the field, equal to options.field.name.value when
+  // options.field is available. Useful if you reuse the same function for
+  // multiple fields, and you need to know which field you're currently
+  // processing. Always a string, even when options.field is null.
+  fieldName: string;
+
+  // The FieldNode object used to read this field. Useful if you need to
+  // know about other attributes of the field, such as its directives. This
+  // option will be null when a string was passed to options.readField.
+  field: FieldNode | null;
+
+  variables?: Record<string, any>;
+
+  // In rare advanced use cases, a read or merge function may wish to
+  // consult the current Policies object, for example to call
+  // getStoreFieldName manually.
+  policies: Policies;
+
+  // Utilities for dealing with { __ref } objects.
+  isReference: typeof isReference;
+  toReference: Policies["toReference"];
+
+  // Helper function for reading other fields within the current object.
+  // If a foreign object or reference is provided, the field will be read
+  // from that object instead of the current object, so this function can
+  // be used (together with isReference) to examine the cache outside the
+  // current object. If a FieldNode is passed instead of a string, and
+  // that FieldNode has arguments, the same options.variables will be used
+  // to compute the argument values. Note that this function will invoke
+  // custom read functions for other fields, if defined. Always returns
+  // immutable data (enforced with Object.freeze in development).
+  readField<T = StoreValue>(
+    nameOrField: string | FieldNode,
+    foreignObjOrRef?: StoreObject | Reference,
+  ): Readonly<T>;
+
+  // A handy place to put field-specific data that you want to survive
+  // across multiple read function calls. Useful for field-level caching,
+  // if your read function does any expensive work.
+  storage: StorageType;
+
+  // Call this function to invalidate any cached queries that previously
+  // consumed this field. If you use options.storage to cache the result
+  // of an expensive read function, updating options.storage and then
+  // calling options.invalidate() can be a good way to deliver the new
+  // result asynchronously.
+  invalidate(): void;
+}
+
+type FieldReadFunction<TExisting, TResult = TExisting> = (
+  // When reading a field, one often needs to know about any existing
+  // value stored for that field. If the field is read before any value
+  // has been written to the cache, this existing parameter will be
+  // undefined, which makes it easy to use a default parameter expression
+  // to supply the initial value. This parameter is positional (rather
+  // than one of the named options) because that makes it possible for the
+  // developer to annotate it with a type, without also having to provide
+  // a whole new type for the options object.
+  existing: Readonly<TExisting> | undefined,
+  options: FieldFunctionOptions,
+) => TResult;
+
+type FieldMergeFunction<TExisting> = (
+  existing: Readonly<TExisting> | undefined,
+  // The incoming parameter needs to be positional as well, for the same
+  // reasons discussed in FieldReadFunction above.
+  incoming: Readonly<StoreValue>,
+  options: FieldFunctionOptions,
+) => TExisting;
+
+export function defaultDataIdFromObject(object: StoreObject) {
+  const { __typename, id, _id } = object;
+  if (typeof __typename === "string") {
+    if (typeof id !== "undefined") return `${__typename}:${id}`;
+    if (typeof _id !== "undefined") return `${__typename}:${_id}`;
+  }
+  return null;
+}
+
+const nullKeyFieldsFn: KeyFieldsFunction = () => null;
+const simpleKeyArgsFn: KeyArgsFunction = field => field.name.value;
+
+export type PossibleTypesMap = {
+  [supertype: string]: string[];
+};
+
+export class Policies {
+  private typePolicies: {
+    [__typename: string]: {
+      keyFn?: KeyFieldsFunction;
+      subtypes?: Set<string>;
+      fields?: {
+        [fieldName: string]: {
+          keyFn?: KeyArgsFunction;
+          read?: FieldReadFunction<StoreValue>;
+          merge?: FieldMergeFunction<StoreValue>;
+        };
+      };
+    };
+  } = Object.create(null);
+
+  public readonly rootTypenamesById: Readonly<Record<string, string>> = {
+    __proto__: null, // Equivalent to Object.create(null)
+    ROOT_QUERY: "Query",
+    ROOT_MUTATION: "Mutation",
+    ROOT_SUBSCRIPTION: "Subscription",
+  };
+
+  public readonly usingPossibleTypes = false;
+
+  constructor(private config: {
+    dataIdFromObject?: KeyFieldsFunction;
+    possibleTypes?: PossibleTypesMap;
+    typePolicies?: TypePolicies;
+  } = {}) {
+    this.config = {
+      dataIdFromObject: defaultDataIdFromObject,
+      ...config,
+    };
+
+    if (config.possibleTypes) {
+      this.addPossibleTypes(config.possibleTypes);
+    }
+
+    if (config.typePolicies) {
+      this.addTypePolicies(config.typePolicies);
+    }
+  }
+
+  // Bound function that returns a Reference using this.identify.
+  // Provided to read/merge functions as part of their options.
+  public toReference = (
+    object: StoreObject,
+    selectionSet?: SelectionSetNode,
+    fragmentMap?: FragmentMap,
+  ) => {
+    const id = this.identify(object, selectionSet, fragmentMap);
+    return id && makeReference(id);
+  }
+
+  public identify(
+    object: StoreObject,
+    selectionSet?: SelectionSetNode,
+    fragmentMap?: FragmentMap,
+  ): string | null {
+    // TODO Consider subtypes?
+    // TODO Use an AliasMap here?
+    const typename = selectionSet && fragmentMap
+      ? getTypenameFromResult(object, selectionSet, fragmentMap)
+      : object.__typename;
+
+    const context = {
+      typename,
+      selectionSet,
+      fragmentMap,
+      policies: this,
+    };
+
+    let id: string | null;
+
+    const policy = this.getTypePolicy(typename, false);
+    const keyFn = policy && policy.keyFn;
+    if (keyFn) {
+      id = keyFn(object, context);
+    } else {
+      id = this.config.dataIdFromObject
+        ? this.config.dataIdFromObject(object, context)
+        : null;
+    }
+
+    return id && String(id);
+  }
+
+  public addTypePolicies(typePolicies: TypePolicies) {
+    Object.keys(typePolicies).forEach(typename => {
+      const existing = this.getTypePolicy(typename, true);
+      const incoming = typePolicies[typename];
+      const { keyFields, fields } = incoming;
+
+      if (incoming.queryType) this.setRootTypename("Query", typename);
+      if (incoming.mutationType) this.setRootTypename("Mutation", typename);
+      if (incoming.subscriptionType) this.setRootTypename("Subscription", typename);
+
+      existing.keyFn =
+        // Pass false to disable normalization for this typename.
+        keyFields === false ? nullKeyFieldsFn :
+        // Pass an array of strings to use those fields to compute a
+        // composite ID for objects of this typename.
+        Array.isArray(keyFields) ? keyFieldsFnFromSpecifier(keyFields) :
+        // Pass a function to take full control over identification.
+        typeof keyFields === "function" ? keyFields : void 0;
+
+      if (fields) {
+        Object.keys(fields).forEach(fieldName => {
+          const existing = this.getFieldPolicy(typename, fieldName, true);
+          const incoming = fields[fieldName];
+
+          if (typeof incoming === "function") {
+            existing.read = incoming;
+          } else {
+            const { keyArgs, read, merge } = incoming;
+
+            existing.keyFn =
+              // Pass false to disable argument-based differentiation of
+              // field identities.
+              keyArgs === false ? simpleKeyArgsFn :
+              // Pass an array of strings to use named arguments to
+              // compute a composite identity for the field.
+              Array.isArray(keyArgs) ? keyArgsFnFromSpecifier(keyArgs) :
+              // Pass a function to take full control over field identity.
+              typeof keyArgs === "function" ? keyArgs :
+              // Leave existing.keyFn unchanged if all above cases fail.
+              existing.keyFn;
+
+            if (typeof read === "function") existing.read = read;
+            if (typeof merge === "function") existing.merge = merge;
+          }
+
+          if (existing.read || existing.merge) {
+            // If we have a read or merge function, assume keyArgs:false
+            // unless existing.keyFn has already been explicitly set.
+            existing.keyFn = existing.keyFn || simpleKeyArgsFn;
+          }
+        });
+      }
+    });
+  }
+
+  private setRootTypename(
+    which: "Query" | "Mutation" | "Subscription",
+    typename: string,
+  ) {
+    const rootId = "ROOT_" + which.toUpperCase();
+    const old = this.rootTypenamesById[rootId];
+    if (typename !== old) {
+      invariant(old === which, `Cannot change root ${which} __typename more than once`);
+      (this.rootTypenamesById as any)[rootId] = typename;
+    }
+  }
+
+  public addPossibleTypes(possibleTypes: PossibleTypesMap) {
+    (this.usingPossibleTypes as boolean) = true;
+    Object.keys(possibleTypes).forEach(supertype => {
+      const subtypeSet = this.getSubtypeSet(supertype, true);
+      possibleTypes[supertype].forEach(subtypeSet.add, subtypeSet);
+    });
+  }
+
+  private getTypePolicy(
+    typename: string,
+    createIfMissing: boolean,
+  ): Policies["typePolicies"][string] {
+    const { typePolicies } = this;
+    return typePolicies[typename] || (
+      createIfMissing && (typePolicies[typename] = Object.create(null)));
+  }
+
+  private getSubtypeSet(
+    supertype: string,
+    createIfMissing: boolean,
+  ): Set<string> {
+    const policy = this.getTypePolicy(supertype, createIfMissing);
+    if (policy) {
+      return policy.subtypes || (
+        createIfMissing && (policy.subtypes = new Set<string>()));
+    }
+  }
+
+  private getFieldPolicy(
+    typename: string,
+    fieldName: string,
+    createIfMissing: boolean,
+  ): Policies["typePolicies"][string]["fields"][string] {
+    const typePolicy = this.getTypePolicy(typename, createIfMissing);
+    if (typePolicy) {
+      const fieldPolicies = typePolicy.fields || (
+        createIfMissing && (typePolicy.fields = Object.create(null)));
+      if (fieldPolicies) {
+        return fieldPolicies[fieldName] || (
+          createIfMissing && (fieldPolicies[fieldName] = Object.create(null)));
+      }
+    }
+  }
+
+  public fragmentMatches(
+    fragment: InlineFragmentNode | FragmentDefinitionNode,
+    typename: string,
+  ): boolean {
+    if (!fragment.typeCondition) return true;
+
+    // If the fragment has a type condition but the object we're matching
+    // against does not have a __typename, the fragment cannot match.
+    if (!typename) return false;
+
+    const supertype = fragment.typeCondition.name.value;
+    if (typename === supertype) return true;
+
+    if (this.usingPossibleTypes) {
+      const workQueue = [this.getSubtypeSet(supertype, false)];
+      // It's important to keep evaluating workQueue.length each time through
+      // the loop, because the queue can grow while we're iterating over it.
+      for (let i = 0; i < workQueue.length; ++i) {
+        const subtypes = workQueue[i];
+        if (subtypes) {
+          if (subtypes.has(typename)) return true;
+          subtypes.forEach(subtype => {
+            const subsubtypes = this.getSubtypeSet(subtype, false);
+            if (subsubtypes && workQueue.indexOf(subsubtypes) < 0) {
+              workQueue.push(subsubtypes);
+            }
+          });
+        }
+      }
+    }
+
+    return false;
+  }
+
+  public makeFieldValueGetter(store: NormalizedCache) {
+    const policies = this;
+
+    // Provides a uniform interface for reading field values, whether or not
+    // objectOrReference is a normalized entity.
+    return function getFieldValue<T = StoreValue>(
+      objectOrReference: StoreObject | Reference,
+      storeFieldName: string,
+    ): Readonly<T> {
+      let fieldValue: StoreValue;
+      if (isReference(objectOrReference)) {
+        const dataId = objectOrReference.__ref;
+        fieldValue = store.getFieldValue(dataId, storeFieldName);
+        if (fieldValue === void 0 && storeFieldName === "__typename") {
+          // We can infer the __typename of singleton root objects like
+          // ROOT_QUERY ("Query") and ROOT_MUTATION ("Mutation"), even if
+          // we have never written that information into the cache.
+          return policies.rootTypenamesById[dataId] as any;
+        }
+      } else {
+        fieldValue = objectOrReference && objectOrReference[storeFieldName];
+      }
+      if (process.env.NODE_ENV !== "production") {
+        // Enforce Readonly<T> at runtime, in development.
+        maybeDeepFreeze(fieldValue);
+      }
+      return fieldValue as T;
+    };
+  }
+
+  public getStoreFieldName(
+    typename: string | undefined,
+    field: FieldNode,
+    variables: Record<string, any>,
+  ): string {
+    const fieldName = field.name.value;
+    let storeFieldName: string | undefined;
+
+    if (typeof typename === "string") {
+      const policy = this.getFieldPolicy(typename, fieldName, false);
+      const keyFn = policy && policy.keyFn;
+      if (keyFn) {
+        // If the custom keyFn returns a falsy value, fall back to
+        // fieldName instead.
+        storeFieldName = keyFn(field, {
+          typename,
+          variables,
+          policies: this,
+        }) || fieldName;
+      }
+    }
+
+    if (storeFieldName === void 0) {
+      storeFieldName = storeKeyNameFromField(field, variables);
+    }
+
+    // Make sure custom field names start with the actual field.name.value
+    // of the field, so we can always figure out which properties of a
+    // StoreObject correspond to which original field names.
+    return fieldName === fieldNameFromStoreName(storeFieldName)
+      ? storeFieldName
+      : fieldName + ":" + storeFieldName;
+  }
+
+  private storageTrie = new KeyTrie<StorageType>(true);
+  private fieldDep = dep<StorageType>();
+
+  public readField<V = StoreValue>(
+    objectOrReference: StoreObject | Reference,
+    nameOrField: string | FieldNode,
+    getFieldValue: FieldValueGetter,
+    variables?: Record<string, any>,
+    typename = getFieldValue<string>(objectOrReference, "__typename"),
+  ): Readonly<V> {
+    invariant(
+      objectOrReference,
+      "Must provide an object or Reference when calling Policies#readField",
+    );
+
+    const policies = this;
+    const storeFieldName = typeof nameOrField === "string" ? nameOrField
+      : policies.getStoreFieldName(typename, nameOrField, variables);
+    const fieldName = fieldNameFromStoreName(storeFieldName);
+    const existing = getFieldValue<V>(objectOrReference, storeFieldName);
+    const policy = policies.getFieldPolicy(typename, fieldName, false);
+    const read = policy && policy.read;
+
+    if (read) {
+      const storage = policies.storageTrie.lookup(
+        isReference(objectOrReference)
+          ? objectOrReference.__ref
+          : objectOrReference,
+        storeFieldName,
+      );
+
+      // By depending on the options.storage object when we call
+      // policy.read, we can easily invalidate the result of the read
+      // function when/if the options.invalidate function is called.
+      policies.fieldDep(storage);
+
+      return read(existing, {
+        args: typeof nameOrField === "string" ? null :
+          argumentsObjectFromField(nameOrField, variables),
+        field: typeof nameOrField === "string" ? null : nameOrField,
+        fieldName,
+        variables,
+        policies,
+        isReference,
+        toReference: policies.toReference,
+        storage,
+        // I'm not sure why it's necessary to repeat the parameter types
+        // here, but TypeScript complains if I leave them out.
+        readField<T>(
+          nameOrField: string | FieldNode,
+          foreignObjOrRef?: Reference,
+        ) {
+          return policies.readField<T>(
+            foreignObjOrRef || objectOrReference,
+            nameOrField,
+            getFieldValue,
+            variables,
+          );
+        },
+        invalidate() {
+          policies.fieldDep.dirty(storage);
+        },
+      }) as Readonly<V>;
+    }
+
+    return existing;
+  }
+
+  public hasMergeFunction(
+    typename: string,
+    fieldName: string,
+  ) {
+    const policy = this.getFieldPolicy(typename, fieldName, false);
+    return !!(policy && policy.merge);
+  }
+
+  public applyMerges<T extends StoreValue>(
+    existing: T | Reference,
+    incoming: T | FieldValueToBeMerged,
+    getFieldValue: FieldValueGetter,
+    variables: Record<string, any>,
+    storageKeys?: [string | StoreObject, string],
+  ): T {
+    const policies = this;
+
+    if (isFieldValueToBeMerged(incoming)) {
+      const field = incoming.__field;
+      const fieldName = field.name.value;
+      const policy = policies.getFieldPolicy(
+        incoming.__typename, fieldName, false);
+
+      // The incoming data can have multiple layers of nested objects, so we
+      // need to handle child merges before handling parent merges. This
+      // post-order traversal also ensures that the incoming data passed to
+      // parent merge functions never contains any FieldValueToBeMerged
+      // objects for fields within child objects.
+      const applied = policies.applyMerges(
+        existing,
+        incoming.__value as T,
+        getFieldValue,
+        variables,
+        storageKeys,
+      );
+
+      const merge = policy && policy.merge;
+      if (merge) {
+        if (process.env.NODE_ENV !== "production") {
+          // It may be tempting to modify existing data directly, for example
+          // by pushing more elements onto an existing array, but merge
+          // functions are expected to be pure, so it's important that we
+          // enforce immutability in development.
+          maybeDeepFreeze(existing);
+        }
+
+        // If storage ends up null, that just means no options.storage object
+        // has ever been created for a read function for this field before, so
+        // there's nothing this merge function could do with options.storage
+        // that would help the read function do its work. Most merge functions
+        // will never need to worry about options.storage, but if you're
+        // reading this comment then you probably have good reasons for
+        // wanting to know esoteric details like these, you wizard, you.
+        const storage = storageKeys
+          ? policies.storageTrie.lookupArray(storageKeys)
+          : null;
+
+        return merge(existing, applied, {
+          args: argumentsObjectFromField(field, variables),
+          field,
+          fieldName,
+          variables,
+          policies,
+          isReference,
+          toReference: policies.toReference,
+          readField<T>(
+            nameOrField: string | FieldNode,
+            foreignObjOrRef: StoreObject | Reference,
+          ) {
+            // Unlike options.readField for read functions, we do not fall
+            // back to the current object if no foreignObjOrRef is provided,
+            // because it's not clear what the current object should be for
+            // merge functions: the (possibly undefined) existing object, or
+            // the incoming object? If you think your merge function needs
+            // to read sibling fields in order to produce a new value for
+            // the current field, you might want to rethink your strategy,
+            // because that's a recipe for making merge behavior sensitive
+            // to the order in which fields are written into the cache.
+            // However, readField(name, ref) is useful for merge functions
+            // that need to deduplicate child objects and references.
+            return policies.readField<T>(
+              foreignObjOrRef,
+              nameOrField,
+              getFieldValue,
+              variables,
+            );
+          },
+          storage,
+          invalidate() {
+            if (storage) {
+              policies.fieldDep.dirty(storage);
+            }
+          },
+        }) as T;
+      }
+
+      return applied;
+    }
+
+    if (incoming && typeof incoming === "object") {
+      const e = existing as StoreObject | Reference;
+      const i = incoming as object as StoreObject;
+
+      // If the existing object is a { __ref } object, e.__ref provides a
+      // stable key for looking up the storage object associated with
+      // e.__ref and storeFieldName. Otherwise, storage is enabled only if
+      // existing is actually a non-null object. It's less common for a
+      // merge function to use options.storage, but it's conceivable that a
+      // pair of read and merge functions might want to cooperate in
+      // managing their shared options.storage object.
+      const firstStorageKey = isReference(e)
+        ? e.__ref
+        : typeof e === "object" && e;
+
+      Object.keys(i).forEach(storeFieldName => {
+        i[storeFieldName] = policies.applyMerges(
+          getFieldValue(e, storeFieldName),
+          i[storeFieldName],
+          getFieldValue,
+          variables,
+          // Avoid enabling storage when firstStorageKey is falsy, which
+          // implies no options.storage object has ever been created for a
+          // read function for this field.
+          firstStorageKey && [firstStorageKey, storeFieldName],
+        );
+      });
+    }
+
+    return incoming;
+  }
+}
+
+function keyArgsFnFromSpecifier(
+  specifier: KeySpecifier,
+): KeyArgsFunction {
+  const topLevelArgNames: Record<string, true> = Object.create(null);
+
+  specifier.forEach(name => {
+    if (typeof name === "string") {
+      topLevelArgNames[name] = true;
+    }
+  });
+
+  return (field, context) => {
+    const fieldName = field.name.value;
+
+    if (field.arguments && field.arguments.length > 0) {
+      const args = Object.create(null);
+
+      field.arguments.forEach(arg => {
+        // Avoid converting arguments that were not mentioned in the specifier.
+        if (topLevelArgNames[arg.name.value] === true) {
+          valueToObjectRepresentation(args, arg.name, arg.value, context.variables);
+        }
+      });
+
+      return `${fieldName}:${
+        JSON.stringify(computeKeyObject(args, specifier))
+      }`;
+    }
+
+    return fieldName;
+  };
+}
+
+function keyFieldsFnFromSpecifier(
+  specifier: KeySpecifier,
+): KeyFieldsFunction {
+  const trie = new KeyTrie<{
+    aliasMap?: AliasMap;
+  }>(canUseWeakMap);
+
+  return (object, context) => {
+    let aliasMap: AliasMap;
+    if (context.selectionSet && context.fragmentMap) {
+      const info = trie.lookupArray([
+        context.selectionSet,
+        context.fragmentMap,
+      ]);
+      aliasMap = info.aliasMap || (
+        info.aliasMap = makeAliasMap(context.selectionSet, context.fragmentMap)
+      );
+    }
+    return `${context.typename}:${
+      JSON.stringify(computeKeyObject(object, specifier, aliasMap))
+    }`;
+  };
+}
+
+type AliasMap = {
+  // Map from store key to corresponding response key. Undefined when there are
+  // no aliased fields in this selection set.
+  aliases?: Record<string, string>;
+  // Map from store key to AliasMap correponding to a child selection set.
+  // Undefined when there are no child selection sets.
+  subsets?: Record<string, AliasMap>;
+};
+
+function makeAliasMap(
+  selectionSet: SelectionSetNode,
+  fragmentMap: FragmentMap,
+): AliasMap {
+  let map: AliasMap = Object.create(null);
+  // TODO Cache this work, perhaps by storing selectionSet._aliasMap?
+  const workQueue = new Set([selectionSet]);
+  workQueue.forEach(selectionSet => {
+    selectionSet.selections.forEach(selection => {
+      if (isField(selection)) {
+        if (selection.alias) {
+          const responseKey = selection.alias.value;
+          const storeKey = selection.name.value;
+          if (storeKey !== responseKey) {
+            const aliases = map.aliases || (map.aliases = Object.create(null));
+            aliases[storeKey] = responseKey;
+          }
+        }
+        if (selection.selectionSet) {
+          const subsets = map.subsets || (map.subsets = Object.create(null));
+          subsets[selection.name.value] =
+            makeAliasMap(selection.selectionSet, fragmentMap);
+        }
+      } else {
+        const fragment = getFragmentFromSelection(selection, fragmentMap);
+        workQueue.add(fragment.selectionSet);
+      }
+    });
+  });
+  return map;
+}
+
+function computeKeyObject(
+  response: Record<string, any>,
+  specifier: KeySpecifier,
+  aliasMap?: AliasMap,
+): Record<string, any> {
+  const keyObj = Object.create(null);
+  let prevKey: string | undefined;
+  specifier.forEach(s => {
+    if (Array.isArray(s)) {
+      if (typeof prevKey === "string") {
+        const subsets = aliasMap && aliasMap.subsets;
+        const subset = subsets && subsets[prevKey];
+        keyObj[prevKey] = computeKeyObject(response[prevKey], s, subset);
+      }
+    } else {
+      const aliases = aliasMap && aliasMap.aliases;
+      const responseName = aliases && aliases[s] || s;
+      invariant(
+        hasOwn.call(response, responseName),
+        // TODO Make this appropriate for keyArgs as well
+        `Missing field ${responseName} while computing key fields`,
+      );
+      keyObj[prevKey = s] = response[responseName];
+    }
+  });
+  return keyObj;
+}
diff --git a/src/cache/inmemory/readFromStore.ts b/src/cache/inmemory/readFromStore.ts
new file mode 100644
--- /dev/null
+++ b/src/cache/inmemory/readFromStore.ts
@@ -0,0 +1,393 @@
+import {
+  DocumentNode,
+  FieldNode,
+  FragmentDefinitionNode,
+  InlineFragmentNode,
+  SelectionSetNode,
+} from 'graphql';
+import { wrap } from 'optimism';
+import { invariant, InvariantError } from 'ts-invariant';
+
+import {
+  isField,
+  isInlineFragment,
+  resultKeyNameFromField,
+  Reference,
+  isReference,
+  makeReference,
+} from '../../utilities/graphql/storeUtils';
+import { createFragmentMap, FragmentMap } from '../../utilities/graphql/fragments';
+import { shouldInclude } from '../../utilities/graphql/directives';
+import {
+  getDefaultValues,
+  getFragmentDefinitions,
+  getMainDefinition,
+  getQueryDefinition,
+} from '../../utilities/graphql/getFromAST';
+import { maybeDeepFreeze } from '../../utilities/common/maybeDeepFreeze';
+import { mergeDeepArray } from '../../utilities/common/mergeDeep';
+import { Cache } from '../core/types/Cache';
+import {
+  DiffQueryAgainstStoreOptions,
+  ReadQueryOptions,
+  StoreObject,
+  NormalizedCache,
+} from './types';
+import { supportsResultCaching } from './entityStore';
+import { getTypenameFromStoreObject } from './helpers';
+import { Policies, FieldValueGetter } from './policies';
+
+export type VariableMap = { [name: string]: any };
+
+interface ExecContext {
+  query: DocumentNode;
+  store: NormalizedCache;
+  policies: Policies;
+  fragmentMap: FragmentMap;
+  variables: VariableMap;
+  getFieldValue: FieldValueGetter;
+};
+
+export type ExecResultMissingField = {
+  object: StoreObject;
+  fieldName: string;
+};
+
+export type ExecResult<R = any> = {
+  result: R;
+  // Empty array if no missing fields encountered while computing result.
+  missing?: ExecResultMissingField[];
+};
+
+type ExecSelectionSetOptions = {
+  selectionSet: SelectionSetNode;
+  objectOrReference: StoreObject | Reference;
+  context: ExecContext;
+};
+
+type ExecSubSelectedArrayOptions = {
+  field: FieldNode;
+  array: any[];
+  context: ExecContext;
+};
+
+export interface StoreReaderConfig {
+  addTypename?: boolean;
+  policies: Policies;
+}
+
+export class StoreReader {
+  constructor(private config: StoreReaderConfig) {
+    this.config = { addTypename: true, ...config };
+
+    const {
+      executeSelectionSet,
+      executeSubSelectedArray,
+    } = this;
+
+    this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
+      return executeSelectionSet.call(this, options);
+    }, {
+      makeCacheKey({
+        selectionSet,
+        objectOrReference,
+        context,
+      }: ExecSelectionSetOptions) {
+        if (supportsResultCaching(context.store)) {
+          return context.store.makeCacheKey(
+            selectionSet,
+            JSON.stringify(context.variables),
+            isReference(objectOrReference)
+              ? objectOrReference.__ref
+              : objectOrReference,
+          );
+        }
+      }
+    });
+
+    this.executeSubSelectedArray = wrap((options: ExecSubSelectedArrayOptions) => {
+      return executeSubSelectedArray.call(this, options);
+    }, {
+      makeCacheKey({ field, array, context }) {
+        if (supportsResultCaching(context.store)) {
+          return context.store.makeCacheKey(
+            field,
+            array,
+            JSON.stringify(context.variables),
+          );
+        }
+      }
+    });
+  }
+
+  /**
+   * Resolves the result of a query solely from the store (i.e. never hits the server).
+   *
+   * @param {Store} store The {@link NormalizedCache} used by Apollo for the `data` portion of the
+   * store.
+   *
+   * @param {DocumentNode} query The query document to resolve from the data available in the store.
+   *
+   * @param {Object} [variables] A map from the name of a variable to its value. These variables can
+   * be referenced by the query document.
+   */
+  public readQueryFromStore<QueryType>(
+    options: ReadQueryOptions,
+  ): QueryType | undefined {
+    return this.diffQueryAgainstStore<QueryType>({
+      ...options,
+      returnPartialData: false,
+    }).result;
+  }
+
+  /**
+   * Given a store and a query, return as much of the result as possible and
+   * identify if any data was missing from the store.
+   * @param  {DocumentNode} query A parsed GraphQL query document
+   * @param  {Store} store The Apollo Client store object
+   * @return {result: Object, complete: [boolean]}
+   */
+  public diffQueryAgainstStore<T>({
+    store,
+    query,
+    variables,
+    returnPartialData = true,
+    rootId = 'ROOT_QUERY',
+    config,
+  }: DiffQueryAgainstStoreOptions): Cache.DiffResult<T> {
+    const { policies } = this.config;
+
+    const execResult = this.executeSelectionSet({
+      selectionSet: getMainDefinition(query).selectionSet,
+      objectOrReference: makeReference(rootId),
+      context: {
+        store,
+        query,
+        policies,
+        variables: {
+          ...getDefaultValues(getQueryDefinition(query)),
+          ...variables,
+        },
+        fragmentMap: createFragmentMap(getFragmentDefinitions(query)),
+        getFieldValue: policies.makeFieldValueGetter(store),
+      },
+    });
+
+    const hasMissingFields =
+      execResult.missing && execResult.missing.length > 0;
+
+    if (hasMissingFields && ! returnPartialData) {
+      execResult.missing!.forEach(info => {
+        throw new InvariantError(`Can't find field ${
+          info.fieldName
+        } on object ${
+          JSON.stringify(info.object, null, 2)
+        }.`);
+      });
+    }
+
+    return {
+      result: execResult.result,
+      complete: !hasMissingFields,
+    };
+  }
+
+  private executeSelectionSet({
+    selectionSet,
+    objectOrReference,
+    context,
+  }: ExecSelectionSetOptions): ExecResult {
+    const { fragmentMap, variables, policies, getFieldValue } = context;
+    const objectsToMerge: { [key: string]: any }[] = [];
+    const finalResult: ExecResult = { result: null };
+    const typename = getFieldValue<string>(objectOrReference, "__typename");
+
+    if (this.config.addTypename &&
+        typeof typename === "string" &&
+        Object.values(
+          policies.rootTypenamesById
+        ).indexOf(typename) < 0) {
+      // Ensure we always include a default value for the __typename
+      // field, if we have one, and this.config.addTypename is true. Note
+      // that this field can be overridden by other merged objects.
+      objectsToMerge.push({ __typename: typename });
+    }
+
+    function getMissing() {
+      return finalResult.missing || (finalResult.missing = []);
+    }
+
+    function handleMissing<T>(result: ExecResult<T>): T {
+      if (result.missing) getMissing().push(...result.missing);
+      return result.result;
+    }
+
+    selectionSet.selections.forEach(selection => {
+      // Omit fields with directives @skip(if: <truthy value>) or
+      // @include(if: <falsy value>).
+      if (!shouldInclude(selection, variables)) return;
+
+      if (isField(selection)) {
+        let fieldValue = policies.readField(
+          objectOrReference,
+          selection,
+          getFieldValue,
+          variables,
+          typename,
+        );
+
+        if (fieldValue === void 0) {
+          getMissing().push({
+            object: objectOrReference as StoreObject,
+            fieldName: selection.name.value,
+          });
+
+        } else if (Array.isArray(fieldValue)) {
+          fieldValue = handleMissing(this.executeSubSelectedArray({
+            field: selection,
+            array: fieldValue,
+            context,
+          }));
+
+        } else if (!selection.selectionSet) {
+          // If the field does not have a selection set, then we handle it
+          // as a scalar value. However, that value should not contain any
+          // Reference objects, and should be frozen in development, if it
+          // happens to be an object that is mutable.
+          if (process.env.NODE_ENV !== 'production') {
+            assertSelectionSetForIdValue(
+              context.store,
+              selection,
+              fieldValue,
+            );
+            maybeDeepFreeze(fieldValue);
+          }
+
+        } else if (fieldValue != null) {
+          // In this case, because we know the field has a selection set,
+          // it must be trying to query a GraphQLObjectType, which is why
+          // fieldValue must be != null.
+          fieldValue = handleMissing(this.executeSelectionSet({
+            selectionSet: selection.selectionSet,
+            objectOrReference: fieldValue as StoreObject | Reference,
+            context,
+          }));
+        }
+
+        if (fieldValue !== void 0) {
+          objectsToMerge.push({
+            [resultKeyNameFromField(selection)]: fieldValue,
+          });
+        }
+
+      } else {
+        let fragment: InlineFragmentNode | FragmentDefinitionNode;
+
+        if (isInlineFragment(selection)) {
+          fragment = selection;
+        } else {
+          // This is a named fragment
+          invariant(
+            fragment = fragmentMap[selection.name.value],
+            `No fragment named ${selection.name.value}`,
+          );
+        }
+
+        if (policies.fragmentMatches(fragment, typename)) {
+          objectsToMerge.push(handleMissing(
+            this.executeSelectionSet({
+              selectionSet: fragment.selectionSet,
+              objectOrReference,
+              context,
+            })
+          ));
+        }
+      }
+    });
+
+    // Perform a single merge at the end so that we can avoid making more
+    // defensive shallow copies than necessary.
+    finalResult.result = mergeDeepArray(objectsToMerge);
+
+    if (process.env.NODE_ENV !== 'production') {
+      Object.freeze(finalResult.result);
+    }
+
+    return finalResult;
+  }
+
+  private executeSubSelectedArray({
+    field,
+    array,
+    context,
+  }: ExecSubSelectedArrayOptions): ExecResult {
+    let missing: ExecResultMissingField[] | undefined;
+
+    function handleMissing<T>(childResult: ExecResult<T>): T {
+      if (childResult.missing) {
+        missing = missing || [];
+        missing.push(...childResult.missing);
+      }
+
+      return childResult.result;
+    }
+
+    array = array.map(item => {
+      // null value in array
+      if (item === null) {
+        return null;
+      }
+
+      // This is a nested array, recurse
+      if (Array.isArray(item)) {
+        return handleMissing(this.executeSubSelectedArray({
+          field,
+          array: item,
+          context,
+        }));
+      }
+
+      // This is an object, run the selection set on it
+      if (field.selectionSet) {
+        return handleMissing(this.executeSelectionSet({
+          selectionSet: field.selectionSet,
+          objectOrReference: item,
+          context,
+        }));
+      }
+
+      if (process.env.NODE_ENV !== 'production') {
+        assertSelectionSetForIdValue(context.store, field, item);
+      }
+
+      return item;
+    });
+
+    if (process.env.NODE_ENV !== 'production') {
+      Object.freeze(array);
+    }
+
+    return { result: array, missing };
+  }
+}
+
+function assertSelectionSetForIdValue(
+  store: NormalizedCache,
+  field: FieldNode,
+  fieldValue: any,
+) {
+  if (!field.selectionSet) {
+    const workSet = new Set([fieldValue]);
+    workSet.forEach(value => {
+      if (value && typeof value === "object") {
+        invariant(
+          !isReference(value),
+          `Missing selection set for object of type ${
+            getTypenameFromStoreObject(store, value)
+          } returned for query field ${field.name.value}`,
+        );
+        Object.values(value).forEach(workSet.add, workSet);
+      }
+    });
+  }
+}
diff --git a/packages/apollo-cache-inmemory/src/types.ts b/src/cache/inmemory/types.ts
similarity index 65%
rename from packages/apollo-cache-inmemory/src/types.ts
rename to src/cache/inmemory/types.ts
--- a/packages/apollo-cache-inmemory/src/types.ts
+++ b/src/cache/inmemory/types.ts
@@ -1,12 +1,14 @@
 import { DocumentNode } from 'graphql';
-import { FragmentMatcher } from './readFromStore';
-import { Transaction } from 'apollo-cache';
-import { IdValue, StoreValue } from 'apollo-utilities';
+
+import { Transaction } from '../core/cache';
+import { StoreValue } from '../../utilities/graphql/storeUtils';
 
 export interface IdGetterObj extends Object {
   __typename?: string;
   id?: string;
+  _id?: string;
 }
+
 export declare type IdGetter = (
   value: IdGetterObj,
 ) => string | null | undefined;
@@ -16,9 +18,11 @@ export declare type IdGetter = (
  * StoreObjects from the cache
  */
 export interface NormalizedCache {
+  has(dataId: string): boolean;
   get(dataId: string): StoreObject;
-  set(dataId: string, value: StoreObject): void;
-  delete(dataId: string): void;
+  getFieldValue(dataId: string, storeFieldName: string): StoreValue;
+  merge(dataId: string, incoming: StoreObject): void;
+  delete(dataId: string, fieldName?: string): void;
   clear(): void;
 
   // non-Map elements:
@@ -30,6 +34,17 @@ export interface NormalizedCache {
    * replace the state of the store
    */
   replace(newData: NormalizedCacheObject): void;
+
+  /**
+   * Retain (or release) a given root ID to protect (or expose) it and its
+   * transitive child entities from (or to) garbage collection. The current
+   * retainment count is returned by both methods. Note that releasing a root
+   * ID does not cause that entity to be garbage collected, but merely removes
+   * it from the set of root IDs that will be considered during the next
+   * mark-and-sweep collection.
+   */
+  retain(rootId: string): number;
+  release(rootId: string): number;
 }
 
 /**
@@ -42,7 +57,7 @@ export interface NormalizedCacheObject {
 
 export interface StoreObject {
   __typename?: string;
-  [storeFieldKey: string]: StoreValue;
+  [storeFieldName: string]: StoreValue;
 }
 
 export type OptimisticStoreItem = {
@@ -54,7 +69,6 @@ export type OptimisticStoreItem = {
 export type ReadQueryOptions = {
   store: NormalizedCache;
   query: DocumentNode;
-  fragmentMatcherFunction?: FragmentMatcher;
   variables?: Object;
   previousResult?: any;
   rootId?: string;
@@ -67,37 +81,7 @@ export type DiffQueryAgainstStoreOptions = ReadQueryOptions & {
 
 export type ApolloReducerConfig = {
   dataIdFromObject?: IdGetter;
-  fragmentMatcher?: FragmentMatcherInterface;
   addTypename?: boolean;
-  cacheRedirects?: CacheResolverMap;
-};
-
-export type ReadStoreContext = {
-  readonly store: NormalizedCache;
-  readonly cacheRedirects: CacheResolverMap;
-  readonly dataIdFromObject?: IdGetter;
-};
-
-export interface FragmentMatcherInterface {
-  match(
-    idValue: IdValue,
-    typeCondition: string,
-    context: ReadStoreContext,
-  ): boolean | 'heuristic';
-}
-
-export type PossibleTypesMap = { [key: string]: string[] };
-
-export type IntrospectionResultData = {
-  __schema: {
-    types: {
-      kind: string;
-      name: string;
-      possibleTypes: {
-        name: string;
-      }[];
-    }[];
-  };
 };
 
 export type CacheResolver = (
diff --git a/src/cache/inmemory/writeToStore.ts b/src/cache/inmemory/writeToStore.ts
new file mode 100644
--- /dev/null
+++ b/src/cache/inmemory/writeToStore.ts
@@ -0,0 +1,299 @@
+import { SelectionSetNode, FieldNode, DocumentNode } from 'graphql';
+import { invariant } from 'ts-invariant';
+
+import {
+  createFragmentMap,
+  FragmentMap,
+  getFragmentFromSelection,
+} from '../../utilities/graphql/fragments';
+
+import {
+  getDefaultValues,
+  getFragmentDefinitions,
+  getOperationDefinition,
+} from '../../utilities/graphql/getFromAST';
+
+import {
+  getTypenameFromResult,
+  makeReference,
+  isField,
+  resultKeyNameFromField,
+  StoreValue,
+} from '../../utilities/graphql/storeUtils';
+
+import { shouldInclude } from '../../utilities/graphql/directives';
+import { cloneDeep } from '../../utilities/common/cloneDeep';
+
+import { Policies, FieldValueGetter } from './policies';
+import { defaultNormalizedCacheFactory } from './entityStore';
+import { NormalizedCache, StoreObject } from './types';
+import { makeProcessedFieldsMerger } from './helpers';
+
+export type WriteContext = {
+  readonly store: NormalizedCache;
+  readonly written: {
+    [dataId: string]: SelectionSetNode[];
+  };
+  readonly variables?: any;
+  readonly fragmentMap?: FragmentMap;
+  getFieldValue: FieldValueGetter;
+  // General-purpose deep-merge function for use during writes.
+  merge<T>(existing: T, incoming: T): T;
+};
+
+export interface StoreWriterConfig {
+  policies: Policies;
+};
+
+export class StoreWriter {
+  private policies: Policies;
+
+  constructor(config: StoreWriterConfig) {
+    this.policies = config.policies;
+  }
+
+  /**
+   * Writes the result of a query to the store.
+   *
+   * @param result The result object returned for the query document.
+   *
+   * @param query The query document whose result we are writing to the store.
+   *
+   * @param store The {@link NormalizedCache} used by Apollo for the `data` portion of the store.
+   *
+   * @param variables A map from the name of a variable to its value. These variables can be
+   * referenced by the query document.
+   */
+  public writeQueryToStore({
+    query,
+    result,
+    dataId = 'ROOT_QUERY',
+    store = defaultNormalizedCacheFactory(),
+    variables,
+  }: {
+    query: DocumentNode;
+    result: Object;
+    dataId?: string;
+    store?: NormalizedCache;
+    variables?: Object;
+  }): NormalizedCache {
+    const operationDefinition = getOperationDefinition(query)!;
+
+    // Any IDs written explicitly to the cache (including ROOT_QUERY, most
+    // frequently) will be retained as reachable root IDs on behalf of their
+    // owner DocumentNode objects, until/unless evicted for all owners.
+    store.retain(dataId);
+
+    const merger = makeProcessedFieldsMerger();
+
+    return this.writeSelectionSetToStore({
+      result: result || Object.create(null),
+      dataId,
+      selectionSet: operationDefinition.selectionSet,
+      context: {
+        store,
+        written: Object.create(null),
+        merge<T>(existing: T, incoming: T) {
+          return merger.merge(existing, incoming) as T;
+        },
+        variables: {
+          ...getDefaultValues(operationDefinition),
+          ...variables,
+        },
+        fragmentMap: createFragmentMap(getFragmentDefinitions(query)),
+        getFieldValue: this.policies.makeFieldValueGetter(store),
+      },
+    });
+  }
+
+  private writeSelectionSetToStore({
+    dataId,
+    result,
+    selectionSet,
+    context,
+  }: {
+    dataId: string;
+    result: Record<string, any>;
+    selectionSet: SelectionSetNode;
+    context: WriteContext;
+  }): NormalizedCache {
+    const { policies } = this;
+    const { store, written } = context;
+
+    // Avoid processing the same entity object using the same selection set
+    // more than once. We use an array instead of a Set since most entity IDs
+    // will be written using only one selection set, so the size of this array
+    // is likely to be very small, meaning indexOf is likely to be faster than
+    // Set.prototype.has.
+    const sets = written[dataId] || (written[dataId] = []);
+    if (sets.indexOf(selectionSet) >= 0) return store;
+    sets.push(selectionSet);
+
+    const entityRef = makeReference(dataId);
+    const typename =
+      // If the result has a __typename, trust that.
+      getTypenameFromResult(result, selectionSet, context.fragmentMap) ||
+      // If the entity identified by dataId has a __typename in the store,
+      // fall back to that.
+      context.getFieldValue<string>(entityRef, "__typename");
+
+    store.merge(
+      dataId,
+      policies.applyMerges(
+        entityRef,
+        this.processSelectionSet({
+          result,
+          selectionSet,
+          context,
+          typename,
+        }),
+        context.getFieldValue,
+        context.variables,
+      ),
+    );
+
+    return store;
+  }
+
+  private processSelectionSet({
+    result,
+    selectionSet,
+    context,
+    typename,
+  }: {
+    result: Record<string, any>;
+    selectionSet: SelectionSetNode;
+    context: WriteContext;
+    typename: string;
+  }): StoreObject {
+    let mergedFields: StoreObject = Object.create(null);
+    if (typeof typename === "string") {
+      mergedFields.__typename = typename;
+    }
+
+    selectionSet.selections.forEach(selection => {
+      if (!shouldInclude(selection, context.variables)) {
+        return;
+      }
+
+      const { policies } = this;
+
+      if (isField(selection)) {
+        const resultFieldKey = resultKeyNameFromField(selection);
+        const value = result[resultFieldKey];
+
+        if (typeof value !== 'undefined') {
+          const storeFieldName = policies.getStoreFieldName(
+            typename,
+            selection,
+            context.variables,
+          );
+
+          const incomingValue =
+            this.processFieldValue(value, selection, context);
+
+          mergedFields = context.merge(mergedFields, {
+            // If a custom merge function is defined for this field, store
+            // a special FieldValueToBeMerged object, so that we can run
+            // the merge function later, after all processSelectionSet
+            // work is finished.
+            [storeFieldName]: policies.hasMergeFunction(
+              typename,
+              selection.name.value,
+            ) ? {
+              __field: selection,
+              __typename: typename,
+              __value: incomingValue,
+            } : incomingValue,
+          });
+
+        } else if (
+          policies.usingPossibleTypes &&
+          !(
+            selection.directives &&
+            selection.directives.some(
+              ({ name }) =>
+                name && (name.value === 'defer' || name.value === 'client'),
+            )
+          )
+        ) {
+          // XXX We'd like to throw an error, but for backwards compatibility's sake
+          // we just print a warning for the time being.
+          //throw new WriteError(`Missing field ${resultFieldKey} in ${JSON.stringify(result, null, 2).substring(0, 100)}`);
+          invariant.warn(
+            `Missing field ${resultFieldKey} in ${JSON.stringify(
+              result,
+              null,
+              2,
+            ).substring(0, 100)}`,
+          );
+        }
+      } else {
+        // This is not a field, so it must be a fragment, either inline or named
+        const fragment = getFragmentFromSelection(
+          selection,
+          context.fragmentMap,
+        );
+
+        if (policies.fragmentMatches(fragment, typename)) {
+          mergedFields = context.merge(
+            mergedFields,
+            this.processSelectionSet({
+              result,
+              selectionSet: fragment.selectionSet,
+              context,
+              typename,
+            }),
+          );
+        }
+      }
+    });
+
+    return mergedFields;
+  }
+
+  private processFieldValue(
+    value: any,
+    field: FieldNode,
+    context: WriteContext,
+  ): StoreValue {
+    if (!field.selectionSet || value === null) {
+      // In development, we need to clone scalar values so that they can be
+      // safely frozen with maybeDeepFreeze in readFromStore.ts. In production,
+      // it's cheaper to store the scalar values directly in the cache.
+      return process.env.NODE_ENV === 'production' ? value : cloneDeep(value);
+    }
+
+    if (Array.isArray(value)) {
+      return value.map((item, i) => this.processFieldValue(item, field, context));
+    }
+
+    if (value) {
+      const dataId = this.policies.identify(
+        value,
+        // Since value is a result object rather than a normalized StoreObject,
+        // we need to consider aliases when computing its key fields.
+        field.selectionSet,
+        context.fragmentMap,
+      );
+
+      if (typeof dataId === 'string') {
+        this.writeSelectionSetToStore({
+          dataId,
+          result: value,
+          selectionSet: field.selectionSet,
+          context,
+        });
+        return makeReference(dataId);
+      }
+    }
+
+    return this.processSelectionSet({
+      result: value,
+      selectionSet: field.selectionSet,
+      context,
+      typename: getTypenameFromResult(
+        value, field.selectionSet, context.fragmentMap),
+    });
+  }
+}
diff --git a/packages/apollo-client/src/config/jest/setup.ts b/src/config/jest/setup.ts
similarity index 100%
rename from packages/apollo-client/src/config/jest/setup.ts
rename to src/config/jest/setup.ts
diff --git a/packages/apollo-client/src/core/LocalState.ts b/src/core/LocalState.ts
similarity index 95%
rename from packages/apollo-client/src/core/LocalState.ts
rename to src/core/LocalState.ts
--- a/packages/apollo-client/src/core/LocalState.ts
+++ b/src/core/LocalState.ts
@@ -10,30 +10,28 @@ import {
   ASTNode,
 } from 'graphql';
 import { visit, BREAK } from 'graphql/language/visitor';
+import { invariant } from 'ts-invariant';
 
-import { ApolloCache } from 'apollo-cache';
+import { ApolloCache } from '../cache/core/cache';
 import {
   getMainDefinition,
+  getFragmentDefinitions,
+} from '../utilities/graphql/getFromAST';
+import { hasDirectives, shouldInclude } from '../utilities/graphql/directives';
+import { FragmentMap, createFragmentMap } from '../utilities/graphql/fragments';
+import {
   buildQueryFromSelectionSet,
-  hasDirectives,
   removeClientSetsFromDocument,
-  mergeDeep,
-  mergeDeepArray,
-  FragmentMap,
+} from '../utilities/graphql/transform';
+import { mergeDeep, mergeDeepArray } from '../utilities/common/mergeDeep';
+import {
   argumentsObjectFromField,
   resultKeyNameFromField,
-  getFragmentDefinitions,
-  createFragmentMap,
-  shouldInclude,
   isField,
   isInlineFragment,
-} from 'apollo-utilities';
-
-import { invariant } from 'ts-invariant';
-
-import ApolloClient from '../ApolloClient';
+} from '../utilities/graphql/storeUtils';
+import { ApolloClient } from '../ApolloClient';
 import { Resolvers, OperationVariables } from './types';
-import { capitalizeFirstLetter } from '../util/capitalizeFirstLetter';
 
 export type Resolver = (
   rootValue?: any,
@@ -279,7 +277,8 @@ export class LocalState<TCacheShape> {
       .operation;
 
     const defaultOperationType = definitionOperation
-      ? capitalizeFirstLetter(definitionOperation)
+      ? definitionOperation.charAt(0).toUpperCase() +
+        definitionOperation.slice(1)
       : 'Query';
 
     const { cache, client } = this;
diff --git a/packages/apollo-client/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts
similarity index 84%
rename from packages/apollo-client/src/core/ObservableQuery.ts
rename to src/core/ObservableQuery.ts
--- a/packages/apollo-client/src/core/ObservableQuery.ts
+++ b/src/core/ObservableQuery.ts
@@ -1,12 +1,11 @@
-import {
-  isEqual,
-  tryFunctionOrLogError,
-  cloneDeep,
-  getOperationDefinition,
-} from 'apollo-utilities';
-import { GraphQLError } from 'graphql';
+import { invariant, InvariantError } from 'ts-invariant';
+import { equal } from '@wry/equality';
+
+import { tryFunctionOrLogError } from '../utilities/common/errorHandling';
+import { cloneDeep } from '../utilities/common/cloneDeep';
+import { getOperationDefinition } from '../utilities/graphql/getFromAST';
 import { NetworkStatus, isNetworkRequestInFlight } from './networkStatus';
-import { Observable, Observer, Subscription } from '../util/Observable';
+import { Observable, Observer, Subscription } from '../utilities/observables/Observable';
 import { ApolloError } from '../errors/ApolloError';
 import { QueryManager } from './QueryManager';
 import { ApolloQueryResult, FetchType, OperationVariables } from './types';
@@ -16,38 +15,18 @@ import {
   SubscribeToMoreOptions,
   ErrorPolicy,
 } from './watchQueryOptions';
-
 import { QueryStoreValue } from '../data/queries';
+import { isNonEmptyArray } from '../utilities/common/arrays';
 
-import { invariant, InvariantError } from 'ts-invariant';
-import { isNonEmptyArray } from '../util/arrays';
-
-// XXX remove in the next breaking semver change (3.0)
-// Deprecated, use ApolloCurrentQueryResult
-export type ApolloCurrentResult<T> = {
-  data: T | {};
-  errors?: ReadonlyArray<GraphQLError>;
-  loading: boolean;
-  networkStatus: NetworkStatus;
-  error?: ApolloError;
-  partial?: boolean;
-};
-
-export type ApolloCurrentQueryResult<T> = {
-  data: T | undefined;
-  errors?: ReadonlyArray<GraphQLError>;
-  loading: boolean;
-  networkStatus: NetworkStatus;
+export type ApolloCurrentQueryResult<T> = ApolloQueryResult<T> & {
   error?: ApolloError;
-  partial?: boolean;
-  stale?: boolean;
 };
 
 export interface FetchMoreOptions<
   TData = any,
   TVariables = OperationVariables
 > {
-  updateQuery: (
+  updateQuery?: (
     previousQueryResult: TData,
     options: {
       fetchMoreResult?: TData;
@@ -151,56 +130,44 @@ export class ObservableQuery<
     });
   }
 
-  // XXX remove in the next breaking semver change (3.0)
-  // Deprecated, use getCurrentResult()
-  public currentResult(): ApolloCurrentResult<TData> {
-    const result = this.getCurrentResult() as ApolloCurrentResult<TData>;
-    if (result.data === undefined) {
-      result.data = {};
-    }
-    return result;
-  }
-
-  /**
-   * Return the result of the query from the local cache as well as some fetching status
-   * `loading` and `networkStatus` allow to know if a request is in flight
-   * `partial` lets you know if the result from the local cache is complete or partial
-   * @return {data: Object, error: ApolloError, loading: boolean, networkStatus: number, partial: boolean}
-   */
   public getCurrentResult(): ApolloCurrentQueryResult<TData> {
-    if (this.isTornDown) {
-      const { lastResult } = this;
-      return {
-        data: !this.lastError && lastResult && lastResult.data || void 0,
-        error: this.lastError,
-        loading: false,
-        networkStatus: NetworkStatus.error,
-      };
-    }
-
-    const { data, partial } = this.queryManager.getCurrentQueryResult(this);
-    const queryStoreValue = this.queryManager.queryStore.get(this.queryId);
-    let result: ApolloQueryResult<TData>;
-
+    const { lastResult, lastError } = this;
     const { fetchPolicy } = this.options;
-
     const isNetworkFetchPolicy =
       fetchPolicy === 'network-only' ||
       fetchPolicy === 'no-cache';
 
+    const networkStatus =
+      lastError ? NetworkStatus.error :
+      lastResult ? lastResult.networkStatus :
+      isNetworkFetchPolicy ? NetworkStatus.loading :
+      NetworkStatus.ready;
+
+    const result: ApolloCurrentQueryResult<TData> = {
+      data: !lastError && lastResult && lastResult.data || void 0,
+      error: this.lastError,
+      loading: isNetworkRequestInFlight(networkStatus),
+      networkStatus,
+      stale: lastResult ? lastResult.stale : false,
+    };
+
+    if (this.isTornDown) {
+      return result;
+    }
+
+    const queryStoreValue = this.queryManager.queryStore.get(this.queryId);
     if (queryStoreValue) {
       const { networkStatus } = queryStoreValue;
 
       if (hasError(queryStoreValue, this.options.errorPolicy)) {
-        return {
+        return Object.assign(result, {
           data: void 0,
-          loading: false,
           networkStatus,
           error: new ApolloError({
             graphQLErrors: queryStoreValue.graphQLErrors,
             networkError: queryStoreValue.networkError,
           }),
-        };
+        });
       }
 
       // Variables might have been added dynamically at query time, when
@@ -216,38 +183,19 @@ export class ObservableQuery<
         this.variables = this.options.variables;
       }
 
-      result = {
-        data,
+      Object.assign(result, {
         loading: isNetworkRequestInFlight(networkStatus),
         networkStatus,
-      } as ApolloQueryResult<TData>;
+      });
 
       if (queryStoreValue.graphQLErrors && this.options.errorPolicy === 'all') {
         result.errors = queryStoreValue.graphQLErrors;
       }
-
-    } else {
-      // We need to be careful about the loading state we show to the user, to try
-      // and be vaguely in line with what the user would have seen from .subscribe()
-      // but to still provide useful information synchronously when the query
-      // will not end up hitting the server.
-      // See more: https://github.com/apollostack/apollo-client/issues/707
-      // Basically: is there a query in flight right now (modolo the next tick)?
-      const loading = isNetworkFetchPolicy ||
-        (partial && fetchPolicy !== 'cache-only');
-
-      result = {
-        data,
-        loading,
-        networkStatus: loading ? NetworkStatus.loading : NetworkStatus.ready,
-      } as ApolloQueryResult<TData>;
     }
 
-    if (!partial) {
-      this.updateLastResult({ ...result, stale: false });
-    }
+    this.updateLastResult(result);
 
-    return { ...result, partial };
+    return result;
   }
 
   // Compares newResult to the snapshot we took of this.lastResult when it was
@@ -259,7 +207,7 @@ export class ObservableQuery<
       newResult &&
       snapshot.networkStatus === newResult.networkStatus &&
       snapshot.stale === newResult.stale &&
-      isEqual(snapshot.data, newResult.data)
+      equal(snapshot.data, newResult.data)
     );
   }
 
@@ -312,7 +260,7 @@ export class ObservableQuery<
       fetchPolicy = 'network-only';
     }
 
-    if (!isEqual(this.variables, variables)) {
+    if (!equal(this.variables, variables)) {
       // update observable variables
       this.variables = {
         ...this.variables,
@@ -320,7 +268,7 @@ export class ObservableQuery<
       };
     }
 
-    if (!isEqual(this.options.variables, this.variables)) {
+    if (!equal(this.options.variables, this.variables)) {
       // Update the existing options with new variables
       this.options.variables = {
         ...this.options.variables,
@@ -339,12 +287,6 @@ export class ObservableQuery<
     fetchMoreOptions: FetchMoreQueryOptions<TVariables, K> &
       FetchMoreOptions<TData, TVariables>,
   ): Promise<ApolloQueryResult<TData>> {
-    // early return if no update Query
-    invariant(
-      fetchMoreOptions.updateQuery,
-      'updateQuery option is required. This function defines how to update the query data with the new results.',
-    );
-
     const combinedOptions = {
       ...(fetchMoreOptions.query ? fetchMoreOptions : {
         ...this.options,
@@ -368,12 +310,14 @@ export class ObservableQuery<
       )
       .then(
         fetchMoreResult => {
-          this.updateQuery((previousResult: any) =>
-            fetchMoreOptions.updateQuery(previousResult, {
-              fetchMoreResult: fetchMoreResult.data as TData,
+          this.updateQuery((previousResult: any) => {
+            const data = fetchMoreResult.data as TData;
+            const { updateQuery } = fetchMoreOptions;
+            return updateQuery ? updateQuery(previousResult, {
+              fetchMoreResult: data,
               variables: combinedOptions.variables as TVariables,
-            }),
-          );
+            }) : data;
+          });
           this.queryManager.stopQuery(qid);
           return fetchMoreResult as ApolloQueryResult<TData>;
         },
@@ -502,7 +446,7 @@ export class ObservableQuery<
 
     variables = variables || this.variables;
 
-    if (!tryFetch && isEqual(variables, this.variables)) {
+    if (!tryFetch && equal(variables, this.variables)) {
       // If we have no observers, then we don't actually want to make a network
       // request. As soon as someone observes the query, the request will kick
       // off. For now, we just store any changes. (See #1077)
@@ -545,11 +489,13 @@ export class ObservableQuery<
     );
 
     if (newResult) {
-      queryManager.dataStore.markUpdateQueryResult(
-        document,
+      queryManager.cache.write({
+        query: document,
+        result: newResult,
+        dataId: 'ROOT_QUERY',
         variables,
-        newResult,
-      );
+      });
+
       queryManager.broadcastQueries();
     }
   }
@@ -571,6 +517,9 @@ export class ObservableQuery<
     this.lastResultSnapshot = this.queryManager.assumeImmutableResults
       ? newResult
       : cloneDeep(newResult);
+    if (!isNonEmptyArray(newResult.errors)) {
+      delete this.lastError;
+    }
     return previousResult;
   }
 
@@ -651,7 +600,7 @@ export class ObservableQuery<
                 previousResult &&
                 fetchPolicy !== 'cache-only' &&
                 queryManager.transform(query).serverQuery &&
-                !isEqual(previousVariables, variables)
+                !equal(previousVariables, variables)
               ) {
                 this.refetch();
               } else {
diff --git a/packages/apollo-client/src/core/QueryManager.ts b/src/core/QueryManager.ts
similarity index 86%
rename from packages/apollo-client/src/core/QueryManager.ts
rename to src/core/QueryManager.ts
--- a/packages/apollo-client/src/core/QueryManager.ts
+++ b/src/core/QueryManager.ts
@@ -1,25 +1,31 @@
-import { execute, ApolloLink, FetchResult } from 'apollo-link';
 import { ExecutionResult, DocumentNode } from 'graphql';
-import { Cache } from 'apollo-cache';
+import { invariant, InvariantError } from 'ts-invariant';
+
+import { ApolloLink } from '../link/core/ApolloLink';
+import { execute } from '../link/core/execute';
+import { FetchResult } from '../link/core/types';
+import { Cache } from '../cache/core/types/Cache';
+import { DataProxy } from '../cache/core/types/DataProxy';
+
 import {
   getDefaultValues,
   getOperationDefinition,
   getOperationName,
+} from '../utilities/graphql/getFromAST';
+import {
   hasDirectives,
-  graphQLResultHasError,
   hasClientExports,
-  removeConnectionDirectiveFromDocument,
-  canUseWeakMap,
-} from 'apollo-utilities';
-
-import { invariant, InvariantError } from 'ts-invariant';
-
+} from '../utilities/graphql/directives';
+import {
+  graphQLResultHasError,
+  tryFunctionOrLogError,
+} from '../utilities/common/errorHandling';
+import { removeConnectionDirectiveFromDocument } from '../utilities/graphql/transform';
+import { canUseWeakMap } from '../utilities/common/canUse';
 import { isApolloError, ApolloError } from '../errors/ApolloError';
-import { Observer, Subscription, Observable } from '../util/Observable';
-import { QueryWithUpdater, DataStore } from '../data/store';
+import { Observer, Subscription, Observable } from '../utilities/observables/Observable';
 import { MutationStore } from '../data/mutations';
 import { QueryStore, QueryStoreValue } from '../data/queries';
-
 import {
   QueryOptions,
   WatchQueryOptions,
@@ -34,10 +40,12 @@ import {
   ApolloQueryResult,
   FetchType,
   OperationVariables,
+  MutationQueryReducer,
 } from './types';
 import { LocalState } from './LocalState';
-import { asyncMap, multiplex } from '../util/observables';
-import { isNonEmptyArray } from '../util/arrays';
+import { asyncMap, multiplex } from '../utilities/observables/observables';
+import { isNonEmptyArray } from '../utilities/common/arrays';
+import { ApolloCache } from '../cache/core/cache';
 
 const { hasOwnProperty } = Object.prototype;
 
@@ -55,11 +63,16 @@ export interface QueryInfo {
   cancel?: () => void;
 }
 
+type QueryWithUpdater = {
+  updater: MutationQueryReducer<Object>;
+  query: QueryStoreValue;
+};
+
 export class QueryManager<TStore> {
+  public cache: ApolloCache<TStore>;
   public link: ApolloLink;
   public mutationStore: MutationStore = new MutationStore();
   public queryStore: QueryStore = new QueryStore();
-  public dataStore: DataStore<TStore>;
   public readonly assumeImmutableResults: boolean;
 
   private queryDeduplication: boolean;
@@ -85,30 +98,30 @@ export class QueryManager<TStore> {
   private fetchQueryRejectFns = new Map<string, Function>();
 
   constructor({
+    cache,
     link,
     queryDeduplication = false,
-    store,
     onBroadcast = () => undefined,
     ssrMode = false,
     clientAwareness = {},
     localState,
     assumeImmutableResults,
   }: {
+    cache: ApolloCache<TStore>;
     link: ApolloLink;
     queryDeduplication?: boolean;
-    store: DataStore<TStore>;
     onBroadcast?: () => void;
     ssrMode?: boolean;
     clientAwareness?: Record<string, string>;
     localState?: LocalState<TStore>;
     assumeImmutableResults?: boolean;
   }) {
+    this.cache = cache;
     this.link = link;
     this.queryDeduplication = queryDeduplication;
-    this.dataStore = store;
     this.onBroadcast = onBroadcast;
     this.clientAwareness = clientAwareness;
-    this.localState = localState || new LocalState({ cache: store.getCache() });
+    this.localState = localState || new LocalState({ cache });
     this.ssrMode = ssrMode;
     this.assumeImmutableResults = !!assumeImmutableResults;
   }
@@ -194,14 +207,22 @@ export class QueryManager<TStore> {
       variables,
     );
 
-    this.dataStore.markMutationInit({
-      mutationId,
-      document: mutation,
-      variables,
-      updateQueries: generateUpdateQueriesInfo(),
-      update: updateWithProxyFn,
-      optimisticResponse,
-    });
+    if (optimisticResponse) {
+      const optimistic = typeof optimisticResponse === 'function'
+        ? optimisticResponse(variables)
+        : optimisticResponse;
+
+      this.cache.recordOptimisticTransaction(cache => {
+        markMutationResult({
+          mutationId: mutationId,
+          result: { data: optimistic },
+          document: mutation,
+          variables: variables,
+          queryUpdatersById: generateUpdateQueriesInfo(),
+          update: updateWithProxyFn,
+        }, cache);
+      }, mutationId);
+    }
 
     this.broadcastQueries();
 
@@ -231,14 +252,14 @@ export class QueryManager<TStore> {
           self.mutationStore.markMutationResult(mutationId);
 
           if (fetchPolicy !== 'no-cache') {
-            self.dataStore.markMutationResult({
+            markMutationResult({
               mutationId,
               result,
               document: mutation,
               variables,
-              updateQueries: generateUpdateQueriesInfo(),
+              queryUpdatersById: generateUpdateQueriesInfo(),
               update: updateWithProxyFn,
-            });
+            }, self.cache);
           }
 
           storeResult = result as FetchResult<T>;
@@ -246,10 +267,9 @@ export class QueryManager<TStore> {
 
         error(err: Error) {
           self.mutationStore.markMutationError(mutationId, err);
-          self.dataStore.markMutationComplete({
-            mutationId,
-            optimisticResponse,
-          });
+          if (optimisticResponse) {
+            self.cache.removeOptimistic(mutationId);
+          }
           self.broadcastQueries();
           self.setQuery(mutationId, () => ({ document: null }));
           reject(
@@ -264,10 +284,9 @@ export class QueryManager<TStore> {
             self.mutationStore.markMutationError(mutationId, error);
           }
 
-          self.dataStore.markMutationComplete({
-            mutationId,
-            optimisticResponse,
-          });
+          if (optimisticResponse) {
+            self.cache.removeOptimistic(mutationId);
+          }
 
           self.broadcastQueries();
 
@@ -366,7 +385,7 @@ export class QueryManager<TStore> {
     // Unless we are completely skipping the cache, we want to diff the query
     // against the cache before we fetch it from the network interface.
     if (!isNetworkOnly) {
-      const { complete, result } = this.dataStore.getCache().diff({
+      const { complete, result } = this.cache.diff({
         query,
         variables,
         returnPartialData: true,
@@ -412,9 +431,9 @@ export class QueryManager<TStore> {
       fetchMoreForQueryId,
     });
 
-    this.broadcastQueries();
-
     if (shouldFetch) {
+      this.broadcastQueries();
+
       const networkResult = this.fetchRequest<T>({
         requestId,
         queryId,
@@ -495,13 +514,22 @@ export class QueryManager<TStore> {
         newData: { result: result.data, complete: true },
       }));
     } else {
-      this.dataStore.markQueryResult(
-        result,
-        this.getQuery(queryId).document!,
-        variables,
-        fetchMoreForQueryId,
-        errorPolicy === 'ignore' || errorPolicy === 'all',
-      );
+      const document = this.getQuery(queryId).document!;
+      const ignoreErrors = errorPolicy === 'ignore' || errorPolicy === 'all';
+
+      let writeWithErrors = !graphQLResultHasError(result);
+      if (ignoreErrors && graphQLResultHasError(result) && result.data) {
+        writeWithErrors = true;
+      }
+
+      if (!fetchMoreForQueryId && writeWithErrors) {
+        this.cache.write({
+          result: result.data,
+          dataId: 'ROOT_QUERY',
+          query: document,
+          variables: variables,
+        });
+      }
     }
   }
 
@@ -607,7 +635,7 @@ export class QueryManager<TStore> {
             data = lastResult.data;
             isMissing = false;
           } else {
-            const diffResult = this.dataStore.getCache().diff({
+            const diffResult = this.cache.diff({
               query: document as DocumentNode,
               variables:
                 queryStoreValue.previousVariables ||
@@ -626,6 +654,7 @@ export class QueryManager<TStore> {
         // result and mark it as stale.
         const stale = isMissing && !(
           options.returnPartialData ||
+          options.partialRefetch ||
           fetchPolicy === 'cache-only'
         );
 
@@ -665,10 +694,9 @@ export class QueryManager<TStore> {
     const { transformCache } = this;
 
     if (!transformCache.has(document)) {
-      const cache = this.dataStore.getCache();
-      const transformed = cache.transformDocument(document);
+      const transformed = this.cache.transformDocument(document);
       const forLink = removeConnectionDirectiveFromDocument(
-        cache.transformForLink(transformed));
+        this.cache.transformForLink(transformed));
 
       const clientQuery = this.localState.clientQuery(transformed);
       const serverQuery = this.localState.serverQuery(forLink);
@@ -738,11 +766,28 @@ export class QueryManager<TStore> {
 
     let transformedOptions = { ...options } as WatchQueryOptions<TVariables>;
 
-    return new ObservableQuery<T, TVariables>({
+    const observable = new ObservableQuery<T, TVariables>({
       queryManager: this,
       options: transformedOptions,
       shouldSubscribe: shouldSubscribe,
     });
+
+    this.queryStore.initQuery({
+      queryId: observable.queryId,
+      document: this.transform(options.query).document,
+      variables: options.variables,
+      storePreviousVariables: false,
+      // Even if options.pollInterval is a number, we have not started
+      // polling this query yet (and we have not yet performed the first
+      // fetch), so NetworkStatus.loading (not NetworkStatus.poll or
+      // NetworkStatus.refetch) is the appropriate status for now.
+      isPoll: false,
+      isRefetch: false,
+      metadata: options.metadata,
+      fetchMoreForQueryId: void 0,
+    });
+
+    return observable;
   }
 
   public query<T>(options: QueryOptions): Promise<ApolloQueryResult<T>> {
@@ -826,7 +871,7 @@ export class QueryManager<TStore> {
 
       return previousResult;
     };
-    return this.dataStore.getCache().watch({
+    return this.cache.watch({
       query: document as DocumentNode,
       variables: options.variables,
       optimistic: true,
@@ -873,7 +918,7 @@ export class QueryManager<TStore> {
     this.mutationStore.reset();
 
     // begin removing data from the store
-    return this.dataStore.reset();
+    return this.cache.reset();
   }
 
   public resetStore(): Promise<ApolloQueryResult<any>[]> {
@@ -927,23 +972,6 @@ export class QueryManager<TStore> {
     return this.fetchQuery<T>(queryId, options);
   }
 
-  public startQuery<T>(
-    queryId: string,
-    options: WatchQueryOptions,
-    listener: QueryListener,
-  ) {
-    invariant.warn("The QueryManager.startQuery method has been deprecated");
-
-    this.addQueryListener(queryId, listener);
-
-    this.fetchQuery<T>(queryId, options)
-      // `fetchQuery` returns a Promise. In case of a failure it should be caucht or else the
-      // console will show an `Uncaught (in promise)` message. Ignore the error for now.
-      .catch(() => undefined);
-
-    return queryId;
-  }
-
   public startGraphQLSubscription<T = any>({
     query,
     fetchPolicy,
@@ -960,11 +988,17 @@ export class QueryManager<TStore> {
         false,
       ).map(result => {
         if (!fetchPolicy || fetchPolicy !== 'no-cache') {
-          this.dataStore.markSubscriptionResult(
-            result,
-            query,
-            variables,
-          );
+          // the subscription interface should handle not sending us results we no longer subscribe to.
+          // XXX I don't think we ever send in an object with errors, but we might in the future...
+          if (!graphQLResultHasError(result)) {
+            this.cache.write({
+              query,
+              result: result.data,
+              dataId: 'ROOT_SUBSCRIPTION',
+              variables: variables,
+            });
+          }
+
           this.broadcastQueries();
         }
 
@@ -1018,7 +1052,7 @@ export class QueryManager<TStore> {
     this.queries.delete(queryId);
   }
 
-  public getCurrentQueryResult<T>(
+  private getCurrentQueryResult<T>(
     observableQuery: ObservableQuery<T>,
     optimistic: boolean = true,
   ): {
@@ -1037,7 +1071,7 @@ export class QueryManager<TStore> {
       return { data: undefined, partial: false };
     }
 
-    const { result, complete } = this.dataStore.getCache().diff<T>({
+    const { result, complete } = this.cache.diff<T>({
       query,
       variables,
       previousResult: lastResult ? lastResult.data : undefined,
@@ -1060,14 +1094,14 @@ export class QueryManager<TStore> {
   } {
     let observableQuery: ObservableQuery<TData, any>;
     if (typeof queryIdOrObservable === 'string') {
-      const { observableQuery: foundObserveableQuery } = this.getQuery(
+      const { observableQuery: foundObservableQuery } = this.getQuery(
         queryIdOrObservable,
       );
       invariant(
-        foundObserveableQuery,
+        foundObservableQuery,
         `ObservableQuery with this id doesn't exist: ${queryIdOrObservable}`
       );
-      observableQuery = foundObserveableQuery!;
+      observableQuery = foundObservableQuery!;
     } else {
       observableQuery = queryIdOrObservable;
     }
@@ -1253,7 +1287,7 @@ export class QueryManager<TStore> {
           resultFromStore = result.data;
         } else {
           // ensure result is combined with data already in store
-          const { result, complete } = this.dataStore.getCache().diff<T>({
+          const { result, complete } = this.cache.diff<T>({
             variables,
             query: document,
             optimistic: false,
@@ -1406,3 +1440,73 @@ export class QueryManager<TStore> {
     this.pollingInfoByQueryId.delete(queryId);
   }
 }
+
+function markMutationResult<TStore>(
+  mutation: {
+    mutationId: string;
+    result: ExecutionResult;
+    document: DocumentNode;
+    variables: any;
+    queryUpdatersById: Record<string, QueryWithUpdater>;
+    update: ((proxy: DataProxy, mutationResult: Object) => void) | undefined;
+  },
+  cache: ApolloCache<TStore>,
+) {
+  // Incorporate the result from this mutation into the store
+  if (!graphQLResultHasError(mutation.result)) {
+    const cacheWrites: Cache.WriteOptions[] = [{
+      result: mutation.result.data,
+      dataId: 'ROOT_MUTATION',
+      query: mutation.document,
+      variables: mutation.variables,
+    }];
+
+    const { queryUpdatersById } = mutation;
+    if (queryUpdatersById) {
+      Object.keys(queryUpdatersById).forEach(id => {
+        const { query, updater } = queryUpdatersById[id];
+
+        // Read the current query result from the store.
+        const { result: currentQueryResult, complete } = cache.diff({
+          query: query.document,
+          variables: query.variables,
+          returnPartialData: true,
+          optimistic: false,
+        });
+
+        if (complete) {
+          // Run our reducer using the current query result and the mutation result.
+          const nextQueryResult = tryFunctionOrLogError(
+            () => updater(currentQueryResult, {
+              mutationResult: mutation.result,
+              queryName: getOperationName(query.document) || undefined,
+              queryVariables: query.variables,
+            }),
+          );
+
+          // Write the modified result back into the store if we got a new result.
+          if (nextQueryResult) {
+            cacheWrites.push({
+              result: nextQueryResult,
+              dataId: 'ROOT_QUERY',
+              query: query.document,
+              variables: query.variables,
+            });
+          }
+        }
+      });
+    }
+
+    cache.performTransaction(c => {
+      cacheWrites.forEach(write => c.write(write));
+
+      // If the mutation has some writes associated with it then we need to
+      // apply those writes to the store by running this reducer again with a
+      // write action.
+      const { update } = mutation;
+      if (update) {
+        tryFunctionOrLogError(() => update(c, mutation.result));
+      }
+    });
+  }
+}
diff --git a/src/core/index.ts b/src/core/index.ts
new file mode 100644
--- /dev/null
+++ b/src/core/index.ts
@@ -0,0 +1,77 @@
+/* Core */
+
+export {
+  ApolloClient,
+  ApolloClientOptions,
+  DefaultOptions
+} from '../ApolloClient';
+export {
+  ObservableQuery,
+  FetchMoreOptions,
+  UpdateQueryOptions,
+  ApolloCurrentQueryResult,
+} from '../core/ObservableQuery';
+export {
+  QueryBaseOptions,
+  QueryOptions,
+  WatchQueryOptions,
+  MutationOptions,
+  SubscriptionOptions,
+  FetchPolicy,
+  WatchQueryFetchPolicy,
+  ErrorPolicy,
+  FetchMoreQueryOptions,
+  SubscribeToMoreOptions,
+  MutationUpdaterFn,
+} from '../core/watchQueryOptions';
+export { NetworkStatus } from '../core/networkStatus';
+export * from '../core/types';
+export {
+  Resolver,
+  FragmentMatcher as LocalStateFragmentMatcher,
+} from '../core/LocalState';
+export { isApolloError, ApolloError } from '../errors/ApolloError';
+
+/* Cache */
+
+export * from '../cache';
+
+/* Link */
+
+export { empty } from '../link/core/empty';
+export { from } from '../link/core/from';
+export { split } from '../link/core/split';
+export { concat } from '../link/core/concat';
+export { execute } from '../link/core/execute';
+export { ApolloLink } from '../link/core/ApolloLink';
+export * from '../link/core/types';
+export {
+  parseAndCheckHttpResponse,
+  ServerParseError
+} from '../link/http/parseAndCheckHttpResponse';
+export {
+  serializeFetchParameter,
+  ClientParseError
+} from '../link/http/serializeFetchParameter';
+export {
+  HttpOptions,
+  fallbackHttpConfig,
+  selectHttpOptionsAndBody,
+  UriFunction
+} from '../link/http/selectHttpOptionsAndBody';
+export { checkFetcher } from '../link/http/checkFetcher';
+export { createSignalIfSupported } from '../link/http/createSignalIfSupported';
+export { selectURI } from '../link/http/selectURI';
+export { createHttpLink } from '../link/http/createHttpLink';
+export { HttpLink } from '../link/http/HttpLink';
+export { fromError } from '../link/utils/fromError';
+export { ServerError, throwServerError } from '../link/utils/throwServerError';
+
+/* Utilities */
+
+export { Observable } from '../utilities/observables/Observable';
+export { getMainDefinition } from '../utilities/graphql/getFromAST';
+
+/* Supporting */
+
+export { default as gql } from 'graphql-tag';
diff --git a/packages/apollo-client/src/core/networkStatus.ts b/src/core/networkStatus.ts
similarity index 100%
rename from packages/apollo-client/src/core/networkStatus.ts
rename to src/core/networkStatus.ts
diff --git a/packages/apollo-client/src/core/types.ts b/src/core/types.ts
similarity index 91%
rename from packages/apollo-client/src/core/types.ts
rename to src/core/types.ts
--- a/packages/apollo-client/src/core/types.ts
+++ b/src/core/types.ts
@@ -1,6 +1,6 @@
-import { FetchResult } from 'apollo-link';
 import { DocumentNode, GraphQLError } from 'graphql';
 
+import { FetchResult } from '../link/core/types';
 import { QueryStoreValue } from '../data/queries';
 import { NetworkStatus } from './networkStatus';
 import { Resolver } from './LocalState';
@@ -11,7 +11,7 @@ export type QueryListener = (
   forceResolvers?: boolean,
 ) => void;
 
-export type OperationVariables = { [key: string]: any };
+export type OperationVariables = Record<string, any>;
 
 export type PureQueryOptions = {
   query: DocumentNode;
diff --git a/packages/apollo-client/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts
similarity index 96%
rename from packages/apollo-client/src/core/watchQueryOptions.ts
rename to src/core/watchQueryOptions.ts
--- a/packages/apollo-client/src/core/watchQueryOptions.ts
+++ b/src/core/watchQueryOptions.ts
@@ -1,9 +1,8 @@
 import { DocumentNode, ExecutionResult } from 'graphql';
-import { FetchResult } from 'apollo-link';
-import { DataProxy } from 'apollo-cache';
 
+import { FetchResult } from '../link/core/types';
+import { DataProxy } from '../cache/core/types/DataProxy';
 import { MutationQueryReducersMap } from './types';
-
 import { PureQueryOptions, OperationVariables } from './types';
 
 /**
@@ -104,6 +103,13 @@ export interface ModifiableWatchQueryOptions<TVariables = OperationVariables>
    * be fully satisfied by the cache, instead of returning nothing.
    */
   returnPartialData?: boolean;
+
+  /**
+   * If `true`, perform a query `refetch` if the query result is marked as
+   * being partial, and the returned data is reset to an empty Object by the
+   * Apollo Client `QueryManager` (due to a cache miss).
+   */
+  partialRefetch?: boolean;
 }
 
 /**
diff --git a/packages/apollo-client/src/data/mutations.ts b/src/data/mutations.ts
similarity index 100%
rename from packages/apollo-client/src/data/mutations.ts
rename to src/data/mutations.ts
diff --git a/packages/apollo-client/src/data/queries.ts b/src/data/queries.ts
similarity index 96%
rename from packages/apollo-client/src/data/queries.ts
rename to src/data/queries.ts
--- a/packages/apollo-client/src/data/queries.ts
+++ b/src/data/queries.ts
@@ -1,8 +1,9 @@
 import { DocumentNode, GraphQLError, ExecutionResult } from 'graphql';
-import { isEqual } from 'apollo-utilities';
 import { invariant } from 'ts-invariant';
+
+import { equal } from '@wry/equality';
 import { NetworkStatus } from '../core/networkStatus';
-import { isNonEmptyArray } from '../util/arrays';
+import { isNonEmptyArray } from '../utilities/common/arrays';
 
 export type QueryStoreValue = {
   document: DocumentNode;
@@ -43,7 +44,7 @@ export class QueryStore {
     invariant(
       !previousQuery ||
       previousQuery.document === query.document ||
-      isEqual(previousQuery.document, query.document),
+      equal(previousQuery.document, query.document),
       'Internal Error: may not update existing query string in store',
     );
 
@@ -56,7 +57,7 @@ export class QueryStore {
       previousQuery.networkStatus !== NetworkStatus.loading
       // if the previous query was still loading, we don't want to remember it at all.
     ) {
-      if (!isEqual(previousQuery.variables, query.variables)) {
+      if (!equal(previousQuery.variables, query.variables)) {
         isSetVariables = true;
         previousVariables = previousQuery.variables;
       }
diff --git a/packages/apollo-client/src/errors/ApolloError.ts b/src/errors/ApolloError.ts
similarity index 97%
rename from packages/apollo-client/src/errors/ApolloError.ts
rename to src/errors/ApolloError.ts
--- a/packages/apollo-client/src/errors/ApolloError.ts
+++ b/src/errors/ApolloError.ts
@@ -1,5 +1,6 @@
 import { GraphQLError } from 'graphql';
-import { isNonEmptyArray } from '../util/arrays';
+
+import { isNonEmptyArray } from '../utilities/common/arrays';
 
 export function isApolloError(err: Error): err is ApolloError {
   return err.hasOwnProperty('graphQLErrors');
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,2 @@
+export * from './core';
+export * from './react';
diff --git a/src/link/core/ApolloLink.ts b/src/link/core/ApolloLink.ts
new file mode 100644
--- /dev/null
+++ b/src/link/core/ApolloLink.ts
@@ -0,0 +1,150 @@
+import { InvariantError, invariant } from 'ts-invariant';
+
+import { Observable } from '../../utilities/observables/Observable';
+import {
+  NextLink,
+  Operation,
+  RequestHandler,
+  FetchResult,
+  GraphQLRequest
+} from './types';
+import { validateOperation } from '../utils/validateOperation';
+import { createOperation } from '../utils/createOperation';
+import { transformOperation } from '../utils/transformOperation';
+
+function passthrough(op: Operation, forward: NextLink) {
+  return forward ? forward(op) : Observable.of();
+}
+
+function toLink(handler: RequestHandler | ApolloLink) {
+  return typeof handler === 'function' ? new ApolloLink(handler) : handler;
+}
+
+function isTerminating(link: ApolloLink): boolean {
+  return link.request.length <= 1;
+}
+
+class LinkError extends Error {
+  public link: ApolloLink;
+  constructor(message?: string, link?: ApolloLink) {
+    super(message);
+    this.link = link;
+  }
+}
+
+export class ApolloLink {
+  public static empty(): ApolloLink {
+    return new ApolloLink(() => Observable.of());
+  }
+
+  public static from(links: ApolloLink[]): ApolloLink {
+    if (links.length === 0) return ApolloLink.empty();
+    return links.map(toLink).reduce((x, y) => x.concat(y)) as ApolloLink;
+  }
+
+  public static split(
+    test: (op: Operation) => boolean,
+    left: ApolloLink | RequestHandler,
+    right?: ApolloLink | RequestHandler,
+  ): ApolloLink {
+    const leftLink = toLink(left);
+    const rightLink = toLink(right || new ApolloLink(passthrough));
+
+    if (isTerminating(leftLink) && isTerminating(rightLink)) {
+      return new ApolloLink(operation => {
+        return test(operation)
+          ? leftLink.request(operation) || Observable.of()
+          : rightLink.request(operation) || Observable.of();
+      });
+    } else {
+      return new ApolloLink((operation, forward) => {
+        return test(operation)
+          ? leftLink.request(operation, forward) || Observable.of()
+          : rightLink.request(operation, forward) || Observable.of();
+      });
+    }
+  }
+
+  public static execute(
+    link: ApolloLink,
+    operation: GraphQLRequest,
+  ): Observable<FetchResult> {
+    return (
+      link.request(
+        createOperation(
+          operation.context,
+          transformOperation(validateOperation(operation)),
+        ),
+      ) || Observable.of()
+    );
+  }
+
+  public static concat(
+    first: ApolloLink | RequestHandler,
+    second: ApolloLink | RequestHandler,
+  ) {
+    const firstLink = toLink(first);
+    if (isTerminating(firstLink)) {
+      invariant.warn(
+        new LinkError(
+          `You are calling concat on a terminating link, which will have no effect`,
+          firstLink,
+        ),
+      );
+      return firstLink;
+    }
+    const nextLink = toLink(second);
+
+    if (isTerminating(nextLink)) {
+      return new ApolloLink(
+        operation =>
+          firstLink.request(
+            operation,
+            op => nextLink.request(op) || Observable.of(),
+          ) || Observable.of(),
+      );
+    } else {
+      return new ApolloLink((operation, forward) => {
+        return (
+          firstLink.request(operation, op => {
+            return nextLink.request(op, forward) || Observable.of();
+          }) || Observable.of()
+        );
+      });
+    }
+  }
+
+  constructor(request?: RequestHandler) {
+    if (request) this.request = request;
+  }
+
+  public split(
+    test: (op: Operation) => boolean,
+    left: ApolloLink | RequestHandler,
+    right?: ApolloLink | RequestHandler,
+  ): ApolloLink {
+    return this.concat(
+      ApolloLink.split(test, left, right || new ApolloLink(passthrough))
+    );
+  }
+
+  public concat(next: ApolloLink | RequestHandler): ApolloLink {
+    return ApolloLink.concat(this, next);
+  }
+
+  public request(
+    operation: Operation,
+    forward?: NextLink,
+  ): Observable<FetchResult> | null {
+    throw new InvariantError('request is not implemented');
+  }
+
+  protected onError(reason: any) {
+    throw reason;
+  }
+
+  public setOnError(fn: (reason: any) => any): this {
+    this.onError = fn;
+    return this;
+  }
+}
diff --git a/src/link/core/concat.ts b/src/link/core/concat.ts
new file mode 100644
--- /dev/null
+++ b/src/link/core/concat.ts
@@ -0,0 +1,3 @@
+import { ApolloLink } from './ApolloLink';
+
+export const concat = ApolloLink.concat;
diff --git a/src/link/core/empty.ts b/src/link/core/empty.ts
new file mode 100644
--- /dev/null
+++ b/src/link/core/empty.ts
@@ -0,0 +1,3 @@
+import { ApolloLink } from './ApolloLink';
+
+export const empty = ApolloLink.empty;
diff --git a/src/link/core/execute.ts b/src/link/core/execute.ts
new file mode 100644
--- /dev/null
+++ b/src/link/core/execute.ts
@@ -0,0 +1,3 @@
+import { ApolloLink } from './ApolloLink';
+
+export const execute = ApolloLink.execute;
diff --git a/src/link/core/from.ts b/src/link/core/from.ts
new file mode 100644
--- /dev/null
+++ b/src/link/core/from.ts
@@ -0,0 +1,3 @@
+import { ApolloLink } from './ApolloLink';
+
+export const from = ApolloLink.from;
diff --git a/src/link/core/split.ts b/src/link/core/split.ts
new file mode 100644
--- /dev/null
+++ b/src/link/core/split.ts
@@ -0,0 +1,3 @@
+import { ApolloLink } from './ApolloLink';
+
+export const split = ApolloLink.split;
diff --git a/src/link/core/types.ts b/src/link/core/types.ts
new file mode 100644
--- /dev/null
+++ b/src/link/core/types.ts
@@ -0,0 +1,38 @@
+import { DocumentNode } from 'graphql/language/ast';
+import { ExecutionResult } from 'graphql/execution/execute';
+export { ExecutionResult, DocumentNode };
+
+import { Observable } from '../../utilities/observables/Observable';
+
+export interface GraphQLRequest {
+  query: DocumentNode;
+  variables?: Record<string, any>;
+  operationName?: string;
+  context?: Record<string, any>;
+  extensions?: Record<string, any>;
+}
+
+export interface Operation {
+  query: DocumentNode;
+  variables: Record<string, any>;
+  operationName: string;
+  extensions: Record<string, any>;
+  setContext: (context: Record<string, any>) => Record<string, any>;
+  getContext: () => Record<string, any>;
+}
+
+export type FetchResult<
+  TData = { [key: string]: any },
+  C = Record<string, any>,
+  E = Record<string, any>
+> = ExecutionResult<TData> & {
+  extensions?: E;
+  context?: C;
+};
+
+export type NextLink = (operation: Operation) => Observable<FetchResult>;
+
+export type RequestHandler = (
+  operation: Operation,
+  forward: NextLink,
+) => Observable<FetchResult> | null;
diff --git a/src/link/http/HttpLink.ts b/src/link/http/HttpLink.ts
new file mode 100644
--- /dev/null
+++ b/src/link/http/HttpLink.ts
@@ -0,0 +1,11 @@
+import { ApolloLink } from '../core/ApolloLink';
+import { RequestHandler } from '../core/types';
+import { HttpOptions } from './selectHttpOptionsAndBody';
+import { createHttpLink } from './createHttpLink';
+
+export class HttpLink extends ApolloLink {
+  public requester: RequestHandler;
+  constructor(public options: HttpOptions = {}) {
+    super(createHttpLink(options).request);
+  }
+}
diff --git a/src/link/http/checkFetcher.ts b/src/link/http/checkFetcher.ts
new file mode 100644
--- /dev/null
+++ b/src/link/http/checkFetcher.ts
@@ -0,0 +1,20 @@
+import { InvariantError } from 'ts-invariant';
+
+export const checkFetcher = (fetcher: WindowOrWorkerGlobalScope['fetch']) => {
+  if (!fetcher && typeof fetch === 'undefined') {
+    let library: string = 'unfetch';
+    if (typeof window === 'undefined') library = 'node-fetch';
+    throw new InvariantError(
+      '"fetch" has not been found globally and no fetcher has been ' +
+      'configured. To fix this, install a fetch package ' +
+      `(like https://www.npmjs.com/package/${library}), instantiate the ` +
+      'fetcher, and pass it into your `HttpLink` constructor. For example:' +
+      '\n\n' +
+      `import fetch from '${library}';\n` +
+      "import { ApolloClient, HttpLink } from '@apollo/client';\n" +
+      'const client = new ApolloClient({\n' +
+      "  link: new HttpLink({ uri: '/graphq', fetch })\n" +
+      '});'
+    );
+  }
+};
diff --git a/src/link/http/createHttpLink.ts b/src/link/http/createHttpLink.ts
new file mode 100644
--- /dev/null
+++ b/src/link/http/createHttpLink.ts
@@ -0,0 +1,181 @@
+import { DefinitionNode } from 'graphql';
+
+import { Observable } from '../../utilities/observables/Observable';
+import { serializeFetchParameter } from './serializeFetchParameter';
+import { selectURI } from './selectURI';
+import { parseAndCheckHttpResponse } from './parseAndCheckHttpResponse';
+import { checkFetcher } from './checkFetcher';
+import {
+  selectHttpOptionsAndBody,
+  fallbackHttpConfig,
+  HttpOptions
+} from './selectHttpOptionsAndBody';
+import { createSignalIfSupported } from './createSignalIfSupported';
+import { rewriteURIForGET } from './rewriteURIForGET';
+import { ApolloLink } from '../core/ApolloLink';
+import { fromError } from '../utils/fromError';
+
+export const createHttpLink = (linkOptions: HttpOptions = {}) => {
+  let {
+    uri = '/graphql',
+    // use default global fetch if nothing passed in
+    fetch: fetcher,
+    includeExtensions,
+    useGETForQueries,
+    ...requestOptions
+  } = linkOptions;
+
+  // dev warnings to ensure fetch is present
+  checkFetcher(fetcher);
+
+  //fetcher is set here rather than the destructuring to ensure fetch is
+  //declared before referencing it. Reference in the destructuring would cause
+  //a ReferenceError
+  if (!fetcher) {
+    fetcher = fetch;
+  }
+
+  const linkConfig = {
+    http: { includeExtensions },
+    options: requestOptions.fetchOptions,
+    credentials: requestOptions.credentials,
+    headers: requestOptions.headers,
+  };
+
+  return new ApolloLink(operation => {
+    let chosenURI = selectURI(operation, uri);
+
+    const context = operation.getContext();
+
+    // `apollographql-client-*` headers are automatically set if a
+    // `clientAwareness` object is found in the context. These headers are
+    // set first, followed by the rest of the headers pulled from
+    // `context.headers`. If desired, `apollographql-client-*` headers set by
+    // the `clientAwareness` object can be overridden by
+    // `apollographql-client-*` headers set in `context.headers`.
+    const clientAwarenessHeaders: {
+      'apollographql-client-name'?: string;
+      'apollographql-client-version'?: string;
+    } = {};
+
+    if (context.clientAwareness) {
+      const { name, version } = context.clientAwareness;
+      if (name) {
+        clientAwarenessHeaders['apollographql-client-name'] = name;
+      }
+      if (version) {
+        clientAwarenessHeaders['apollographql-client-version'] = version;
+      }
+    }
+
+    const contextHeaders = { ...clientAwarenessHeaders, ...context.headers };
+
+    const contextConfig = {
+      http: context.http,
+      options: context.fetchOptions,
+      credentials: context.credentials,
+      headers: contextHeaders,
+    };
+
+    //uses fallback, link, and then context to build options
+    const { options, body } = selectHttpOptionsAndBody(
+      operation,
+      fallbackHttpConfig,
+      linkConfig,
+      contextConfig,
+    );
+
+    let controller: any;
+    if (!(options as any).signal) {
+      const { controller: _controller, signal } = createSignalIfSupported();
+      controller = _controller;
+      if (controller) (options as any).signal = signal;
+    }
+
+    // If requested, set method to GET if there are no mutations.
+    const definitionIsMutation = (d: DefinitionNode) => {
+      return d.kind === 'OperationDefinition' && d.operation === 'mutation';
+    };
+    if (
+      useGETForQueries &&
+      !operation.query.definitions.some(definitionIsMutation)
+    ) {
+      options.method = 'GET';
+    }
+
+    if (options.method === 'GET') {
+      const { newURI, parseError } = rewriteURIForGET(chosenURI, body);
+      if (parseError) {
+        return fromError(parseError);
+      }
+      chosenURI = newURI;
+    } else {
+      try {
+        (options as any).body = serializeFetchParameter(body, 'Payload');
+      } catch (parseError) {
+        return fromError(parseError);
+      }
+    }
+
+    return new Observable(observer => {
+      fetcher(chosenURI, options)
+        .then(response => {
+          operation.setContext({ response });
+          return response;
+        })
+        .then(parseAndCheckHttpResponse(operation))
+        .then(result => {
+          // we have data and can send it to back up the link chain
+          observer.next(result);
+          observer.complete();
+          return result;
+        })
+        .catch(err => {
+          // fetch was cancelled so it's already been cleaned up in the unsubscribe
+          if (err.name === 'AbortError') return;
+          // if it is a network error, BUT there is graphql result info
+          // fire the next observer before calling error
+          // this gives apollo-client (and react-apollo) the `graphqlErrors` and `networErrors`
+          // to pass to UI
+          // this should only happen if we *also* have data as part of the response key per
+          // the spec
+          if (err.result && err.result.errors && err.result.data) {
+            // if we don't call next, the UI can only show networkError because AC didn't
+            // get any graphqlErrors
+            // this is graphql execution result info (i.e errors and possibly data)
+            // this is because there is no formal spec how errors should translate to
+            // http status codes. So an auth error (401) could have both data
+            // from a public field, errors from a private field, and a status of 401
+            // {
+            //  user { // this will have errors
+            //    firstName
+            //  }
+            //  products { // this is public so will have data
+            //    cost
+            //  }
+            // }
+            //
+            // the result of above *could* look like this:
+            // {
+            //   data: { products: [{ cost: "$10" }] },
+            //   errors: [{
+            //      message: 'your session has timed out',
+            //      path: []
+            //   }]
+            // }
+            // status code of above would be a 401
+            // in the UI you want to show data where you can, errors as data where you can
+            // and use correct http status codes
+            observer.next(err.result);
+          }
+          observer.error(err);
+        });
+
+      return () => {
+        // XXX support canceling this request
+        // https://developers.google.com/web/updates/2017/09/abortable-fetch
+        if (controller) controller.abort();
+      };
+    });
+  });
+};
diff --git a/src/link/http/createSignalIfSupported.ts b/src/link/http/createSignalIfSupported.ts
new file mode 100644
--- /dev/null
+++ b/src/link/http/createSignalIfSupported.ts
@@ -0,0 +1,8 @@
+export const createSignalIfSupported = () => {
+  if (typeof AbortController === 'undefined')
+    return { controller: false, signal: false };
+
+  const controller = new AbortController();
+  const signal = controller.signal;
+  return { controller, signal };
+};
diff --git a/src/link/http/parseAndCheckHttpResponse.ts b/src/link/http/parseAndCheckHttpResponse.ts
new file mode 100644
--- /dev/null
+++ b/src/link/http/parseAndCheckHttpResponse.ts
@@ -0,0 +1,57 @@
+import { Operation } from '../core/types';
+import { throwServerError } from '../utils/throwServerError';
+
+const { hasOwnProperty } = Object.prototype;
+
+export type ServerParseError = Error & {
+  response: Response;
+  statusCode: number;
+  bodyText: string;
+};
+
+export function parseAndCheckHttpResponse(
+  operations: Operation | Operation[],
+) {
+  return (response: Response) => response
+    .text()
+    .then(bodyText => {
+      try {
+        return JSON.parse(bodyText);
+      } catch (err) {
+        const parseError = err as ServerParseError;
+        parseError.name = 'ServerParseError';
+        parseError.response = response;
+        parseError.statusCode = response.status;
+        parseError.bodyText = bodyText;
+        throw parseError;
+      }
+    })
+    .then((result: any) => {
+      if (response.status >= 300) {
+        // Network error
+        throwServerError(
+          response,
+          result,
+          `Response not successful: Received status code ${response.status}`,
+        );
+      }
+
+      if (
+        !Array.isArray(result) &&
+        !hasOwnProperty.call(result, 'data') &&
+        !hasOwnProperty.call(result, 'errors')
+      ) {
+        // Data error
+        throwServerError(
+          response,
+          result,
+          `Server response was missing for query '${
+            Array.isArray(operations)
+              ? operations.map(op => op.operationName)
+              : operations.operationName
+          }'.`,
+        );
+      }
+      return result;
+    });
+}
diff --git a/src/link/http/rewriteURIForGET.ts b/src/link/http/rewriteURIForGET.ts
new file mode 100644
--- /dev/null
+++ b/src/link/http/rewriteURIForGET.ts
@@ -0,0 +1,62 @@
+import { serializeFetchParameter } from './serializeFetchParameter';
+import { Body } from './selectHttpOptionsAndBody';
+
+// For GET operations, returns the given URI rewritten with parameters, or a
+// parse error.
+export function rewriteURIForGET(chosenURI: string, body: Body) {
+  // Implement the standard HTTP GET serialization, plus 'extensions'. Note
+  // the extra level of JSON serialization!
+  const queryParams: string[] = [];
+  const addQueryParam = (key: string, value: string) => {
+    queryParams.push(`${key}=${encodeURIComponent(value)}`);
+  };
+
+  if ('query' in body) {
+    addQueryParam('query', body.query);
+  }
+  if (body.operationName) {
+    addQueryParam('operationName', body.operationName);
+  }
+  if (body.variables) {
+    let serializedVariables;
+    try {
+      serializedVariables = serializeFetchParameter(
+        body.variables,
+        'Variables map',
+      );
+    } catch (parseError) {
+      return { parseError };
+    }
+    addQueryParam('variables', serializedVariables);
+  }
+  if (body.extensions) {
+    let serializedExtensions;
+    try {
+      serializedExtensions = serializeFetchParameter(
+        body.extensions,
+        'Extensions map',
+      );
+    } catch (parseError) {
+      return { parseError };
+    }
+    addQueryParam('extensions', serializedExtensions);
+  }
+
+  // Reconstruct the URI with added query params.
+  // XXX This assumes that the URI is well-formed and that it doesn't
+  //     already contain any of these query params. We could instead use the
+  //     URL API and take a polyfill (whatwg-url@6) for older browsers that
+  //     don't support URLSearchParams. Note that some browsers (and
+  //     versions of whatwg-url) support URL but not URLSearchParams!
+  let fragment = '',
+    preFragment = chosenURI;
+  const fragmentStart = chosenURI.indexOf('#');
+  if (fragmentStart !== -1) {
+    fragment = chosenURI.substr(fragmentStart);
+    preFragment = chosenURI.substr(0, fragmentStart);
+  }
+  const queryParamsPrefix = preFragment.indexOf('?') === -1 ? '?' : '&';
+  const newURI =
+    preFragment + queryParamsPrefix + queryParams.join('&') + fragment;
+  return { newURI };
+}
diff --git a/src/link/http/selectHttpOptionsAndBody.ts b/src/link/http/selectHttpOptionsAndBody.ts
new file mode 100644
--- /dev/null
+++ b/src/link/http/selectHttpOptionsAndBody.ts
@@ -0,0 +1,138 @@
+import { print } from 'graphql/language/printer';
+
+import { Operation } from '../core/types';
+
+export interface UriFunction {
+  (operation: Operation): string;
+}
+
+export interface Body {
+  query?: string;
+  operationName?: string;
+  variables?: Record<string, any>;
+  extensions?: Record<string, any>;
+}
+
+export interface HttpOptions {
+  /**
+   * The URI to use when fetching operations.
+   *
+   * Defaults to '/graphql'.
+   */
+  uri?: string | UriFunction;
+
+  /**
+   * Passes the extensions field to your graphql server.
+   *
+   * Defaults to false.
+   */
+  includeExtensions?: boolean;
+
+  /**
+   * A `fetch`-compatible API to use when making requests.
+   */
+  fetch?: WindowOrWorkerGlobalScope['fetch'];
+
+  /**
+   * An object representing values to be sent as headers on the request.
+   */
+  headers?: any;
+
+  /**
+   * The credentials policy you want to use for the fetch call.
+   */
+  credentials?: string;
+
+  /**
+   * Any overrides of the fetch options argument to pass to the fetch call.
+   */
+  fetchOptions?: any;
+
+  /**
+   * If set to true, use the HTTP GET method for query operations. Mutations
+   * will still use the method specified in fetchOptions.method (which defaults
+   * to POST).
+   */
+  useGETForQueries?: boolean;
+}
+
+export interface HttpQueryOptions {
+  includeQuery?: boolean;
+  includeExtensions?: boolean;
+}
+
+export interface HttpConfig {
+  http?: HttpQueryOptions;
+  options?: any;
+  headers?: any;
+  credentials?: any;
+}
+
+const defaultHttpOptions: HttpQueryOptions = {
+  includeQuery: true,
+  includeExtensions: false,
+};
+
+const defaultHeaders = {
+  // headers are case insensitive (https://stackoverflow.com/a/5259004)
+  accept: '*/*',
+  'content-type': 'application/json',
+};
+
+const defaultOptions = {
+  method: 'POST',
+};
+
+export const fallbackHttpConfig = {
+  http: defaultHttpOptions,
+  headers: defaultHeaders,
+  options: defaultOptions,
+};
+
+export const selectHttpOptionsAndBody = (
+  operation: Operation,
+  fallbackConfig: HttpConfig,
+  ...configs: Array<HttpConfig>
+) => {
+  let options: HttpConfig & Record<string, any> = {
+    ...fallbackConfig.options,
+    headers: fallbackConfig.headers,
+    credentials: fallbackConfig.credentials,
+  };
+  let http: HttpQueryOptions = fallbackConfig.http;
+
+  /*
+   * use the rest of the configs to populate the options
+   * configs later in the list will overwrite earlier fields
+   */
+  configs.forEach(config => {
+    options = {
+      ...options,
+      ...config.options,
+      headers: {
+        ...options.headers,
+        ...config.headers,
+      },
+    };
+    if (config.credentials) options.credentials = config.credentials;
+
+    http = {
+      ...http,
+      ...config.http,
+    };
+  });
+
+  //The body depends on the http options
+  const { operationName, extensions, variables, query } = operation;
+  const body: Body = { operationName, variables };
+
+  if (http.includeExtensions) (body as any).extensions = extensions;
+
+  // not sending the query (i.e persisted queries)
+  if (http.includeQuery) (body as any).query = print(query);
+
+  return {
+    options,
+    body,
+  };
+};
diff --git a/src/link/http/selectURI.ts b/src/link/http/selectURI.ts
new file mode 100644
--- /dev/null
+++ b/src/link/http/selectURI.ts
@@ -0,0 +1,17 @@
+import { Operation } from '../core/types';
+
+export const selectURI = (
+  operation: Operation,
+  fallbackURI?: string | ((operation: Operation) => string),
+) => {
+  const context = operation.getContext();
+  const contextURI = context.uri;
+
+  if (contextURI) {
+    return contextURI;
+  } else if (typeof fallbackURI === 'function') {
+    return fallbackURI(operation);
+  } else {
+    return (fallbackURI as string) || '/graphql';
+  }
+};
diff --git a/src/link/http/serializeFetchParameter.ts b/src/link/http/serializeFetchParameter.ts
new file mode 100644
--- /dev/null
+++ b/src/link/http/serializeFetchParameter.ts
@@ -0,0 +1,19 @@
+import { InvariantError } from 'ts-invariant';
+
+export type ClientParseError = InvariantError & {
+  parseError: Error;
+};
+
+export const serializeFetchParameter = (p: any, label: string) => {
+  let serialized;
+  try {
+    serialized = JSON.stringify(p);
+  } catch (e) {
+    const parseError = new InvariantError(
+      `Network request failed. ${label} is not serializable: ${e.message}`,
+    ) as ClientParseError;
+    parseError.parseError = e;
+    throw parseError;
+  }
+  return serialized;
+};
diff --git a/src/link/utils/createOperation.ts b/src/link/utils/createOperation.ts
new file mode 100644
--- /dev/null
+++ b/src/link/utils/createOperation.ts
@@ -0,0 +1,28 @@
+import { GraphQLRequest, Operation } from '../core/types';
+
+export function createOperation(
+  starting: any,
+  operation: GraphQLRequest,
+): Operation {
+  let context = { ...starting };
+  const setContext = (next: any) => {
+    if (typeof next === 'function') {
+      context = { ...context, ...next(context) };
+    } else {
+      context = { ...context, ...next };
+    }
+  };
+  const getContext = () => ({ ...context });
+
+  Object.defineProperty(operation, 'setContext', {
+    enumerable: false,
+    value: setContext,
+  });
+
+  Object.defineProperty(operation, 'getContext', {
+    enumerable: false,
+    value: getContext,
+  });
+
+  return operation as Operation;
+}
diff --git a/src/link/utils/fromError.ts b/src/link/utils/fromError.ts
new file mode 100644
--- /dev/null
+++ b/src/link/utils/fromError.ts
@@ -0,0 +1,7 @@
+import { Observable } from '../../utilities/observables/Observable';
+
+export function fromError<T>(errorValue: any): Observable<T> {
+  return new Observable<T>(observer => {
+    observer.error(errorValue);
+  });
+}
diff --git a/src/link/utils/fromPromise.ts b/src/link/utils/fromPromise.ts
new file mode 100644
--- /dev/null
+++ b/src/link/utils/fromPromise.ts
@@ -0,0 +1,12 @@
+import { Observable } from '../../utilities/observables/Observable';
+
+export function fromPromise<T>(promise: Promise<T>): Observable<T> {
+  return new Observable<T>(observer => {
+    promise
+      .then((value: T) => {
+        observer.next(value);
+        observer.complete();
+      })
+      .catch(observer.error.bind(observer));
+  });
+}
diff --git a/src/link/utils/throwServerError.ts b/src/link/utils/throwServerError.ts
new file mode 100644
--- /dev/null
+++ b/src/link/utils/throwServerError.ts
@@ -0,0 +1,18 @@
+export type ServerError = Error & {
+  response: Response;
+  result: Record<string, any>;
+  statusCode: number;
+};
+
+export const throwServerError = (
+  response: Response,
+  result: any,
+  message: string
+) => {
+  const error = new Error(message) as ServerError;
+  error.name = 'ServerError';
+  error.response = response;
+  error.statusCode = response.status;
+  error.result = result;
+  throw error;
+};
diff --git a/src/link/utils/toPromise.ts b/src/link/utils/toPromise.ts
new file mode 100644
--- /dev/null
+++ b/src/link/utils/toPromise.ts
@@ -0,0 +1,22 @@
+import { invariant } from 'ts-invariant';
+
+import { Observable } from '../../utilities/observables/Observable';
+
+export function toPromise<R>(observable: Observable<R>): Promise<R> {
+  let completed = false;
+  return new Promise<R>((resolve, reject) => {
+    observable.subscribe({
+      next: data => {
+        if (completed) {
+          invariant.warn(
+            `Promise Wrapper does not support multiple results from Observable`,
+          );
+        } else {
+          completed = true;
+          resolve(data);
+        }
+      },
+      error: reject,
+    });
+  });
+}
diff --git a/src/link/utils/transformOperation.ts b/src/link/utils/transformOperation.ts
new file mode 100644
--- /dev/null
+++ b/src/link/utils/transformOperation.ts
@@ -0,0 +1,21 @@
+import { GraphQLRequest, Operation } from '../core/types';
+import { getOperationName } from '../../utilities/graphql/getFromAST';
+
+export function transformOperation(operation: GraphQLRequest): GraphQLRequest {
+  const transformedOperation: GraphQLRequest = {
+    variables: operation.variables || {},
+    extensions: operation.extensions || {},
+    operationName: operation.operationName,
+    query: operation.query,
+  };
+
+  // Best guess at an operation name
+  if (!transformedOperation.operationName) {
+    transformedOperation.operationName =
+      typeof transformedOperation.query !== 'string'
+        ? getOperationName(transformedOperation.query)
+        : '';
+  }
+
+  return transformedOperation as Operation;
+}
diff --git a/src/link/utils/validateOperation.ts b/src/link/utils/validateOperation.ts
new file mode 100644
--- /dev/null
+++ b/src/link/utils/validateOperation.ts
@@ -0,0 +1,20 @@
+import { InvariantError } from 'ts-invariant';
+
+import { GraphQLRequest } from '../core/types';
+
+export function validateOperation(operation: GraphQLRequest): GraphQLRequest {
+  const OPERATION_FIELDS = [
+    'query',
+    'operationName',
+    'variables',
+    'extensions',
+    'context',
+  ];
+  for (let key of Object.keys(operation)) {
+    if (OPERATION_FIELDS.indexOf(key) < 0) {
+      throw new InvariantError(`illegal argument: ${key}`);
+    }
+  }
+
+  return operation;
+}
diff --git a/src/react/context/ApolloContext.ts b/src/react/context/ApolloContext.ts
new file mode 100644
--- /dev/null
+++ b/src/react/context/ApolloContext.ts
@@ -0,0 +1,34 @@
+import { ApolloClient } from '../../ApolloClient';
+import { requireReactLazily } from '../react';
+
+export interface ApolloContextValue {
+  client?: ApolloClient<object>;
+  renderPromises?: Record<any, any>;
+}
+
+// To make sure Apollo Client doesn't create more than one React context
+// (which can lead to problems like having an Apollo Client instance added
+// in one context, then attempting to retrieve it from another different
+// context), a single Apollo context is created and tracked in global state.
+// Since the created context is React specific, we've decided to attach it to
+// the `React` object for sharing.
+
+const contextSymbol = Symbol.for('__APOLLO_CONTEXT__');
+
+export function resetApolloContext() {
+  const React = requireReactLazily();
+  Object.defineProperty(React, contextSymbol, {
+    value: React.createContext<ApolloContextValue>({}),
+    enumerable: false,
+    configurable: true,
+    writable: false,
+  });
+}
+
+export function getApolloContext() {
+  const React = requireReactLazily();
+  if (!(React as any)[contextSymbol]) {
+    resetApolloContext();
+  }
+  return (React as any)[contextSymbol] as React.Context<ApolloContextValue>;
+}
diff --git a/src/react/data/MutationData.ts b/src/react/data/MutationData.ts
new file mode 100644
--- /dev/null
+++ b/src/react/data/MutationData.ts
@@ -0,0 +1,184 @@
+import { equal } from '@wry/equality';
+
+import { DocumentType } from '../parser/parser';
+import { ApolloError } from '../../errors/ApolloError';
+import {
+  MutationDataOptions,
+  MutationTuple,
+  MutationFunctionOptions,
+  MutationResult
+} from '../types/types';
+import { OperationData } from './OperationData';
+import { OperationVariables } from '../../core/types';
+import { FetchResult } from '../../link/core/types';
+
+export class MutationData<
+  TData = any,
+  TVariables = OperationVariables
+> extends OperationData {
+  private mostRecentMutationId: number;
+  private result: MutationResult<TData>;
+  private previousResult?: MutationResult<TData>;
+  private setResult: (result: MutationResult<TData>) => any;
+
+  constructor({
+    options,
+    context,
+    result,
+    setResult
+  }: {
+    options: MutationDataOptions<TData, TVariables>;
+    context: any;
+    result: MutationResult<TData>;
+    setResult: (result: MutationResult<TData>) => any;
+  }) {
+    super(options, context);
+    this.verifyDocumentType(options.mutation, DocumentType.Mutation);
+    this.result = result;
+    this.setResult = setResult;
+    this.mostRecentMutationId = 0;
+  }
+
+  public execute(result: MutationResult<TData>) {
+    this.isMounted = true;
+    this.verifyDocumentType(this.getOptions().mutation, DocumentType.Mutation);
+    result.client = this.refreshClient().client;
+    return [this.runMutation, result] as MutationTuple<TData, TVariables>;
+  }
+
+  public afterExecute() {
+    this.isMounted = true;
+    return this.unmount.bind(this);
+  }
+
+  public cleanup() {
+    // No cleanup required.
+  }
+
+  private runMutation = (
+    mutationFunctionOptions: MutationFunctionOptions<
+      TData,
+      TVariables
+    > = {} as MutationFunctionOptions<TData, TVariables>
+  ) => {
+    this.onMutationStart();
+    const mutationId = this.generateNewMutationId();
+
+    return this.mutate(mutationFunctionOptions)
+      .then((response: FetchResult<TData>) => {
+        this.onMutationCompleted(response, mutationId);
+        return response;
+      })
+      .catch((error: ApolloError) => {
+        this.onMutationError(error, mutationId);
+        if (!this.getOptions().onError) throw error;
+      });
+  };
+
+  private mutate(
+    mutationFunctionOptions: MutationFunctionOptions<TData, TVariables>
+  ) {
+    const {
+      mutation,
+      variables,
+      optimisticResponse,
+      update,
+      context: mutationContext = {},
+      awaitRefetchQueries = false,
+      fetchPolicy
+    } = this.getOptions();
+    const mutateOptions = { ...mutationFunctionOptions };
+
+    const mutateVariables = Object.assign(
+      {},
+      variables,
+      mutateOptions.variables
+    );
+    delete mutateOptions.variables;
+
+    return this.refreshClient().client.mutate({
+      mutation,
+      optimisticResponse,
+      refetchQueries:
+        mutateOptions.refetchQueries || this.getOptions().refetchQueries,
+      awaitRefetchQueries,
+      update,
+      context: mutationContext,
+      fetchPolicy,
+      variables: mutateVariables,
+      ...mutateOptions
+    });
+  }
+
+  private onMutationStart() {
+    if (!this.result.loading && !this.getOptions().ignoreResults) {
+      this.updateResult({
+        loading: true,
+        error: undefined,
+        data: undefined,
+        called: true
+      });
+    }
+  }
+
+  private onMutationCompleted(
+    response: FetchResult<TData>,
+    mutationId: number
+  ) {
+    const { onCompleted, ignoreResults } = this.getOptions();
+
+    const { data, errors } = response;
+    const error =
+      errors && errors.length > 0
+        ? new ApolloError({ graphQLErrors: errors })
+        : undefined;
+
+    const callOncomplete = () =>
+      onCompleted ? onCompleted(data as TData) : null;
+
+    if (this.isMostRecentMutation(mutationId) && !ignoreResults) {
+      this.updateResult({
+        called: true,
+        loading: false,
+        data,
+        error
+      });
+    }
+    callOncomplete();
+  }
+
+  private onMutationError(error: ApolloError, mutationId: number) {
+    const { onError } = this.getOptions();
+
+    if (this.isMostRecentMutation(mutationId)) {
+      this.updateResult({
+        loading: false,
+        error,
+        data: undefined,
+        called: true
+      });
+    }
+
+    if (onError) {
+      onError(error);
+    }
+  }
+
+  private generateNewMutationId(): number {
+    return ++this.mostRecentMutationId;
+  }
+
+  private isMostRecentMutation(mutationId: number) {
+    return this.mostRecentMutationId === mutationId;
+  }
+
+  private updateResult(result: MutationResult<TData>) {
+    if (
+      this.isMounted &&
+      (!this.previousResult || !equal(this.previousResult, result))
+    ) {
+      this.setResult(result);
+      this.previousResult = result;
+    }
+  }
+}
diff --git a/src/react/data/OperationData.ts b/src/react/data/OperationData.ts
new file mode 100644
--- /dev/null
+++ b/src/react/data/OperationData.ts
@@ -0,0 +1,80 @@
+import { DocumentNode } from 'graphql';
+import { equal } from '@wry/equality';
+import { invariant } from 'ts-invariant';
+
+import { ApolloClient } from '../../ApolloClient';
+import { DocumentType, parser, operationName } from '../parser/parser';
+import { CommonOptions } from '../types/types';
+
+export abstract class OperationData<TOptions = any> {
+  public isMounted: boolean = false;
+  public previousOptions: CommonOptions<TOptions> = {} as CommonOptions<
+    TOptions
+  >;
+  public context: any = {};
+  public client: ApolloClient<object> | undefined;
+
+  private options: CommonOptions<TOptions> = {} as CommonOptions<TOptions>;
+
+  constructor(options?: CommonOptions<TOptions>, context?: any) {
+    this.options = options || ({} as CommonOptions<TOptions>);
+    this.context = context || {};
+  }
+
+  public getOptions(): CommonOptions<TOptions> {
+    return this.options;
+  }
+
+  public setOptions(
+    newOptions: CommonOptions<TOptions>,
+    storePrevious: boolean = false
+  ) {
+    if (storePrevious && !equal(this.options, newOptions)) {
+      this.previousOptions = this.options;
+    }
+    this.options = newOptions;
+  }
+
+  public abstract execute(...args: any): any;
+  public abstract afterExecute(...args: any): void | (() => void);
+  public abstract cleanup(): void;
+
+  protected unmount() {
+    this.isMounted = false;
+  }
+
+  protected refreshClient() {
+    const client =
+      (this.options && this.options.client) ||
+      (this.context && this.context.client);
+
+    invariant(
+      !!client,
+      'Could not find "client" in the context or passed in as an option. ' +
+        'Wrap the root component in an <ApolloProvider>, or pass an ' +
+        'ApolloClient instance in via options.'
+    );
+
+    let isNew = false;
+    if (client !== this.client) {
+      isNew = true;
+      this.client = client;
+      this.cleanup();
+    }
+    return {
+      client: this.client as ApolloClient<object>,
+      isNew
+    };
+  }
+
+  protected verifyDocumentType(document: DocumentNode, type: DocumentType) {
+    const operation = parser(document);
+    const requiredOperationName = operationName(type);
+    const usedOperationName = operationName(operation.type);
+    invariant(
+      operation.type === type,
+      `Running a ${requiredOperationName} requires a graphql ` +
+        `${requiredOperationName}, but a ${usedOperationName} was used instead.`
+    );
+  }
+}
diff --git a/src/react/data/QueryData.ts b/src/react/data/QueryData.ts
new file mode 100644
--- /dev/null
+++ b/src/react/data/QueryData.ts
@@ -0,0 +1,507 @@
+import { equal } from '@wry/equality';
+
+import { ApolloError } from '../../errors/ApolloError';
+import { NetworkStatus } from '../../core/networkStatus';
+import {
+  FetchMoreQueryOptions,
+  SubscribeToMoreOptions
+} from '../../core/watchQueryOptions';
+import {
+  FetchMoreOptions,
+  UpdateQueryOptions
+} from '../../core/ObservableQuery';
+import { DocumentType } from '../parser/parser';
+import {
+  QueryResult,
+  QueryPreviousData,
+  QueryDataOptions,
+  QueryCurrentObservable,
+  QueryTuple,
+  QueryLazyOptions,
+  ObservableQueryFields
+} from '../types/types';
+import { OperationData } from './OperationData';
+
+export class QueryData<TData, TVariables> extends OperationData {
+  private previousData: QueryPreviousData<TData, TVariables> = {};
+  private currentObservable: QueryCurrentObservable<TData, TVariables> = {};
+  private forceUpdate: any;
+
+  private runLazy: boolean = false;
+  private lazyOptions?: QueryLazyOptions<TVariables>;
+
+  constructor({
+    options,
+    context,
+    forceUpdate
+  }: {
+    options: QueryDataOptions<TData, TVariables>;
+    context: any;
+    forceUpdate: any;
+  }) {
+    super(options, context);
+    this.forceUpdate = forceUpdate;
+  }
+
+  public execute(): QueryResult<TData, TVariables> {
+    this.refreshClient();
+
+    const { skip, query } = this.getOptions();
+    if (skip || query !== this.previousData.query) {
+      this.removeQuerySubscription();
+      this.previousData.query = query;
+    }
+
+    this.updateObservableQuery();
+
+    if (this.isMounted) this.startQuerySubscription();
+
+    return this.getExecuteSsrResult() || this.getExecuteResult();
+  }
+
+  public executeLazy(): QueryTuple<TData, TVariables> {
+    return !this.runLazy
+      ? [
+          this.runLazyQuery,
+          {
+            loading: false,
+            networkStatus: NetworkStatus.ready,
+            called: false,
+            data: undefined
+          } as QueryResult<TData, TVariables>
+        ]
+      : [this.runLazyQuery, this.execute()];
+  }
+
+  // For server-side rendering
+  public fetchData(): Promise<void> | boolean {
+    const options = this.getOptions();
+    if (options.skip || options.ssr === false) return false;
+    return new Promise(resolve => this.startQuerySubscription(resolve));
+  }
+
+  public afterExecute({
+    queryResult,
+    lazy = false,
+  }: {
+    queryResult: QueryResult<TData, TVariables>;
+    lazy?: boolean;
+  }) {
+    this.isMounted = true;
+
+    if (!lazy || this.runLazy) {
+      this.handleErrorOrCompleted(queryResult);
+
+      // When the component is done rendering stored query errors, we'll
+      // remove those errors from the `ObservableQuery` query store, so they
+      // aren't re-displayed on subsequent (potentially error free)
+      // requests/responses.
+      setTimeout(() => {
+        this.currentObservable.query &&
+          this.currentObservable.query.resetQueryStoreErrors();
+      });
+    }
+
+    this.previousOptions = this.getOptions();
+    return this.unmount.bind(this);
+  }
+
+  public cleanup() {
+    this.removeQuerySubscription();
+    delete this.currentObservable.query;
+    delete this.previousData.result;
+  }
+
+  public getOptions() {
+    const options = super.getOptions();
+
+    if (this.lazyOptions) {
+      options.variables = {
+        ...options.variables,
+        ...this.lazyOptions.variables
+      };
+      options.context = {
+        ...options.context,
+        ...this.lazyOptions.context
+      };
+    }
+
+    // skip is not supported when using lazy query execution.
+    if (this.runLazy) {
+      delete options.skip;
+    }
+
+    return options;
+  }
+
+  private runLazyQuery = (options?: QueryLazyOptions<TVariables>) => {
+    this.cleanup();
+
+    this.runLazy = true;
+    this.lazyOptions = options;
+    this.forceUpdate();
+  };
+
+  private getExecuteResult(): QueryResult<TData, TVariables> {
+    const result = this.getQueryResult();
+    this.startQuerySubscription();
+    return result;
+  };
+
+  private getExecuteSsrResult() {
+    const treeRenderingInitiated = this.context && this.context.renderPromises;
+    const ssrDisabled = this.getOptions().ssr === false;
+    const fetchDisabled = this.refreshClient().client.disableNetworkFetches;
+
+    const ssrLoading = {
+      loading: true,
+      networkStatus: NetworkStatus.loading,
+      called: true,
+      data: undefined
+    } as QueryResult<TData, TVariables>;
+
+    // If SSR has been explicitly disabled, and this function has been called
+    // on the server side, return the default loading state.
+    if (ssrDisabled && (treeRenderingInitiated || fetchDisabled)) {
+      return ssrLoading;
+    }
+
+    let result;
+    if (treeRenderingInitiated) {
+      result =
+        this.context.renderPromises!.addQueryPromise(
+          this,
+          this.getQueryResult
+        ) || ssrLoading;
+    }
+
+    return result;
+  }
+
+  private prepareObservableQueryOptions() {
+    const options = this.getOptions();
+    this.verifyDocumentType(options.query, DocumentType.Query);
+    const displayName = options.displayName || 'Query';
+
+    // Set the fetchPolicy to cache-first for network-only and cache-and-network
+    // fetches for server side renders.
+    if (
+      this.context &&
+      this.context.renderPromises &&
+      (options.fetchPolicy === 'network-only' ||
+        options.fetchPolicy === 'cache-and-network')
+    ) {
+      options.fetchPolicy = 'cache-first';
+    }
+
+    return {
+      ...options,
+      displayName,
+      context: options.context,
+      metadata: { reactComponent: { displayName } }
+    };
+  }
+
+  private initializeObservableQuery() {
+    // See if there is an existing observable that was used to fetch the same
+    // data and if so, use it instead since it will contain the proper queryId
+    // to fetch the result set. This is used during SSR.
+    if (this.context && this.context.renderPromises) {
+      this.currentObservable.query = this.context.renderPromises.getSSRObservable(
+        this.getOptions()
+      );
+    }
+
+    if (!this.currentObservable.query) {
+      const observableQueryOptions = this.prepareObservableQueryOptions();
+
+      this.previousData.observableQueryOptions = {
+        ...observableQueryOptions,
+        children: null
+      };
+      this.currentObservable.query = this.refreshClient().client.watchQuery({
+        ...observableQueryOptions
+      });
+
+      if (this.context && this.context.renderPromises) {
+        this.context.renderPromises.registerSSRObservable(
+          this.currentObservable.query,
+          observableQueryOptions
+        );
+      }
+    }
+  }
+
+  private updateObservableQuery() {
+    // If we skipped initially, we may not have yet created the observable
+    if (!this.currentObservable.query) {
+      this.initializeObservableQuery();
+      return;
+    }
+
+    const newObservableQueryOptions = {
+      ...this.prepareObservableQueryOptions(),
+      children: null
+    };
+
+    if (
+      !equal(
+        newObservableQueryOptions,
+        this.previousData.observableQueryOptions
+      )
+    ) {
+      this.previousData.observableQueryOptions = newObservableQueryOptions;
+      this.currentObservable
+        .query!.setOptions(newObservableQueryOptions)
+        // The error will be passed to the child container, so we don't
+        // need to log it here. We could conceivably log something if
+        // an option was set. OTOH we don't log errors w/ the original
+        // query. See https://github.com/apollostack/react-apollo/issues/404
+        .catch(() => {});
+    }
+  }
+
+  // Setup a subscription to watch for Apollo Client `ObservableQuery` changes.
+  // When new data is received, and it doesn't match the data that was used
+  // during the last `QueryData.execute` call (and ultimately the last query
+  // component render), trigger the `onNewData` callback. If not specified,
+  // `onNewData` will trigger the `forceUpdate` function, which leads to a
+  // query component re-render.
+  private startQuerySubscription(onNewData: () => void = this.forceUpdate) {
+    if (this.currentObservable.subscription || this.getOptions().skip) return;
+
+    const obsQuery = this.currentObservable.query!;
+    this.currentObservable.subscription = obsQuery.subscribe({
+      next: ({ loading, networkStatus, data }) => {
+        const previousResult = this.previousData.result;
+
+        // Make sure we're not attempting to re-render similar results
+        if (
+          previousResult &&
+          previousResult.loading === loading &&
+          previousResult.networkStatus === networkStatus &&
+          equal(previousResult.data, data)
+        ) {
+          return;
+        }
+
+        // If we skipped previously, `previousResult.data` is set to undefined.
+        // When this subscription is run after skipping, Apollo Client sends
+        // the last query result data alongside the `loading` true state. This
+        // means the previous skipped `data` of undefined and the incoming
+        // data won't match, which would normally mean we want to trigger a
+        // render to show the new data. In this case however we're already
+        // showing the loading state, and want to avoid triggering an
+        // additional and unnecessary render showing the same loading state.
+        if (this.previousOptions.skip) {
+          return;
+        }
+
+        onNewData();
+      },
+      error: error => {
+        this.resubscribeToQuery();
+        if (!error.hasOwnProperty('graphQLErrors')) throw error;
+
+        const previousResult = this.previousData.result;
+        if (
+          (previousResult && previousResult.loading) ||
+          !equal(error, this.previousData.error)
+        ) {
+          this.previousData.error = error;
+          onNewData();
+        }
+      }
+    });
+  }
+
+  private resubscribeToQuery() {
+    this.removeQuerySubscription();
+
+    // Unfortunately, if `lastError` is set in the current
+    // `observableQuery` when the subscription is re-created,
+    // the subscription will immediately receive the error, which will
+    // cause it to terminate again. To avoid this, we first clear
+    // the last error/result from the `observableQuery` before re-starting
+    // the subscription, and restore it afterwards (so the subscription
+    // has a chance to stay open).
+    const lastError = this.currentObservable.query!.getLastError();
+    const lastResult = this.currentObservable.query!.getLastResult();
+    this.currentObservable.query!.resetLastResults();
+    this.startQuerySubscription();
+    Object.assign(this.currentObservable.query!, {
+      lastError,
+      lastResult
+    });
+  }
+
+  private getQueryResult = (): QueryResult<TData, TVariables> => {
+    let result: any = this.observableQueryFields();
+    const options = this.getOptions();
+
+    // When skipping a query (ie. we're not querying for data but still want
+    // to render children), make sure the `data` is cleared out and
+    // `loading` is set to `false` (since we aren't loading anything).
+    if (options.skip) {
+      result = {
+        ...result,
+        data: undefined,
+        error: undefined,
+        loading: false,
+        called: true
+      };
+    } else {
+      // Fetch the current result (if any) from the store.
+      const currentResult = this.currentObservable.query!.getCurrentResult();
+      const { loading, networkStatus, errors } = currentResult;
+      let { error, data } = currentResult;
+
+      // Until a set naming convention for networkError and graphQLErrors is
+      // decided upon, we map errors (graphQLErrors) to the error options.
+      if (errors && errors.length > 0) {
+        error = new ApolloError({ graphQLErrors: errors });
+      }
+
+      result = {
+        ...result,
+        loading,
+        networkStatus,
+        error,
+        called: true
+      };
+
+      if (loading) {
+        const previousData =
+          this.previousData.result && this.previousData.result.data;
+        result.data =
+          previousData && data
+            ? {
+                ...previousData,
+                ...data
+              }
+            : previousData || data;
+      } else if (error) {
+        Object.assign(result, {
+          data: (this.currentObservable.query!.getLastResult() || ({} as any))
+            .data
+        });
+      } else {
+        const { fetchPolicy } = this.currentObservable.query!.options;
+        const { partialRefetch } = options;
+        if (
+          partialRefetch &&
+          (!data || Object.keys(data).length === 0) &&
+          fetchPolicy !== 'cache-only'
+        ) {
+          // When a `Query` component is mounted, and a mutation is executed
+          // that returns the same ID as the mounted `Query`, but has less
+          // fields in its result, Apollo Client's `QueryManager` returns the
+          // data as `undefined` since a hit can't be found in the cache.
+          // This can lead to application errors when the UI elements rendered by
+          // the original `Query` component are expecting certain data values to
+          // exist, and they're all of a sudden stripped away. To help avoid
+          // this we'll attempt to refetch the `Query` data.
+          Object.assign(result, {
+            loading: true,
+            networkStatus: NetworkStatus.loading
+          });
+          result.refetch();
+          return result;
+        }
+
+        result.data = data;
+      }
+    }
+
+    result.client = this.client;
+    // Store options as this.previousOptions.
+    this.setOptions(options, true);
+    this.previousData.loading =
+      this.previousData.result && this.previousData.result.loading || false;
+    return this.previousData.result = result;
+  }
+
+  private handleErrorOrCompleted({
+    data,
+    loading,
+    error,
+  }: QueryResult<TData, TVariables>) {
+    if (!loading) {
+      const { query, variables, onCompleted, onError } = this.getOptions();
+
+      // No changes, so we won't call onError/onCompleted.
+      if (
+        this.previousOptions &&
+        !this.previousData.loading &&
+        equal(this.previousOptions.query, query) &&
+        equal(this.previousOptions.variables, variables)
+      ) {
+        return;
+      }
+
+      if (onCompleted && !error) {
+        onCompleted(data);
+      } else if (onError && error) {
+        onError(error);
+      }
+    }
+  }
+
+  private removeQuerySubscription() {
+    if (this.currentObservable.subscription) {
+      this.currentObservable.subscription.unsubscribe();
+      delete this.currentObservable.subscription;
+    }
+  }
+
+  private obsRefetch = (variables?: TVariables) =>
+    this.currentObservable.query!.refetch(variables);
+
+  private obsFetchMore = <K extends keyof TVariables>(
+    fetchMoreOptions: FetchMoreQueryOptions<TVariables, K> &
+      FetchMoreOptions<TData, TVariables>
+  ) => this.currentObservable.query!.fetchMore(fetchMoreOptions);
+
+  private obsUpdateQuery = <TVars = TVariables>(
+    mapFn: (
+      previousQueryResult: TData,
+      options: UpdateQueryOptions<TVars>
+    ) => TData
+  ) => this.currentObservable.query!.updateQuery(mapFn);
+
+  private obsStartPolling = (pollInterval: number) => {
+    this.currentObservable &&
+      this.currentObservable.query! &&
+      this.currentObservable.query!.startPolling(pollInterval);
+  };
+
+  private obsStopPolling = () => {
+    this.currentObservable &&
+      this.currentObservable.query! &&
+      this.currentObservable.query!.stopPolling();
+  };
+
+  private obsSubscribeToMore = <
+    TSubscriptionData = TData,
+    TSubscriptionVariables = TVariables
+  >(
+    options: SubscribeToMoreOptions<
+      TData,
+      TSubscriptionVariables,
+      TSubscriptionData
+    >
+  ) => this.currentObservable.query!.subscribeToMore(options);
+
+  private observableQueryFields() {
+    const observable = this.currentObservable.query!;
+    return {
+      variables: observable.variables,
+      refetch: this.obsRefetch,
+      fetchMore: this.obsFetchMore,
+      updateQuery: this.obsUpdateQuery,
+      startPolling: this.obsStartPolling,
+      stopPolling: this.obsStopPolling,
+      subscribeToMore: this.obsSubscribeToMore
+    } as ObservableQueryFields<TData, TVariables>;
+  }
+}
diff --git a/src/react/data/SubscriptionData.ts b/src/react/data/SubscriptionData.ts
new file mode 100644
--- /dev/null
+++ b/src/react/data/SubscriptionData.ts
@@ -0,0 +1,150 @@
+import { equal } from '@wry/equality';
+
+import { OperationData } from './OperationData';
+import {
+  SubscriptionCurrentObservable,
+  SubscriptionDataOptions,
+  SubscriptionResult
+} from '../types/types';
+
+export class SubscriptionData<
+  TData = any,
+  TVariables = any
+> extends OperationData<SubscriptionDataOptions<TData, TVariables>> {
+  private setResult: any;
+  private currentObservable: SubscriptionCurrentObservable = {};
+
+  constructor({
+    options,
+    context,
+    setResult
+  }: {
+    options: SubscriptionDataOptions<TData, TVariables>;
+    context: any;
+    setResult: any;
+  }) {
+    super(options, context);
+    this.setResult = setResult;
+    this.initialize(options);
+  }
+
+  public execute(result: SubscriptionResult<TData>) {
+    if (this.getOptions().skip === true) {
+      this.cleanup();
+      return {
+        loading: false,
+        error: undefined,
+        data: undefined,
+        variables: this.getOptions().variables
+      };
+    }
+
+    let currentResult = result;
+    if (this.refreshClient().isNew) {
+      currentResult = this.getLoadingResult();
+    }
+
+    let { shouldResubscribe } = this.getOptions();
+    if (typeof shouldResubscribe === 'function') {
+      shouldResubscribe = !!shouldResubscribe(this.getOptions());
+    }
+
+    if (
+      shouldResubscribe !== false &&
+      this.previousOptions &&
+      Object.keys(this.previousOptions).length > 0 &&
+      (this.previousOptions.subscription !== this.getOptions().subscription ||
+        !equal(this.previousOptions.variables, this.getOptions().variables) ||
+        this.previousOptions.skip !== this.getOptions().skip)
+    ) {
+      this.cleanup();
+      currentResult = this.getLoadingResult();
+    }
+
+    this.initialize(this.getOptions());
+    this.startSubscription();
+
+    this.previousOptions = this.getOptions();
+    return { ...currentResult, variables: this.getOptions().variables };
+  }
+
+  public afterExecute() {
+    this.isMounted = true;
+  }
+
+  public cleanup() {
+    this.endSubscription();
+    delete this.currentObservable.query;
+  }
+
+  private initialize(options: SubscriptionDataOptions<TData, TVariables>) {
+    if (this.currentObservable.query || this.getOptions().skip === true) return;
+    this.currentObservable.query = this.refreshClient().client.subscribe({
+      query: options.subscription,
+      variables: options.variables,
+      fetchPolicy: options.fetchPolicy
+    });
+  }
+
+  private startSubscription() {
+    if (this.currentObservable.subscription) return;
+    this.currentObservable.subscription = this.currentObservable.query!.subscribe(
+      {
+        next: this.updateCurrentData.bind(this),
+        error: this.updateError.bind(this),
+        complete: this.completeSubscription.bind(this)
+      }
+    );
+  }
+
+  private getLoadingResult() {
+    return {
+      loading: true,
+      error: undefined,
+      data: undefined
+    } as SubscriptionResult<TData>;
+  }
+
+  private updateResult(result: SubscriptionResult) {
+    if (this.isMounted) {
+      this.setResult(result);
+    }
+  }
+
+  private updateCurrentData(result: SubscriptionResult<TData>) {
+    const { onSubscriptionData } = this.getOptions();
+
+    this.updateResult({
+      data: result.data,
+      loading: false,
+      error: undefined
+    });
+
+    if (onSubscriptionData) {
+      onSubscriptionData({
+        client: this.refreshClient().client,
+        subscriptionData: result
+      });
+    }
+  }
+
+  private updateError(error: any) {
+    this.updateResult({
+      error,
+      loading: false
+    });
+  }
+
+  private completeSubscription() {
+    const { onSubscriptionComplete } = this.getOptions();
+    if (onSubscriptionComplete) onSubscriptionComplete();
+    this.endSubscription();
+  }
+
+  private endSubscription() {
+    if (this.currentObservable.subscription) {
+      this.currentObservable.subscription.unsubscribe();
+      delete this.currentObservable.subscription;
+    }
+  }
+}
diff --git a/src/react/hooks/useApolloClient.ts b/src/react/hooks/useApolloClient.ts
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/useApolloClient.ts
@@ -0,0 +1,16 @@
+import { invariant } from 'ts-invariant';
+
+import { ApolloClient } from '../../ApolloClient';
+import { getApolloContext } from '../context/ApolloContext';
+import { requireReactLazily } from '../react';
+
+export function useApolloClient(): ApolloClient<object> {
+  const React = requireReactLazily();
+  const { client } = React.useContext(getApolloContext());
+  invariant(
+    client,
+    'No Apollo Client instance can be found. Please ensure that you ' +
+      'have called `ApolloProvider` higher up in your tree.'
+  );
+  return client!;
+}
diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/useLazyQuery.ts
@@ -0,0 +1,15 @@
+import { DocumentNode } from 'graphql';
+
+import { LazyQueryHookOptions, QueryTuple } from '../types/types';
+import { useBaseQuery } from './utils/useBaseQuery';
+import { OperationVariables } from '../../core/types';
+
+export function useLazyQuery<TData = any, TVariables = OperationVariables>(
+  query: DocumentNode,
+  options?: LazyQueryHookOptions<TData, TVariables>
+) {
+  return useBaseQuery<TData, TVariables>(query, options, true) as QueryTuple<
+    TData,
+    TVariables
+  >;
+}
diff --git a/src/react/hooks/useMutation.ts b/src/react/hooks/useMutation.ts
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/useMutation.ts
@@ -0,0 +1,38 @@
+import { DocumentNode } from 'graphql';
+
+import { MutationHookOptions, MutationTuple } from '../types/types';
+import { MutationData } from '../data/MutationData';
+import { OperationVariables } from '../../core/types';
+import { getApolloContext } from '../context/ApolloContext';
+import { requireReactLazily } from '../react';
+
+export function useMutation<TData = any, TVariables = OperationVariables>(
+  mutation: DocumentNode,
+  options?: MutationHookOptions<TData, TVariables>
+): MutationTuple<TData, TVariables> {
+  const { useContext, useState, useRef, useEffect } = requireReactLazily();
+  const context = useContext(getApolloContext());
+  const [result, setResult] = useState({ called: false, loading: false });
+  const updatedOptions = options ? { ...options, mutation } : { mutation };
+
+  const mutationDataRef = useRef<MutationData<TData, TVariables>>();
+  function getMutationDataRef() {
+    if (!mutationDataRef.current) {
+      mutationDataRef.current = new MutationData<TData, TVariables>({
+        options: updatedOptions,
+        context,
+        result,
+        setResult
+      });
+    }
+    return mutationDataRef.current;
+  }
+
+  const mutationData = getMutationDataRef();
+  mutationData.setOptions(updatedOptions);
+  mutationData.context = context;
+
+  useEffect(() => mutationData.afterExecute());
+
+  return mutationData.execute(result);
+}
diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/useQuery.ts
@@ -0,0 +1,15 @@
+import { DocumentNode } from 'graphql';
+
+import { QueryHookOptions, QueryResult } from '../types/types';
+import { useBaseQuery } from './utils/useBaseQuery';
+import { OperationVariables } from '../../core/types';
+
+export function useQuery<TData = any, TVariables = OperationVariables>(
+  query: DocumentNode,
+  options?: QueryHookOptions<TData, TVariables>
+) {
+  return useBaseQuery<TData, TVariables>(query, options, false) as QueryResult<
+    TData,
+    TVariables
+  >;
+}
diff --git a/src/react/hooks/useSubscription.ts b/src/react/hooks/useSubscription.ts
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/useSubscription.ts
@@ -0,0 +1,45 @@
+import { DocumentNode } from 'graphql';
+
+import { SubscriptionHookOptions } from '../types/types';
+import { SubscriptionData } from '../data/SubscriptionData';
+import { OperationVariables } from '../../core/types';
+import { getApolloContext } from '../context/ApolloContext';
+import { requireReactLazily } from '../react';
+
+export function useSubscription<TData = any, TVariables = OperationVariables>(
+  subscription: DocumentNode,
+  options?: SubscriptionHookOptions<TData, TVariables>
+) {
+  const React = requireReactLazily();
+  const { useContext, useState, useRef, useEffect } = React;
+  const context = useContext(getApolloContext());
+  const updatedOptions = options
+    ? { ...options, subscription }
+    : { subscription };
+  const [result, setResult] = useState({
+    loading: !updatedOptions.skip,
+    error: undefined,
+    data: undefined
+  });
+
+  const subscriptionDataRef = useRef<SubscriptionData<TData, TVariables>>();
+  function getSubscriptionDataRef() {
+    if (!subscriptionDataRef.current) {
+      subscriptionDataRef.current = new SubscriptionData<TData, TVariables>({
+        options: updatedOptions,
+        context,
+        setResult
+      });
+    }
+    return subscriptionDataRef.current;
+  }
+
+  const subscriptionData = getSubscriptionDataRef();
+  subscriptionData.setOptions(updatedOptions, true);
+  subscriptionData.context = context;
+
+  useEffect(() => subscriptionData.afterExecute());
+  useEffect(() => subscriptionData.cleanup.bind(subscriptionData), []);
+
+  return subscriptionData.execute(result);
+}
diff --git a/src/react/hooks/utils/useBaseQuery.ts b/src/react/hooks/utils/useBaseQuery.ts
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/utils/useBaseQuery.ts
@@ -0,0 +1,74 @@
+import { DocumentNode } from 'graphql';
+
+import {
+  QueryHookOptions,
+  QueryDataOptions,
+  QueryTuple,
+  QueryResult,
+} from '../../types/types';
+import { QueryData } from '../../data/QueryData';
+import { useDeepMemo } from './useDeepMemo';
+import { OperationVariables } from '../../../core/types';
+import { getApolloContext } from '../../context/ApolloContext';
+import { requireReactLazily } from '../../react';
+
+export function useBaseQuery<TData = any, TVariables = OperationVariables>(
+  query: DocumentNode,
+  options?: QueryHookOptions<TData, TVariables>,
+  lazy = false
+) {
+  const React = requireReactLazily();
+  const { useContext, useEffect, useReducer, useRef } = React;
+  const context = useContext(getApolloContext());
+  const [tick, forceUpdate] = useReducer(x => x + 1, 0);
+  const updatedOptions = options ? { ...options, query } : { query };
+
+  const queryDataRef = useRef<QueryData<TData, TVariables>>();
+
+  if (!queryDataRef.current) {
+    queryDataRef.current = new QueryData<TData, TVariables>({
+      options: updatedOptions as QueryDataOptions<TData, TVariables>,
+      context,
+      forceUpdate
+    });
+  }
+
+  const queryData = queryDataRef.current;
+  queryData.setOptions(updatedOptions);
+  queryData.context = context;
+
+  // `onError` and `onCompleted` callback functions will not always have a
+  // stable identity, so we'll exclude them from the memoization key to
+  // prevent `afterExecute` from being triggered un-necessarily.
+  const memo = {
+    options: {
+      ...updatedOptions,
+      onError: undefined,
+      onCompleted: undefined
+    } as QueryHookOptions<TData, TVariables>,
+    context,
+    tick
+  };
+
+  const result = useDeepMemo(
+    () => (lazy ? queryData.executeLazy() : queryData.execute()),
+    memo
+  );
+
+  const queryResult = lazy
+    ? (result as QueryTuple<TData, TVariables>)[1]
+    : (result as QueryResult<TData, TVariables>);
+
+  useEffect(() => queryData.afterExecute({ queryResult, lazy }), [
+    queryResult.loading,
+    queryResult.networkStatus,
+    queryResult.error,
+    queryResult.data
+  ]);
+
+  useEffect(() => {
+    return () => queryData.cleanup();
+  }, []);
+
+  return result;
+}
diff --git a/src/react/hooks/utils/useDeepMemo.ts b/src/react/hooks/utils/useDeepMemo.ts
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/utils/useDeepMemo.ts
@@ -0,0 +1,25 @@
+import { equal } from '@wry/equality';
+
+import { requireReactLazily } from '../../react';
+
+/**
+ * Memoize a result using deep equality. This hook has two advantages over
+ * React.useMemo: it uses deep equality to compare memo keys, and it guarantees
+ * that the memo function will only be called if the keys are unequal.
+ * React.useMemo cannot be relied on to do this, since it is only a performance
+ * optimization (see https://reactjs.org/docs/hooks-reference.html#usememo).
+ */
+export function useDeepMemo<TKey, TValue>(
+  memoFn: () => TValue,
+  key: TKey
+): TValue {
+  const React = requireReactLazily();
+  const { useRef } = React;
+  const ref = useRef<{ key: TKey; value: TValue }>();
+
+  if (!ref.current || !equal(key, ref.current.key)) {
+    ref.current = { key, value: memoFn() };
+  }
+
+  return ref.current.value;
+}
diff --git a/src/react/index.ts b/src/react/index.ts
new file mode 100644
--- /dev/null
+++ b/src/react/index.ts
@@ -0,0 +1,21 @@
+export { ApolloProvider } from './context/ApolloProvider';
+export { ApolloConsumer } from './context/ApolloConsumer';
+export {
+  getApolloContext,
+  resetApolloContext,
+  ApolloContextValue
+} from './context/ApolloContext';
+export { useQuery } from './hooks/useQuery';
+export { useLazyQuery } from './hooks/useLazyQuery';
+export { useMutation } from './hooks/useMutation';
+export { useSubscription } from './hooks/useSubscription';
+export { useApolloClient } from './hooks/useApolloClient';
+export { RenderPromises } from './ssr/RenderPromises';
+export {
+  DocumentType,
+  IDocumentDefinition,
+  operationName,
+  parser
+} from './parser/parser';
+
+export * from './types/types';
diff --git a/src/react/parser/parser.ts b/src/react/parser/parser.ts
new file mode 100644
--- /dev/null
+++ b/src/react/parser/parser.ts
@@ -0,0 +1,115 @@
+import {
+  DocumentNode,
+  DefinitionNode,
+  VariableDefinitionNode,
+  OperationDefinitionNode
+} from 'graphql';
+import { invariant } from 'ts-invariant';
+
+export enum DocumentType {
+  Query,
+  Mutation,
+  Subscription
+}
+
+export interface IDocumentDefinition {
+  type: DocumentType;
+  name: string;
+  variables: ReadonlyArray<VariableDefinitionNode>;
+}
+
+const cache = new Map();
+
+export function operationName(type: DocumentType) {
+  let name;
+  switch (type) {
+    case DocumentType.Query:
+      name = 'Query';
+      break;
+    case DocumentType.Mutation:
+      name = 'Mutation';
+      break;
+    case DocumentType.Subscription:
+      name = 'Subscription';
+      break;
+  }
+  return name;
+}
+
+// This parser is mostly used to saftey check incoming documents.
+export function parser(document: DocumentNode): IDocumentDefinition {
+  const cached = cache.get(document);
+  if (cached) return cached;
+
+  let variables, type, name;
+
+  invariant(
+    !!document && !!document.kind,
+    `Argument of ${document} passed to parser was not a valid GraphQL ` +
+      `DocumentNode. You may need to use 'graphql-tag' or another method ` +
+      `to convert your operation into a document`
+  );
+
+  const fragments = document.definitions.filter(
+    (x: DefinitionNode) => x.kind === 'FragmentDefinition'
+  );
+
+  const queries = document.definitions.filter(
+    (x: DefinitionNode) =>
+      x.kind === 'OperationDefinition' && x.operation === 'query'
+  );
+
+  const mutations = document.definitions.filter(
+    (x: DefinitionNode) =>
+      x.kind === 'OperationDefinition' && x.operation === 'mutation'
+  );
+
+  const subscriptions = document.definitions.filter(
+    (x: DefinitionNode) =>
+      x.kind === 'OperationDefinition' && x.operation === 'subscription'
+  );
+
+  invariant(
+    !fragments.length ||
+      (queries.length || mutations.length || subscriptions.length),
+    `Passing only a fragment to 'graphql' is not yet supported. ` +
+      `You must include a query, subscription or mutation as well`
+  );
+
+  invariant(
+    queries.length + mutations.length + subscriptions.length <= 1,
+    `react-apollo only supports a query, subscription, or a mutation per HOC. ` +
+      `${document} had ${queries.length} queries, ${subscriptions.length} ` +
+      `subscriptions and ${mutations.length} mutations. ` +
+      `You can use 'compose' to join multiple operation types to a component`
+  );
+
+  type = queries.length ? DocumentType.Query : DocumentType.Mutation;
+  if (!queries.length && !mutations.length) type = DocumentType.Subscription;
+
+  const definitions = queries.length
+    ? queries
+    : mutations.length
+    ? mutations
+    : subscriptions;
+
+  invariant(
+    definitions.length === 1,
+    `react-apollo only supports one definition per HOC. ${document} had ` +
+      `${definitions.length} definitions. ` +
+      `You can use 'compose' to join multiple operation types to a component`
+  );
+
+  const definition = definitions[0] as OperationDefinitionNode;
+  variables = definition.variableDefinitions || [];
+
+  if (definition.name && definition.name.kind === 'Name') {
+    name = definition.name.value;
+  } else {
+    name = 'data'; // fallback to using data if no name
+  }
+
+  const payload = { name, type, variables };
+  cache.set(document, payload);
+  return payload;
+}
diff --git a/src/react/react.ts b/src/react/react.ts
new file mode 100644
--- /dev/null
+++ b/src/react/react.ts
@@ -0,0 +1,9 @@
+let React: typeof import('react');
+
+// Apollo Client can be used without React, which means we want to make sure
+// `react` is only imported/required if actually needed. To help with this
+// the `react` module is lazy loaded using `requireReactLazily` when used by
+// Apollo Client's React integration layer.
+export function requireReactLazily(): typeof import('react') {
+  return React || (React = require('react'));
+}
diff --git a/src/react/ssr/RenderPromises.ts b/src/react/ssr/RenderPromises.ts
new file mode 100644
--- /dev/null
+++ b/src/react/ssr/RenderPromises.ts
@@ -0,0 +1,98 @@
+import { DocumentNode } from 'graphql';
+
+import { ObservableQuery } from '../../core/ObservableQuery';
+import { QueryDataOptions } from '../types/types';
+import { QueryData } from '../data/QueryData';
+
+type QueryInfo = {
+  seen: boolean;
+  observable: ObservableQuery<any, any> | null;
+};
+
+function makeDefaultQueryInfo(): QueryInfo {
+  return {
+    seen: false,
+    observable: null
+  };
+}
+
+export class RenderPromises {
+  // Map from Query component instances to pending fetchData promises.
+  private queryPromises = new Map<QueryDataOptions<any, any>, Promise<any>>();
+
+  // Two-layered map from (query document, stringified variables) to QueryInfo
+  // objects. These QueryInfo objects are intended to survive through the whole
+  // getMarkupFromTree process, whereas specific Query instances do not survive
+  // beyond a single call to renderToStaticMarkup.
+  private queryInfoTrie = new Map<DocumentNode, Map<string, QueryInfo>>();
+
+  // Registers the server side rendered observable.
+  public registerSSRObservable<TData, TVariables>(
+    observable: ObservableQuery<any, TVariables>,
+    props: QueryDataOptions<TData, TVariables>
+  ) {
+    this.lookupQueryInfo(props).observable = observable;
+  }
+
+  // Get's the cached observable that matches the SSR Query instances query and variables.
+  public getSSRObservable<TData, TVariables>(
+    props: QueryDataOptions<TData, TVariables>
+  ) {
+    return this.lookupQueryInfo(props).observable;
+  }
+
+  public addQueryPromise<TData, TVariables>(
+    queryInstance: QueryData<TData, TVariables>,
+    finish: () => React.ReactNode
+  ): React.ReactNode {
+    const info = this.lookupQueryInfo(queryInstance.getOptions());
+    if (!info.seen) {
+      this.queryPromises.set(
+        queryInstance.getOptions(),
+        new Promise(resolve => {
+          resolve(queryInstance.fetchData());
+        })
+      );
+      // Render null to abandon this subtree for this rendering, so that we
+      // can wait for the data to arrive.
+      return null;
+    }
+    return finish();
+  }
+
+  public hasPromises() {
+    return this.queryPromises.size > 0;
+  }
+
+  public consumeAndAwaitPromises() {
+    const promises: Promise<any>[] = [];
+    this.queryPromises.forEach((promise, queryInstance) => {
+      // Make sure we never try to call fetchData for this query document and
+      // these variables again. Since the queryInstance objects change with
+      // every rendering, deduplicating them by query and variables is the
+      // best we can do. If a different Query component happens to have the
+      // same query document and variables, it will be immediately rendered
+      // by calling finish() in addQueryPromise, which could result in the
+      // rendering of an unwanted loading state, but that's not nearly as bad
+      // as getting stuck in an infinite rendering loop because we kept calling
+      // queryInstance.fetchData for the same Query component indefinitely.
+      this.lookupQueryInfo(queryInstance).seen = true;
+      promises.push(promise);
+    });
+    this.queryPromises.clear();
+    return Promise.all(promises);
+  }
+
+  private lookupQueryInfo<TData, TVariables>(
+    props: QueryDataOptions<TData, TVariables>
+  ): QueryInfo {
+    const { queryInfoTrie } = this;
+    const { query, variables } = props;
+    const varMap = queryInfoTrie.get(query) || new Map<string, QueryInfo>();
+    if (!queryInfoTrie.has(query)) queryInfoTrie.set(query, varMap);
+    const variablesString = JSON.stringify(variables);
+    const info = varMap.get(variablesString) || makeDefaultQueryInfo();
+    if (!varMap.has(variablesString)) varMap.set(variablesString, info);
+    return info;
+  }
+}
diff --git a/src/react/types/types.ts b/src/react/types/types.ts
new file mode 100644
--- /dev/null
+++ b/src/react/types/types.ts
@@ -0,0 +1,249 @@
+import { ReactNode } from 'react';
+import { DocumentNode } from 'graphql';
+
+import { Observable } from '../../utilities/observables/Observable';
+import { FetchResult } from '../../link/core/types';
+import { ApolloClient } from '../../ApolloClient';
+import {
+  ApolloQueryResult,
+  PureQueryOptions,
+  OperationVariables
+} from '../../core/types';
+import { ApolloError } from '../../errors/ApolloError';
+import {
+  FetchPolicy,
+  WatchQueryFetchPolicy,
+  ErrorPolicy,
+  FetchMoreQueryOptions,
+  MutationUpdaterFn,
+} from '../../core/watchQueryOptions';
+import { FetchMoreOptions, ObservableQuery } from '../../core/ObservableQuery';
+import { NetworkStatus } from '../../core/networkStatus';
+
+/* Common types */
+
+export type Context = Record<string, any>;
+
+export type CommonOptions<TOptions> = TOptions & {
+  client?: ApolloClient<object>;
+};
+
+/* Query types */
+
+export interface BaseQueryOptions<TVariables = OperationVariables> {
+  ssr?: boolean;
+  variables?: TVariables;
+  fetchPolicy?: WatchQueryFetchPolicy;
+  errorPolicy?: ErrorPolicy;
+  pollInterval?: number;
+  client?: ApolloClient<any>;
+  notifyOnNetworkStatusChange?: boolean;
+  context?: Context;
+  partialRefetch?: boolean;
+  returnPartialData?: boolean;
+}
+
+export interface QueryFunctionOptions<
+  TData = any,
+  TVariables = OperationVariables
+> extends BaseQueryOptions<TVariables> {
+  displayName?: string;
+  skip?: boolean;
+  onCompleted?: (data: TData) => void;
+  onError?: (error: ApolloError) => void;
+}
+
+export type ObservableQueryFields<TData, TVariables> = Pick<
+  ObservableQuery<TData, TVariables>,
+  | 'startPolling'
+  | 'stopPolling'
+  | 'subscribeToMore'
+  | 'updateQuery'
+  | 'refetch'
+  | 'variables'
+> & {
+  fetchMore: (<K extends keyof TVariables>(
+    fetchMoreOptions: FetchMoreQueryOptions<TVariables, K> &
+      FetchMoreOptions<TData, TVariables>
+  ) => Promise<ApolloQueryResult<TData>>) &
+    (<TData2, TVariables2, K extends keyof TVariables2>(
+      fetchMoreOptions: { query?: DocumentNode } & FetchMoreQueryOptions<
+        TVariables2,
+        K
+      > &
+        FetchMoreOptions<TData2, TVariables2>
+    ) => Promise<ApolloQueryResult<TData2>>);
+};
+
+export interface QueryResult<TData = any, TVariables = OperationVariables>
+  extends ObservableQueryFields<TData, TVariables> {
+  client: ApolloClient<any>;
+  data: TData | undefined;
+  error?: ApolloError;
+  loading: boolean;
+  networkStatus: NetworkStatus;
+  called: boolean;
+}
+
+export interface QueryDataOptions<TData = any, TVariables = OperationVariables>
+  extends QueryFunctionOptions<TData, TVariables> {
+  children?: (result: QueryResult<TData, TVariables>) => ReactNode;
+  query: DocumentNode;
+}
+
+export interface QueryHookOptions<TData = any, TVariables = OperationVariables>
+  extends QueryFunctionOptions<TData, TVariables> {
+  query?: DocumentNode;
+}
+
+export interface LazyQueryHookOptions<
+  TData = any,
+  TVariables = OperationVariables
+> extends Omit<QueryFunctionOptions<TData, TVariables>, 'skip'> {
+  query?: DocumentNode;
+}
+
+export interface QueryPreviousData<TData, TVariables> {
+  client?: ApolloClient<object>;
+  query?: DocumentNode;
+  observableQueryOptions?: {};
+  result?: ApolloQueryResult<TData> | null;
+  loading?: boolean;
+  options?: QueryDataOptions<TData, TVariables>;
+  error?: ApolloError;
+}
+
+export interface QueryCurrentObservable<TData, TVariables> {
+  query?: ObservableQuery<TData, TVariables> | null;
+  subscription?: ZenObservable.Subscription;
+}
+
+export interface QueryLazyOptions<TVariables> {
+  variables?: TVariables;
+  context?: Context;
+}
+
+export type QueryTuple<TData, TVariables> = [
+  (options?: QueryLazyOptions<TVariables>) => void,
+  QueryResult<TData, TVariables>
+];
+
+/* Mutation types */
+
+export type RefetchQueriesFunction = (
+  ...args: any[]
+) => Array<string | PureQueryOptions>;
+
+export interface BaseMutationOptions<
+  TData = any,
+  TVariables = OperationVariables
+> {
+  variables?: TVariables;
+  optimisticResponse?: TData | ((vars: TVariables) => TData);
+  refetchQueries?: Array<string | PureQueryOptions> | RefetchQueriesFunction;
+  awaitRefetchQueries?: boolean;
+  errorPolicy?: ErrorPolicy;
+  update?: MutationUpdaterFn<TData>;
+  client?: ApolloClient<object>;
+  notifyOnNetworkStatusChange?: boolean;
+  context?: Context;
+  onCompleted?: (data: TData) => void;
+  onError?: (error: ApolloError) => void;
+  fetchPolicy?: WatchQueryFetchPolicy;
+  ignoreResults?: boolean;
+}
+
+export interface MutationFunctionOptions<
+  TData = any,
+  TVariables = OperationVariables
+> {
+  variables?: TVariables;
+  optimisticResponse?: TData | ((vars: TVariables | {}) => TData);
+  refetchQueries?: Array<string | PureQueryOptions> | RefetchQueriesFunction;
+  awaitRefetchQueries?: boolean;
+  update?: MutationUpdaterFn<TData>;
+  context?: Context;
+  fetchPolicy?: WatchQueryFetchPolicy;
+}
+
+export interface MutationResult<TData = any> {
+  data?: TData;
+  error?: ApolloError;
+  loading: boolean;
+  called: boolean;
+  client?: ApolloClient<object>;
+}
+
+export declare type MutationFunction<
+  TData = any,
+  TVariables = OperationVariables
+> = (
+  options?: MutationFunctionOptions<TData, TVariables>
+) => Promise<FetchResult<TData>>;
+
+export interface MutationHookOptions<
+  TData = any,
+  TVariables = OperationVariables
+> extends BaseMutationOptions<TData, TVariables> {
+  mutation?: DocumentNode;
+}
+
+export interface MutationDataOptions<TData = any, TVariables = OperationVariables>
+  extends BaseMutationOptions<TData, TVariables> {
+  mutation: DocumentNode;
+}
+
+export type MutationTuple<TData, TVariables> = [
+  (
+    options?: MutationFunctionOptions<TData, TVariables>
+  ) => Promise<FetchResult<TData>>,
+  MutationResult<TData>
+];
+
+/* Subscription types */
+
+export interface OnSubscriptionDataOptions<TData = any> {
+  client: ApolloClient<object>;
+  subscriptionData: SubscriptionResult<TData>;
+}
+
+export interface BaseSubscriptionOptions<
+  TData = any,
+  TVariables = OperationVariables
+> {
+  variables?: TVariables;
+  fetchPolicy?: FetchPolicy;
+  shouldResubscribe?:
+    | boolean
+    | ((options: BaseSubscriptionOptions<TData, TVariables>) => boolean);
+  client?: ApolloClient<object>;
+  skip?: boolean;
+  onSubscriptionData?: (options: OnSubscriptionDataOptions<TData>) => any;
+  onSubscriptionComplete?: () => void;
+}
+
+export interface SubscriptionResult<TData = any> {
+  loading: boolean;
+  data?: TData;
+  error?: ApolloError;
+}
+
+export interface SubscriptionHookOptions<
+  TData = any,
+  TVariables = OperationVariables
+> extends BaseSubscriptionOptions<TData, TVariables> {
+  subscription?: DocumentNode;
+}
+
+export interface SubscriptionDataOptions<
+  TData = any,
+  TVariables = OperationVariables
+> extends BaseSubscriptionOptions<TData, TVariables> {
+  subscription: DocumentNode;
+  children?: null | ((result: SubscriptionResult<TData>) => JSX.Element | null);
+}
+
+export interface SubscriptionCurrentObservable {
+  query?: Observable<any>;
+  subscription?: ZenObservable.Subscription;
+}
diff --git a/src/utilities/codemods/mockLinkRejection.ts b/src/utilities/codemods/mockLinkRejection.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/codemods/mockLinkRejection.ts
@@ -0,0 +1,40 @@
+import * as recast from "recast";
+const n = recast.types.namedTypes;
+const b = recast.types.builders;
+
+export default function (fileInfo: any, api: any) {
+  const ast = recast.parse(fileInfo.source, {
+    parser: require("recast/parsers/typescript"),
+  });
+
+  // Transform mockSingleLink(reject, ...) to
+  // mockSingleLink(...).setOnError(reject):
+
+  const transformed = recast.visit(ast, {
+    visitCallExpression(path) {
+      this.traverse(path);
+      const node = path.node;
+      if (n.Identifier.check(node.callee) &&
+          node.callee.name === "mockSingleLink") {
+        const firstArg = node.arguments[0];
+        if ((n.Identifier.check(firstArg) &&
+             firstArg.name === "reject") ||
+            n.Function.check(firstArg)) {
+          path.get("arguments").shift();
+          path.replace(
+            b.callExpression(
+              b.memberExpression(
+                node,
+                b.identifier("setOnError"),
+                false,
+              ),
+              [firstArg],
+            ),
+          );
+        }
+      }
+    },
+  });
+
+  return recast.print(transformed).code;
+}
diff --git a/packages/apollo-client/src/util/arrays.ts b/src/utilities/common/arrays.ts
similarity index 100%
rename from packages/apollo-client/src/util/arrays.ts
rename to src/utilities/common/arrays.ts
diff --git a/packages/apollo-utilities/src/util/assign.ts b/src/utilities/common/assign.ts
similarity index 100%
rename from packages/apollo-utilities/src/util/assign.ts
rename to src/utilities/common/assign.ts
diff --git a/packages/apollo-utilities/src/util/canUse.ts b/src/utilities/common/canUse.ts
similarity index 100%
rename from packages/apollo-utilities/src/util/canUse.ts
rename to src/utilities/common/canUse.ts
diff --git a/packages/apollo-utilities/src/util/cloneDeep.ts b/src/utilities/common/cloneDeep.ts
similarity index 85%
rename from packages/apollo-utilities/src/util/cloneDeep.ts
rename to src/utilities/common/cloneDeep.ts
--- a/packages/apollo-utilities/src/util/cloneDeep.ts
+++ b/src/utilities/common/cloneDeep.ts
@@ -4,12 +4,13 @@ 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());
+  return cloneDeepHelper(value);
 }
 
-function cloneDeepHelper<T>(val: T, seen: Map<any, any>): T {
+function cloneDeepHelper<T>(val: T, seen?: Map<any, any>): T {
   switch (toString.call(val)) {
   case "[object Array]": {
+    seen = seen || new Map;
     if (seen.has(val)) return seen.get(val);
     const copy: T & any[] = (val as any).slice(0);
     seen.set(val, copy);
@@ -20,6 +21,7 @@ function cloneDeepHelper<T>(val: T, seen: Map<any, any>): T {
   }
 
   case "[object Object]": {
+    seen = seen || new Map;
     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.
diff --git a/packages/apollo-utilities/src/util/environment.ts b/src/utilities/common/environment.ts
similarity index 83%
rename from packages/apollo-utilities/src/util/environment.ts
rename to src/utilities/common/environment.ts
--- a/packages/apollo-utilities/src/util/environment.ts
+++ b/src/utilities/common/environment.ts
@@ -11,10 +11,6 @@ export function isEnv(env: string): boolean {
   return getEnv() === env;
 }
 
-export function isProduction(): boolean {
-  return isEnv('production') === true;
-}
-
 export function isDevelopment(): boolean {
   return isEnv('development') === true;
 }
diff --git a/packages/apollo-utilities/src/util/errorHandling.ts b/src/utilities/common/errorHandling.ts
similarity index 100%
rename from packages/apollo-utilities/src/util/errorHandling.ts
rename to src/utilities/common/errorHandling.ts
diff --git a/packages/apollo-utilities/src/util/filterInPlace.ts b/src/utilities/common/filterInPlace.ts
similarity index 100%
rename from packages/apollo-utilities/src/util/filterInPlace.ts
rename to src/utilities/common/filterInPlace.ts
diff --git a/src/utilities/common/maybeDeepFreeze.ts b/src/utilities/common/maybeDeepFreeze.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/common/maybeDeepFreeze.ts
@@ -0,0 +1,25 @@
+import { isDevelopment, isTest } from './environment';
+
+function isObject(value: any) {
+  return value !== null && typeof value === "object";
+}
+
+function deepFreeze(value: any) {
+  const workSet = new Set([value]);
+  workSet.forEach(obj => {
+    if (isObject(obj)) {
+      if (!Object.isFrozen(obj)) Object.freeze(obj);
+      Object.getOwnPropertyNames(obj).forEach(name => {
+        if (isObject(obj[name])) workSet.add(obj[name]);
+      });
+    }
+  });
+  return value;
+}
+
+export function maybeDeepFreeze(obj: any) {
+  if (isDevelopment() || isTest()) {
+    deepFreeze(obj);
+  }
+  return obj;
+}
diff --git a/src/utilities/common/mergeDeep.ts b/src/utilities/common/mergeDeep.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/common/mergeDeep.ts
@@ -0,0 +1,122 @@
+const { hasOwnProperty } = Object.prototype;
+
+// These mergeDeep and mergeDeepArray utilities merge any number of objects
+// together, sharing as much memory as possible with the source objects, while
+// remaining careful to avoid modifying any source objects.
+
+// Logically, the return type of mergeDeep should be the intersection of
+// all the argument types. The binary call signature is by far the most
+// common, but we support 0- through 5-ary as well. After that, the
+// resulting type is just the inferred array element type. Note to nerds:
+// there is a more clever way of doing this that converts the tuple type
+// first to a union type (easy enough: T[number]) and then converts the
+// union to an intersection type using distributive conditional type
+// inference, but that approach has several fatal flaws (boolean becomes
+// true & false, and the inferred type ends up as unknown in many cases),
+// in addition to being nearly impossible to explain/understand.
+export type TupleToIntersection<T extends any[]> =
+  T extends [infer A] ? A :
+  T extends [infer A, infer B] ? A & B :
+  T extends [infer A, infer B, infer C] ? A & B & C :
+  T extends [infer A, infer B, infer C, infer D] ? A & B & C & D :
+  T extends [infer A, infer B, infer C, infer D, infer E] ? A & B & C & D & E :
+  T extends (infer U)[] ? U : any;
+
+export function mergeDeep<T extends any[]>(
+  ...sources: T
+): TupleToIntersection<T> {
+  return mergeDeepArray(sources);
+}
+
+// In almost any situation where you could succeed in getting the
+// TypeScript compiler to infer a tuple type for the sources array, you
+// could just use mergeDeep instead of mergeDeepArray, so instead of
+// trying to convert T[] to an intersection type we just infer the array
+// element type, which works perfectly when the sources array has a
+// consistent element type.
+export function mergeDeepArray<T>(sources: T[]): T {
+  let target = sources[0] || ({} as T);
+  const count = sources.length;
+  if (count > 1) {
+    const merger = new DeepMerger();
+    for (let i = 1; i < count; ++i) {
+      target = merger.merge(target, sources[i]);
+    }
+  }
+  return target;
+}
+
+function isObject(obj: any): obj is Record<string | number, any> {
+  return obj !== null && typeof obj === 'object';
+}
+
+export type ReconcilerFunction<TContextArgs extends any[]> = (
+  this: DeepMerger<TContextArgs>,
+  target: Record<string | number, any>,
+  source: Record<string | number, any>,
+  property: string | number,
+  ...context: TContextArgs
+) => any;
+
+const defaultReconciler: ReconcilerFunction<any[]> =
+  function (target, source, property) {
+    return this.merge(target[property], source[property]);
+  };
+
+export class DeepMerger<TContextArgs extends any[]> {
+  private pastCopies: any[] = [];
+
+  constructor(
+    private reconciler: ReconcilerFunction<TContextArgs> = defaultReconciler,
+  ) {}
+
+  public merge(target: any, source: any, ...context: TContextArgs): any {
+    if (isObject(source) && isObject(target)) {
+      Object.keys(source).forEach(sourceKey => {
+        if (hasOwnProperty.call(target, sourceKey)) {
+          const targetValue = target[sourceKey];
+          if (source[sourceKey] !== targetValue) {
+            const result = this.reconciler(target, source, sourceKey, ...context);
+            // A well-implemented reconciler may return targetValue to indicate
+            // the merge changed nothing about the structure of the target.
+            if (result !== targetValue) {
+              target = this.shallowCopyForMerge(target);
+              target[sourceKey] = result;
+            }
+          }
+        } else {
+          // If there is no collision, the target can safely share memory with
+          // the source, and the recursion can terminate here.
+          target = this.shallowCopyForMerge(target);
+          target[sourceKey] = source[sourceKey];
+        }
+      });
+
+      return target;
+    }
+
+    // If source (or target) is not an object, let source replace target.
+    return source;
+  }
+
+  public isObject = isObject;
+
+  public shallowCopyForMerge<T>(value: T): T {
+    if (
+      value !== null &&
+      typeof value === 'object' &&
+      this.pastCopies.indexOf(value) < 0
+    ) {
+      if (Array.isArray(value)) {
+        value = (value as any).slice(0);
+      } else {
+        value = {
+          __proto__: Object.getPrototypeOf(value),
+          ...value,
+        };
+      }
+      this.pastCopies.push(value);
+    }
+    return value;
+  }
+}
diff --git a/packages/apollo-utilities/src/directives.ts b/src/utilities/graphql/directives.ts
similarity index 85%
rename from packages/apollo-utilities/src/directives.ts
rename to src/utilities/graphql/directives.ts
--- a/packages/apollo-utilities/src/directives.ts
+++ b/src/utilities/graphql/directives.ts
@@ -1,7 +1,6 @@
 // Provides the methods that allow QueryManager to handle the `skip` and
 // `include` directives within GraphQL.
 import {
-  FieldNode,
   SelectionNode,
   VariableNode,
   BooleanValueNode,
@@ -15,29 +14,10 @@ import { visit } from 'graphql/language/visitor';
 
 import { invariant } from 'ts-invariant';
 
-import { argumentsObjectFromField } from './storeUtils';
-
 export type DirectiveInfo = {
   [fieldName: string]: { [argName: string]: any };
 };
 
-export function getDirectiveInfoFromField(
-  field: FieldNode,
-  variables: Object,
-): DirectiveInfo {
-  if (field.directives && field.directives.length) {
-    const directiveObj: DirectiveInfo = {};
-    field.directives.forEach((directive: DirectiveNode) => {
-      directiveObj[directive.name.value] = argumentsObjectFromField(
-        directive,
-        variables,
-      );
-    });
-    return directiveObj;
-  }
-  return null;
-}
-
 export function shouldInclude(
   selection: SelectionNode,
   variables: { [name: string]: any } = {},
diff --git a/packages/apollo-utilities/src/fragments.ts b/src/utilities/graphql/fragments.ts
similarity index 70%
rename from packages/apollo-utilities/src/fragments.ts
rename to src/utilities/graphql/fragments.ts
--- a/packages/apollo-utilities/src/fragments.ts
+++ b/src/utilities/graphql/fragments.ts
@@ -1,4 +1,9 @@
-import { DocumentNode, FragmentDefinitionNode } from 'graphql';
+import {
+  DocumentNode,
+  FragmentDefinitionNode,
+  InlineFragmentNode,
+  SelectionNode
+} from 'graphql';
 import { invariant, InvariantError } from 'ts-invariant';
 
 /**
@@ -90,3 +95,39 @@ export function getFragmentQueryDocument(
 
   return query;
 }
+
+/**
+ * This is an interface that describes a map from fragment names to fragment definitions.
+ */
+export interface FragmentMap {
+  [fragmentName: string]: FragmentDefinitionNode;
+}
+
+// Utility function that takes a list of fragment definitions and makes a hash out of them
+// that maps the name of the fragment to the fragment definition.
+export function createFragmentMap(
+  fragments: FragmentDefinitionNode[] = [],
+): FragmentMap {
+  const symTable: FragmentMap = {};
+  fragments.forEach(fragment => {
+    symTable[fragment.name.value] = fragment;
+  });
+  return symTable;
+}
+
+export function getFragmentFromSelection(
+  selection: SelectionNode,
+  fragmentMap: FragmentMap,
+): InlineFragmentNode | FragmentDefinitionNode | null {
+  switch (selection.kind) {
+    case 'InlineFragment':
+      return selection;
+    case 'FragmentSpread': {
+      const fragment = fragmentMap && fragmentMap[selection.name.value];
+      invariant(fragment, `No fragment named ${selection.name.value}.`);
+      return fragment;
+    }
+    default:
+      return null;
+  }
+}
diff --git a/packages/apollo-utilities/src/getFromAST.ts b/src/utilities/graphql/getFromAST.ts
similarity index 70%
rename from packages/apollo-utilities/src/getFromAST.ts
rename to src/utilities/graphql/getFromAST.ts
--- a/packages/apollo-utilities/src/getFromAST.ts
+++ b/src/utilities/graphql/getFromAST.ts
@@ -7,25 +7,9 @@ import {
 
 import { invariant, InvariantError } from 'ts-invariant';
 
-import { assign } from './util/assign';
+import { assign } from '../common/assign';
 
-import { valueToObjectRepresentation, JsonValue } from './storeUtils';
-
-export function getMutationDefinition(
-  doc: DocumentNode,
-): OperationDefinitionNode {
-  checkDocument(doc);
-
-  let mutationDef: OperationDefinitionNode | null = doc.definitions.filter(
-    definition =>
-      definition.kind === 'OperationDefinition' &&
-      definition.operation === 'mutation',
-  )[0] as OperationDefinitionNode;
-
-  invariant(mutationDef, 'Must contain a mutation definition.');
-
-  return mutationDef;
-}
+import { valueToObjectRepresentation } from './storeUtils';
 
 // Checks the document for errors and throws an exception if there is an error.
 export function checkDocument(doc: DocumentNode) {
@@ -65,14 +49,6 @@ export function getOperationDefinition(
   )[0] as OperationDefinitionNode;
 }
 
-export function getOperationDefinitionOrDie(
-  document: DocumentNode,
-): OperationDefinitionNode {
-  const def = getOperationDefinition(document);
-  invariant(def, `GraphQL document is missing an operation`);
-  return def;
-}
-
 export function getOperationName(doc: DocumentNode): string | null {
   return (
     doc.definitions
@@ -167,29 +143,9 @@ export function getMainDefinition(
   );
 }
 
-/**
- * This is an interface that describes a map from fragment names to fragment definitions.
- */
-export interface FragmentMap {
-  [fragmentName: string]: FragmentDefinitionNode;
-}
-
-// Utility function that takes a list of fragment definitions and makes a hash out of them
-// that maps the name of the fragment to the fragment definition.
-export function createFragmentMap(
-  fragments: FragmentDefinitionNode[] = [],
-): FragmentMap {
-  const symTable: FragmentMap = {};
-  fragments.forEach(fragment => {
-    symTable[fragment.name.value] = fragment;
-  });
-
-  return symTable;
-}
-
 export function getDefaultValues(
   definition: OperationDefinitionNode | undefined,
-): { [key: string]: JsonValue } {
+): Record<string, any> {
   if (
     definition &&
     definition.variableDefinitions &&
@@ -198,8 +154,8 @@ export function getDefaultValues(
     const defaultValues = definition.variableDefinitions
       .filter(({ defaultValue }) => defaultValue)
       .map(
-        ({ variable, defaultValue }): { [key: string]: JsonValue } => {
-          const defaultValueObj: { [key: string]: JsonValue } = {};
+        ({ variable, defaultValue }): Record<string, any> => {
+          const defaultValueObj: Record<string, any> = {};
           valueToObjectRepresentation(
             defaultValueObj,
             variable.name,
@@ -215,19 +171,3 @@ export function getDefaultValues(
 
   return {};
 }
-
-/**
- * Returns the names of all variables declared by the operation.
- */
-export function variablesInOperation(
-  operation: OperationDefinitionNode,
-): Set<string> {
-  const names = new Set<string>();
-  if (operation.variableDefinitions) {
-    for (const definition of operation.variableDefinitions) {
-      names.add(definition.variable.name.value);
-    }
-  }
-
-  return names;
-}
diff --git a/packages/apollo-utilities/src/storeUtils.ts b/src/utilities/graphql/storeUtils.ts
similarity index 73%
rename from packages/apollo-utilities/src/storeUtils.ts
rename to src/utilities/graphql/storeUtils.ts
--- a/packages/apollo-utilities/src/storeUtils.ts
+++ b/src/utilities/graphql/storeUtils.ts
@@ -14,49 +14,36 @@ import {
   ValueNode,
   SelectionNode,
   NameNode,
+  SelectionSetNode,
 } from 'graphql';
 
 import stringify from 'fast-json-stable-stringify';
 import { InvariantError } from 'ts-invariant';
+import { FragmentMap, getFragmentFromSelection } from './fragments';
 
-export interface IdValue {
-  type: 'id';
-  id: string;
-  generated: boolean;
-  typename: string | undefined;
+export interface Reference {
+  readonly __ref: string;
 }
 
-export interface JsonValue {
-  type: 'json';
-  json: any;
+export function makeReference(id: string): Reference {
+  return { __ref: String(id) };
 }
 
-export type ListValue = Array<null | IdValue>;
+export function isReference(obj: any): obj is Reference {
+  return obj && typeof obj === 'object' && typeof obj.__ref === 'string';
+}
 
 export type StoreValue =
   | number
   | string
   | string[]
-  | IdValue
-  | ListValue
-  | JsonValue
+  | Reference
+  | Reference[]
   | null
   | undefined
   | void
   | Object;
 
-export type ScalarValue = StringValueNode | BooleanValueNode | EnumValueNode;
-
-export function isScalarValue(value: ValueNode): value is ScalarValue {
-  return ['StringValue', 'BooleanValue', 'EnumValue'].indexOf(value.kind) > -1;
-}
-
-export type NumberValue = IntValueNode | FloatValueNode;
-
-export function isNumberValue(value: ValueNode): value is NumberValue {
-  return ['IntValue', 'FloatValue'].indexOf(value.kind) > -1;
-}
-
 function isStringValue(value: ValueNode): value is StringValueNode {
   return value.kind === 'StringValue';
 }
@@ -261,6 +248,33 @@ export function resultKeyNameFromField(field: FieldNode): string {
   return field.alias ? field.alias.value : field.name.value;
 }
 
+export function getTypenameFromResult(
+  result: Record<string, any>,
+  selectionSet: SelectionSetNode,
+  fragmentMap: FragmentMap,
+): string | undefined {
+  if (typeof result.__typename === 'string') {
+    return result.__typename;
+  }
+
+  for (const selection of selectionSet.selections) {
+    if (isField(selection)) {
+      if (selection.name.value === '__typename') {
+        return result[resultKeyNameFromField(selection)];
+      }
+    } else {
+      const typename = getTypenameFromResult(
+        result,
+        getFragmentFromSelection(selection, fragmentMap).selectionSet,
+        fragmentMap,
+      );
+      if (typeof typename === 'string') {
+        return typename;
+      }
+    }
+  }
+}
+
 export function isField(selection: SelectionNode): selection is FieldNode {
   return selection.kind === 'Field';
 }
@@ -271,70 +285,5 @@ export function isInlineFragment(
   return selection.kind === 'InlineFragment';
 }
 
-export function isIdValue(idObject: StoreValue): idObject is IdValue {
-  return idObject &&
-    (idObject as IdValue | JsonValue).type === 'id' &&
-    typeof (idObject as IdValue).generated === 'boolean';
-}
-
-export type IdConfig = {
-  id: string;
-  typename: string | undefined;
-};
-
-export function toIdValue(
-  idConfig: string | IdConfig,
-  generated = false,
-): IdValue {
-  return {
-    type: 'id',
-    generated,
-    ...(typeof idConfig === 'string'
-      ? { id: idConfig, typename: undefined }
-      : idConfig),
-  };
-}
-
-export function isJsonValue(jsonObject: StoreValue): jsonObject is JsonValue {
-  return (
-    jsonObject != null &&
-    typeof jsonObject === 'object' &&
-    (jsonObject as IdValue | JsonValue).type === 'json'
-  );
-}
-
-function defaultValueFromVariable(node: VariableNode) {
-  throw new InvariantError(`Variable nodes are not supported by valueFromNode`);
-}
-
 export type VariableValue = (node: VariableNode) => any;
 
-/**
- * Evaluate a ValueNode and yield its value in its natural JS form.
- */
-export function valueFromNode(
-  node: ValueNode,
-  onVariable: VariableValue = defaultValueFromVariable,
-): any {
-  switch (node.kind) {
-    case 'Variable':
-      return onVariable(node);
-    case 'NullValue':
-      return null;
-    case 'IntValue':
-      return parseInt(node.value, 10);
-    case 'FloatValue':
-      return parseFloat(node.value);
-    case 'ListValue':
-      return node.values.map(v => valueFromNode(v, onVariable));
-    case 'ObjectValue': {
-      const value: { [key: string]: any } = {};
-      for (const field of node.fields) {
-        value[field.name.value] = valueFromNode(field.value, onVariable);
-      }
-      return value;
-    }
-    default:
-      return node.value;
-  }
-}
diff --git a/packages/apollo-utilities/src/transform.ts b/src/utilities/graphql/transform.ts
similarity index 92%
rename from packages/apollo-utilities/src/transform.ts
rename to src/utilities/graphql/transform.ts
--- a/packages/apollo-utilities/src/transform.ts
+++ b/src/utilities/graphql/transform.ts
@@ -12,19 +12,21 @@ import {
   VariableNode,
 } from 'graphql';
 import { visit } from 'graphql/language/visitor';
+import { invariant } from 'ts-invariant';
 
 import {
   checkDocument,
   getOperationDefinition,
   getFragmentDefinition,
   getFragmentDefinitions,
-  createFragmentMap,
-  FragmentMap,
   getMainDefinition,
 } from './getFromAST';
-import { filterInPlace } from './util/filterInPlace';
-import { invariant } from 'ts-invariant';
+import { filterInPlace } from '../common/filterInPlace';
 import { isField, isInlineFragment } from './storeUtils';
+import {
+  createFragmentMap,
+  FragmentMap,
+} from './fragments';
 
 export type RemoveNodeConfig<N> = {
   name?: string;
@@ -323,48 +325,6 @@ function hasDirectivesInSelection(
   );
 }
 
-export function getDirectivesFromDocument(
-  directives: GetDirectiveConfig[],
-  doc: DocumentNode,
-): DocumentNode {
-  checkDocument(doc);
-
-  let parentPath: string;
-
-  return nullIfDocIsEmpty(
-    visit(doc, {
-      SelectionSet: {
-        enter(node, _key, _parent, path) {
-          const currentPath = path.join('-');
-
-          if (
-            !parentPath ||
-            currentPath === parentPath ||
-            !currentPath.startsWith(parentPath)
-          ) {
-            if (node.selections) {
-              const selectionsWithDirectives = node.selections.filter(
-                selection => hasDirectivesInSelection(directives, selection),
-              );
-
-              if (hasDirectivesInSelectionSet(directives, node, false)) {
-                parentPath = currentPath;
-              }
-
-              return {
-                ...node,
-                selections: selectionsWithDirectives,
-              };
-            } else {
-              return null;
-            }
-          }
-        },
-      },
-    }),
-  );
-}
-
 function getArgumentMatcher(config: RemoveArgumentsConfig[]) {
   return function argumentMatcher(argument: ArgumentNode) {
     return config.some(
diff --git a/src/utilities/index.ts b/src/utilities/index.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/index.ts
@@ -0,0 +1,63 @@
+export {
+  DirectiveInfo,
+  InclusionDirectives,
+  shouldInclude,
+  hasDirectives,
+  hasClientExports,
+  getDirectiveNames,
+  getInclusionDirectives,
+} from './graphql/directives';
+
+export {
+  FragmentMap,
+  createFragmentMap,
+  getFragmentQueryDocument,
+  getFragmentFromSelection,
+} from './graphql/fragments';
+
+export {
+  checkDocument,
+  getOperationDefinition,
+  getOperationName,
+  getFragmentDefinitions,
+  getQueryDefinition,
+  getFragmentDefinition,
+  getMainDefinition,
+  getDefaultValues,
+} from './graphql/getFromAST';
+
+export {
+  Reference,
+  StoreValue,
+  Directives,
+  VariableValue,
+  makeReference,
+  isReference,
+  isField,
+  isInlineFragment,
+  valueToObjectRepresentation,
+  storeKeyNameFromField,
+  argumentsObjectFromField,
+  resultKeyNameFromField,
+  getStoreKeyName,
+  getTypenameFromResult,
+} from './graphql/storeUtils';
+
+export {
+  RemoveNodeConfig,
+  GetNodeConfig,
+  RemoveDirectiveConfig,
+  GetDirectiveConfig,
+  RemoveArgumentsConfig,
+  GetFragmentSpreadConfig,
+  RemoveFragmentSpreadConfig,
+  RemoveFragmentDefinitionConfig,
+  RemoveVariableDefinitionConfig,
+  addTypenameToDocument,
+  buildQueryFromSelectionSet,
+  removeDirectivesFromDocument,
+  removeConnectionDirectiveFromDocument,
+  removeArgumentsFromDocument,
+  removeFragmentSpreadFromDocument,
+  removeClientSetsFromDocument,
+} from './graphql/transform';
diff --git a/src/utilities/observables/Observable.ts b/src/utilities/observables/Observable.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/observables/Observable.ts
@@ -0,0 +1,21 @@
+import Observable from 'zen-observable';
+
+// This simplified polyfill attempts to follow the ECMAScript Observable
+// proposal (https://github.com/zenparsing/es-observable)
+import 'symbol-observable';
+
+export type Subscription = ZenObservable.Subscription;
+export type Observer<T> = ZenObservable.Observer<T>;
+
+// Use global module augmentation to add RxJS interop functionality. By
+// using this approach (instead of subclassing `Observable` and adding an
+// ['@@observable']() method), we ensure the exported `Observable` retains all
+// existing type declarations from `@types/zen-observable` (which is important
+// for projects like `apollo-link`).
+declare global {
+  interface Observable<T> {
+    ['@@observable'](): Observable<T>;
+  }
+}
+(Observable.prototype as any)['@@observable'] = function () { return this; };
+export { Observable };
diff --git a/packages/apollo-client/src/util/observables.ts b/src/utilities/observables/observables.ts
similarity index 100%
rename from packages/apollo-client/src/util/observables.ts
rename to src/utilities/observables/observables.ts
diff --git a/packages/apollo-client/src/version.ts b/src/version.ts
similarity index 100%
rename from packages/apollo-client/src/version.ts
rename to src/version.ts


Test patch:
diff --git a/docs/source/development-testing/testing.mdx b/docs/source/development-testing/testing.mdx
--- a/docs/source/development-testing/testing.mdx
+++ b/docs/source/development-testing/testing.mdx
@@ -1,21 +1,21 @@
 ---
 title: Testing React components
-description: Have peace of mind when using React Apollo in production
+description: Peace of mind when using Apollo Client with React in production
 ---
 
 import { MultiCodeBlock } from 'gatsby-theme-apollo-docs';
 
-Running tests against code meant for production has long been a best practice. It provides additional security for the code that's already written, and prevents accidental regressions in the future. Components utilizing React Apollo, the React implementation of Apollo Client, are no exception.
+Running tests against code meant for production has long been a best practice. It provides additional security for the code that's already written, and prevents accidental regressions in the future. Components utilizing React with Apollo Client are no exception.
 
-Although React Apollo has a lot going on under the hood, the library provides multiple tools for testing that simplify those abstractions, and allows complete focus on the component logic. These testing utilities have long been used to test the React Apollo library itself, so they will be supported long-term.
+Although Apollo Client's React integration has a lot going on under the hood, the library provides multiple tools for testing that simplify those abstractions, and allows complete focus on the component logic. These testing utilities have long been used to test React functoinality with Apollo Client itself, so they will be supported long-term.
 
 ## An introduction
 
-The React Apollo library relies on [React's context](https://reactjs.org/docs/context.html) to pass the `ApolloClient` instance through the React component tree. In addition, React Apollo makes network requests in order to fetch data. This behavior affects how tests should be written for components that use React Apollo.
+Apollo Client's React integration relies on [React's context](https://reactjs.org/docs/context.html) to pass the `ApolloClient` instance through the React component tree. In addition, Apollo Client makes network requests in order to fetch data. This behavior affects how tests should be written for components that use Apollo Client with React.
 
-This guide will explain step-by-step how to test React Apollo code. The following examples use the [Jest](https://facebook.github.io/jest/docs/en/tutorial-react.html) testing framework, but most concepts should be reusable with other libraries. These examples aim to use as simple of a toolset as possible, so React's [test renderer](https://reactjs.org/docs/test-renderer.html) will be used in place of React-specific tools like [Enzyme](https://github.com/airbnb/enzyme) and [react-testing-library](https://github.com/kentcdodds/react-testing-library).
+This guide will explain step-by-step how to test Apollo Client React code. The following examples use the [Jest](https://facebook.github.io/jest/docs/en/tutorial-react.html) testing framework, but most concepts should be reusable with other libraries. These examples aim to use as simple of a toolset as possible, so React's [test renderer](https://reactjs.org/docs/test-renderer.html) will be used in place of React-specific tools like [Enzyme](https://github.com/airbnb/enzyme) and [react-testing-library](https://github.com/kentcdodds/react-testing-library).
 
-> **Note:** As of React Apollo 3, all testing utilities can now be found in their own `@apollo/react-testing` package.
+> **Note:** As of Apollo Client 3, all testing utilities can now be imported from `@apollo/client/testing`.
 
 Consider the component below, which makes a basic query, and displays its results:
 
@@ -24,8 +24,7 @@ Consider the component below, which makes a basic query, and displays its result
 
 ```jsx
 import React from 'react';
-import gql from 'graphql-tag';
-import { useQuery } from '@apollo/react-hooks';
+import { gql, useQuery } from '@apollo/client';
 
 // Make sure the query is also exported -- not just the component
 export const GET_DOG_QUERY = gql`
@@ -59,8 +58,8 @@ export function Dog({ name }) {
 
 ```jsx
 import React from 'react';
-import gql from 'graphql-tag';
-import { Query } from 'react-apollo';
+import { gql } from "@apollo/client";
+import { Query } from '@apollo/react-components';
 
 // Make sure the query is also exported -- not just the component
 export const GET_DOG_QUERY = gql`
@@ -122,7 +121,7 @@ it('renders without error', () => {
 
 ## `MockedProvider`
 
-The `@apollo/react-testing` package exports a `MockedProvider` component which simplifies the testing of React components by mocking calls to the GraphQL endpoint.  This allows the tests to be run in isolation and provides consistent results on every run by removing the dependence on remote data.
+The `@apollo/client` package exports a `MockedProvider` component which simplifies the testing of React components by mocking calls to the GraphQL endpoint.  This allows the tests to be run in isolation and provides consistent results on every run by removing the dependence on remote data.
 
 By using this `MockedProvider` component, it's possible to specify the exact results that should be returned for a certain query using the `mocks` prop.
 
@@ -131,7 +130,7 @@ Here's an example of a test for the above `Dog` component using `MockedProvider`
 ```jsx
 // dog.test.js
 
-import { MockedProvider } from '@apollo/react-testing';
+import { MockedProvider } from '@apollo/client/testing';
 
 // The component AND the query need to be exported
 import { GET_DOG_QUERY, Dog } from './dog';
diff --git a/packages/apollo-boost/src/__tests__/__snapshots__/config.ts.snap b/packages/apollo-boost/src/__tests__/__snapshots__/config.ts.snap
deleted file mode 100644
--- a/packages/apollo-boost/src/__tests__/__snapshots__/config.ts.snap
+++ /dev/null
@@ -1,9 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`config warns about unsupported parameter 1`] = `
-Array [
-  Array [
-    "ApolloBoost was initialized with unsupported options: link",
-  ],
-]
-`;
diff --git a/packages/apollo-boost/src/__tests__/config.ts b/packages/apollo-boost/src/__tests__/config.ts
deleted file mode 100644
--- a/packages/apollo-boost/src/__tests__/config.ts
+++ /dev/null
@@ -1,225 +0,0 @@
-import ApolloClient, { gql, InMemoryCache } from '../';
-import { stripSymbols } from 'apollo-utilities';
-import fetchMock from 'fetch-mock';
-
-global.fetch = jest.fn(() =>
-  Promise.resolve({ json: () => Promise.resolve({}) }),
-);
-
-const sleep = ms => new Promise(res => setTimeout(res, ms));
-
-describe('config', () => {
-  const query = gql`
-    {
-      foo @client
-    }
-  `;
-
-  const remoteQuery = gql`
-    {
-      foo
-    }
-  `;
-
-  const resolvers = {
-    Query: {
-      foo: () => 'woo',
-    },
-  };
-
-  it('warns about unsupported parameter', () => {
-    jest.spyOn(global.console, 'warn');
-
-    const client = new ApolloClient({
-      link: [],
-    });
-
-    expect(global.console.warn.mock.calls).toMatchSnapshot();
-  });
-
-  it('allows you to pass in a custom fetcher', () => {
-    const customFetcher = jest.fn(() =>
-      Promise.resolve({
-        text: () => Promise.resolve('{"data": {"foo": "bar" }}'),
-      }),
-    );
-
-    const client = new ApolloClient({
-      fetch: customFetcher,
-    });
-
-    client.query({ query }).then(({ data }) => {
-      expect(customFetcher).toHaveBeenCalledTimes(1);
-      expect(stripSymbols(data)).toEqual({ foo: 'bar' });
-    });
-  });
-
-  it('allows you to pass in a request handler', () => {
-    const customFetcher = jest.fn(() =>
-      Promise.resolve({
-        text: () => Promise.resolve('{"data": {"foo": "woo" }}'),
-      }),
-    );
-
-    let requestCalled;
-
-    const client = new ApolloClient({
-      request: () => {
-        requestCalled = true;
-      },
-      fetch: customFetcher,
-    });
-
-    return client
-      .query({ query: remoteQuery, fetchPolicy: 'network-only' })
-      .then(({ data }) => {
-        expect(stripSymbols(data)).toEqual({ foo: 'woo' });
-        expect(requestCalled).toEqual(true);
-      });
-  });
-
-  it('allows you to pass in an async request handler', () => {
-    const customFetcher = jest.fn(() =>
-      Promise.resolve({
-        text: () => Promise.resolve('{"data": {"foo": "woo" }}'),
-      }),
-    );
-
-    let requestCalled;
-
-    const client = new ApolloClient({
-      request: () => {
-        Promise.resolve().then(() => {
-          requestCalled = true;
-        });
-      },
-      fetch: customFetcher,
-    });
-
-    return client
-      .query({ query: remoteQuery, fetchPolicy: 'network-only' })
-      .then(({ data }) => {
-        expect(stripSymbols(data)).toEqual({ foo: 'woo' });
-        expect(requestCalled).toEqual(true);
-      });
-  });
-
-  it('throws if passed cache and cacheRedirects', () => {
-    const cache = new InMemoryCache();
-    const cacheRedirects = { Query: { foo: () => 'woo' } };
-
-    expect(_ => {
-      const client = new ApolloClient({
-        cache,
-        cacheRedirects,
-      });
-    }).toThrow('Incompatible cache configuration');
-  });
-
-  it('allows you to pass in cache', () => {
-    const cache = new InMemoryCache();
-
-    const client = new ApolloClient({
-      cache,
-    });
-
-    expect(client.cache).toBe(cache);
-  });
-
-  it('allows you to pass in cacheRedirects', () => {
-    const cacheRedirects = { Query: { foo: () => 'woo' } };
-
-    const client = new ApolloClient({
-      cacheRedirects,
-    });
-
-    expect(client.cache.config.cacheRedirects).toEqual(cacheRedirects);
-  });
-
-  it('allows you to pass in name and version', () => {
-    const name = 'client-name';
-    const version = 'client-version';
-
-    const client = new ApolloClient({
-      name,
-      version,
-    });
-
-    const { clientAwareness } = client.queryManager as any;
-    expect(clientAwareness.name).toEqual(name);
-    expect(clientAwareness.version).toEqual(version);
-  });
-
-  const makePromise = res =>
-    new Promise((resolve, reject) => setTimeout(() => resolve(res)));
-  const data = { data: { hello: 'world' } };
-
-  describe('credentials', () => {
-    beforeEach(() => {
-      fetchMock.restore();
-      fetchMock.post('/graphql', makePromise(data));
-    });
-
-    afterEach(() => {
-      fetchMock.restore();
-    });
-
-    it('should set `credentials` to `same-origin` by default', () => {
-      const client = new ApolloClient({});
-      client.query({ query: remoteQuery, errorPolicy: 'ignore' });
-      const [uri, options] = fetchMock.lastCall();
-      expect(options.credentials).toEqual('same-origin');
-    });
-
-    it('should set `credentials` to `config.credentials` if supplied', () => {
-      const client = new ApolloClient({
-        credentials: 'some-new-value',
-      });
-      client.query({ query: remoteQuery, errorPolicy: 'ignore' });
-      const [uri, options] = fetchMock.lastCall();
-      expect(options.credentials).toEqual('some-new-value');
-    });
-  });
-
-  describe('headers', () => {
-    beforeEach(() => {
-      fetchMock.restore();
-      fetchMock.post('/graphql', makePromise(data));
-    });
-
-    afterEach(() => {
-      fetchMock.restore();
-    });
-
-    it(
-      'should leave existing `headers` in place if no new headers are ' +
-        'provided',
-      () => {
-        const client = new ApolloClient({});
-        client.query({ query: remoteQuery, errorPolicy: 'ignore' });
-        const [uri, options] = fetchMock.lastCall();
-        expect(options.headers).toEqual({
-          accept: '*/*',
-          'content-type': 'application/json',
-        });
-      },
-    );
-
-    it('should add new `config.headers` to existing headers', () => {
-      const client = new ApolloClient({
-        headers: {
-          'new-header1': 'value1',
-          'new-header2': 'value2',
-        },
-      });
-      client.query({ query: remoteQuery, errorPolicy: 'ignore' });
-      const [uri, options] = fetchMock.lastCall();
-      expect(options.headers).toEqual({
-        accept: '*/*',
-        'content-type': 'application/json',
-        'new-header1': 'value1',
-        'new-header2': 'value2',
-      });
-    });
-  });
-});
diff --git a/packages/apollo-boost/src/__tests__/smoke.ts b/packages/apollo-boost/src/__tests__/smoke.ts
deleted file mode 100644
--- a/packages/apollo-boost/src/__tests__/smoke.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import ApolloClient, { gql, HttpLink, InMemoryCache } from '../';
-
-global.fetch = jest.fn(() =>
-  Promise.resolve({ json: () => Promise.resolve({}) }),
-);
-it('should have the required exports', () => {
-  expect(ApolloClient).toBeDefined();
-  expect(gql).toBeDefined();
-  expect(HttpLink).toBeDefined();
-  expect(InMemoryCache).toBeDefined();
-});
-
-it('should make a client with defaults', () => {
-  const client = new ApolloClient();
-  expect(client.link).toBeDefined();
-  expect(client.store.cache).toBeDefined();
-});
diff --git a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/cache.ts.snap b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/cache.ts.snap
deleted file mode 100644
--- a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/cache.ts.snap
+++ /dev/null
@@ -1,367 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Cache writeFragment will write some deeply nested data into the store at any id (1/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 (1/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 (1/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 (1/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 (1/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 (1/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 (2/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 (2/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 (2/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 (2/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 (2/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 (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,
-    "j": 11,
-    "k": 12,
-  },
-  "foo": Object {
-    "e": 4,
-    "f": 5,
-    "g": 6,
-    "h": Object {
-      "generated": false,
-      "id": "bar",
-      "type": "id",
-      "typename": "Bar",
-    },
-  },
-}
-`;
diff --git a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/mapCache.ts.snap b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/mapCache.ts.snap
deleted file mode 100644
--- a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/mapCache.ts.snap
+++ /dev/null
@@ -1,374 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`MapCache Cache writeFragment will write some deeply nested data into the store at any id (1/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 (1/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 (1/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 (1/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 (1/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 (1/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 (2/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 (2/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 (2/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 (2/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 (2/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 (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,
-    "j": 11,
-    "k": 12,
-  },
-  "foo": Object {
-    "e": 4,
-    "f": 5,
-    "g": 6,
-    "h": Object {
-      "generated": false,
-      "id": "bar",
-      "type": "id",
-      "typename": "Bar",
-    },
-  },
-}
-`;
-
-exports[`MapCache writing to the store throws when trying to write an object without id that was previously queried with id 1`] = `
-"Error writing result to store for query:
- {\\"kind\\":\\"Document\\",\\"definitions\\":[{\\"kind\\":\\"OperationDefinition\\",\\"operation\\":\\"query\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"Failure\\"},\\"variableDefinitions\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"item\\"},\\"arguments\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"stringField\\"},\\"arguments\\":[],\\"directives\\":[]}]}}]}}],\\"loc\\":{\\"start\\":0,\\"end\\":106}}
-Store error: the application attempted to write an object with no provided id but the store already contains an id of abcd for this object. The selectionSet that was trying to be written is:
-{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"item\\"},\\"arguments\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"stringField\\"},\\"arguments\\":[],\\"directives\\":[]}]}}"
-`;
diff --git a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/roundtrip.ts.snap b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/roundtrip.ts.snap
deleted file mode 100644
--- a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/roundtrip.ts.snap
+++ /dev/null
@@ -1,10 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[
-  `writing to the store throws when trying to write an object without id that was previously queried with id 1`
-] = `
-"Error writing result to store for query:
- {\\"kind\\":\\"Document\\",\\"definitions\\":[{\\"kind\\":\\"OperationDefinition\\",\\"operation\\":\\"query\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"Failure\\"},\\"variableDefinitions\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"item\\"},\\"arguments\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"stringField\\"},\\"arguments\\":[],\\"directives\\":[]}]}}]}}],\\"loc\\":{\\"start\\":0,\\"end\\":106}}
-Store error: the application attempted to write an object with no provided id but the store already contains an id of abcd for this object. The selectionSet that was trying to be written is:
-{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"item\\"},\\"arguments\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"stringField\\"},\\"arguments\\":[],\\"directives\\":[]}]}}"
-`;
diff --git a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/writeToStore.ts.snap b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/writeToStore.ts.snap
deleted file mode 100644
--- a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/writeToStore.ts.snap
+++ /dev/null
@@ -1,10 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[
-  `writing to the store throws when trying to write an object without id that was previously queried with id 1`
-] = `
-"Error writing result to store for query:
- {\\"kind\\":\\"Document\\",\\"definitions\\":[{\\"kind\\":\\"OperationDefinition\\",\\"operation\\":\\"query\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"Failure\\"},\\"variableDefinitions\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"item\\"},\\"arguments\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"stringField\\"},\\"arguments\\":[],\\"directives\\":[]}]}}]}}],\\"loc\\":{\\"start\\":0,\\"end\\":106}}
-Store error: the application attempted to write an object with no provided id but the store already contains an id of abcd for this object. The selectionSet that was trying to be written is:
-{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"item\\"},\\"arguments\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"stringField\\"},\\"arguments\\":[],\\"directives\\":[]}]}}"
-`;
diff --git a/packages/apollo-cache-inmemory/src/__tests__/fragmentMatcher.ts b/packages/apollo-cache-inmemory/src/__tests__/fragmentMatcher.ts
deleted file mode 100644
--- a/packages/apollo-cache-inmemory/src/__tests__/fragmentMatcher.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import { IntrospectionFragmentMatcher } from '../fragmentMatcher';
-import { defaultNormalizedCacheFactory } from '../objectCache';
-import { ReadStoreContext } from '../types';
-import { InMemoryCache } from '../inMemoryCache';
-import gql from 'graphql-tag';
-
-describe('FragmentMatcher', () => {
-  it('can match against the root Query', () => {
-    const cache = new InMemoryCache({
-      addTypename: true,
-    });
-
-    const query = gql`
-      query AllPeople {
-        people {
-          id
-          name
-        }
-        ...PeopleTypes
-      }
-      fragment PeopleTypes on Query {
-        __type(name: "Person") {
-          name
-          kind
-        }
-      }
-    `;
-
-    const data = {
-      people: [
-        {
-          __typename: 'Person',
-          id: 123,
-          name: 'Ben',
-        },
-      ],
-      __type: {
-        __typename: '__Type',
-        name: 'Person',
-        kind: 'OBJECT',
-      },
-    };
-
-    cache.writeQuery({ query, data });
-    expect(cache.readQuery({ query })).toEqual(data);
-  });
-});
-
-describe('IntrospectionFragmentMatcher', () => {
-  it('will throw an error if match is called if it is not ready', () => {
-    const ifm = new IntrospectionFragmentMatcher();
-    expect(() => (ifm.match as any)()).toThrowError(/called before/);
-  });
-
-  it('can be seeded with an introspection query result', () => {
-    const ifm = new IntrospectionFragmentMatcher({
-      introspectionQueryResultData: {
-        __schema: {
-          types: [
-            {
-              kind: 'UNION',
-              name: 'Item',
-              possibleTypes: [
-                {
-                  name: 'ItemA',
-                },
-                {
-                  name: 'ItemB',
-                },
-              ],
-            },
-          ],
-        },
-      },
-    });
-
-    const store = defaultNormalizedCacheFactory({
-      a: {
-        __typename: 'ItemB',
-      },
-    });
-
-    const idValue = {
-      type: 'id',
-      id: 'a',
-      generated: false,
-    };
-
-    const readStoreContext = {
-      store,
-      returnPartialData: false,
-      hasMissingField: false,
-      cacheRedirects: {},
-    } as ReadStoreContext;
-
-    expect(ifm.match(idValue as any, 'Item', readStoreContext)).toBe(true);
-    expect(ifm.match(idValue as any, 'NotAnItem', readStoreContext)).toBe(
-      false,
-    );
-  });
-});
diff --git a/packages/apollo-cache-inmemory/src/__tests__/mapCache.ts b/packages/apollo-cache-inmemory/src/__tests__/mapCache.ts
deleted file mode 100644
--- a/packages/apollo-cache-inmemory/src/__tests__/mapCache.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-jest.mock('../objectCache', () => {
-  const { MapCache, mapNormalizedCacheFactory } = require('../mapCache');
-  return {
-    ObjectCache: MapCache,
-    defaultNormalizedCacheFactory: mapNormalizedCacheFactory,
-  };
-});
-
-describe('MapCache', () => {
-  // simply re-runs all the tests
-  // with the alternative implementation of the cache
-  require('./objectCache');
-  require('./cache');
-  require('./diffAgainstStore');
-  require('./fragmentMatcher');
-  require('./readFromStore');
-  require('./diffAgainstStore');
-  require('./roundtrip');
-  require('./writeToStore');
-});
diff --git a/packages/apollo-cache-inmemory/src/__tests__/objectCache.ts b/packages/apollo-cache-inmemory/src/__tests__/objectCache.ts
deleted file mode 100644
--- a/packages/apollo-cache-inmemory/src/__tests__/objectCache.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { ObjectCache } from '../objectCache';
-import { NormalizedCacheObject } from '../types';
-
-describe('ObjectCache', () => {
-  it('should create an empty cache', () => {
-    const cache = new ObjectCache();
-    expect(cache.toObject()).toEqual({});
-  });
-
-  it('should create a cache based on an Object', () => {
-    const contents: NormalizedCacheObject = { a: {} };
-    const cache = new ObjectCache(contents);
-    expect(cache.toObject()).toEqual(contents);
-  });
-
-  it(`should .get() an object from the store by dataId`, () => {
-    const contents: NormalizedCacheObject = { a: {} };
-    const cache = new ObjectCache(contents);
-    expect(cache.get('a')).toBe(contents.a);
-  });
-
-  it(`should .set() an object from the store by dataId`, () => {
-    const obj = {};
-    const cache = new ObjectCache();
-    cache.set('a', obj);
-    expect(cache.get('a')).toBe(obj);
-  });
-
-  it(`should .clear() the store`, () => {
-    const obj = {};
-    const cache = new ObjectCache();
-    cache.set('a', obj);
-    cache.clear();
-    expect(cache.get('a')).toBeUndefined();
-  });
-});
diff --git a/packages/apollo-client/scripts/test_and_lint.sh b/packages/apollo-client/scripts/test_and_lint.sh
deleted file mode 100644
--- a/packages/apollo-client/scripts/test_and_lint.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-sleep 5
-
-run_command="npm run testonly"
-
-if [[ $# -gt 0 ]]; then
-    run_command+=' -- --grep "'
-    run_command+=$@
-    run_command+='"'
-fi
-
-lint_command="npm run lint"
-
-command="$run_command"
-command+=" && "
-command+="$lint_command"
-
-nodemon --watch lib --exec "$command" --delay 0.5
diff --git a/packages/apollo-client/src/__tests__/__snapshots__/ApolloClient.ts.snap b/packages/apollo-client/src/__tests__/__snapshots__/ApolloClient.ts.snap
deleted file mode 100644
--- a/packages/apollo-client/src/__tests__/__snapshots__/ApolloClient.ts.snap
+++ /dev/null
@@ -1,511 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ApolloClient constructor will throw an error if cache is not passed in 1`] = `
-"In order to initialize Apollo Client, you must specify 'link' and 'cache' properties in the options object.
-These options are part of the upgrade requirements when migrating from Apollo Client 1.x to Apollo Client 2.x.
-For more information, please visit: https://www.apollographql.com/docs/tutorial/client.html#apollo-client-setup"
-`;
-
-exports[`ApolloClient constructor will throw an error if link is not passed in 1`] = `
-"In order to initialize Apollo Client, you must specify 'link' and 'cache' properties in the options object.
-These options are part of the upgrade requirements when migrating from Apollo Client 1.x to Apollo Client 2.x.
-For more information, please visit: https://www.apollographql.com/docs/tutorial/client.html#apollo-client-setup"
-`;
-
-exports[`ApolloClient write then read will not use a default id getter if either _id or id is present when __typename is not also present 1`] = `
-Object {
-  "$ROOT_QUERY.bar": Object {
-    "foo": Object {
-      "generated": true,
-      "id": "$ROOT_QUERY.bar.foo",
-      "type": "id",
-      "typename": undefined,
-    },
-    "i": 10,
-    "j": 11,
-  },
-  "$ROOT_QUERY.bar.foo": Object {
-    "_id": "barfoo",
-    "k": 12,
-    "l": 13,
-  },
-  "$ROOT_QUERY.foo": Object {
-    "bar": Object {
-      "generated": false,
-      "id": "bar:foobar",
-      "type": "id",
-      "typename": "bar",
-    },
-    "c": 3,
-    "d": 4,
-  },
-  "ROOT_QUERY": Object {
-    "a": 1,
-    "b": 2,
-    "bar": Object {
-      "generated": true,
-      "id": "$ROOT_QUERY.bar",
-      "type": "id",
-      "typename": undefined,
-    },
-    "foo": Object {
-      "generated": true,
-      "id": "$ROOT_QUERY.foo",
-      "type": "id",
-      "typename": undefined,
-    },
-    "g": 8,
-    "h": 9,
-  },
-  "bar:foobar": Object {
-    "e": 5,
-    "f": 6,
-    "id": "foobar",
-  },
-}
-`;
-
-exports[`ApolloClient write then read will not use a default id getter if id and _id are not present 1`] = `
-Object {
-  "$ROOT_QUERY.bar": Object {
-    "__typename": "bar",
-    "foo": Object {
-      "generated": true,
-      "id": "$ROOT_QUERY.bar.foo",
-      "type": "id",
-      "typename": "foo",
-    },
-    "i": 10,
-    "j": 11,
-  },
-  "$ROOT_QUERY.bar.foo": Object {
-    "__typename": "foo",
-    "k": 12,
-    "l": 13,
-  },
-  "$ROOT_QUERY.foo": Object {
-    "__typename": "foo",
-    "bar": Object {
-      "generated": true,
-      "id": "$ROOT_QUERY.foo.bar",
-      "type": "id",
-      "typename": "bar",
-    },
-    "c": 3,
-    "d": 4,
-  },
-  "$ROOT_QUERY.foo.bar": Object {
-    "__typename": "bar",
-    "e": 5,
-    "f": 6,
-  },
-  "ROOT_QUERY": Object {
-    "a": 1,
-    "b": 2,
-    "bar": Object {
-      "generated": true,
-      "id": "$ROOT_QUERY.bar",
-      "type": "id",
-      "typename": "bar",
-    },
-    "foo": Object {
-      "generated": true,
-      "id": "$ROOT_QUERY.foo",
-      "type": "id",
-      "typename": "foo",
-    },
-    "g": 8,
-    "h": 9,
-  },
-}
-`;
-
-exports[`ApolloClient write then read will use a default id getter if __typename and _id are present 1`] = `
-Object {
-  "$ROOT_QUERY.foo": Object {
-    "__typename": "foo",
-    "bar": Object {
-      "generated": false,
-      "id": "bar:foobar",
-      "type": "id",
-      "typename": "bar",
-    },
-    "c": 3,
-    "d": 4,
-  },
-  "ROOT_QUERY": Object {
-    "a": 1,
-    "b": 2,
-    "foo": Object {
-      "generated": true,
-      "id": "$ROOT_QUERY.foo",
-      "type": "id",
-      "typename": "foo",
-    },
-  },
-  "bar:foobar": Object {
-    "__typename": "bar",
-    "_id": "foobar",
-    "e": 5,
-    "f": 6,
-  },
-}
-`;
-
-exports[`ApolloClient write then read will use a default id getter if __typename and id are present 1`] = `
-Object {
-  "$ROOT_QUERY.foo": Object {
-    "__typename": "foo",
-    "bar": Object {
-      "generated": false,
-      "id": "bar:foobar",
-      "type": "id",
-      "typename": "bar",
-    },
-    "c": 3,
-    "d": 4,
-  },
-  "ROOT_QUERY": Object {
-    "a": 1,
-    "b": 2,
-    "foo": Object {
-      "generated": true,
-      "id": "$ROOT_QUERY.foo",
-      "type": "id",
-      "typename": "foo",
-    },
-  },
-  "bar:foobar": Object {
-    "__typename": "bar",
-    "e": 5,
-    "f": 6,
-    "id": "foobar",
-  },
-}
-`;
-
-exports[`ApolloClient write then read will use a default id getter if one is not specified and __typename is present along with either _id or id 1`] = `
-Object {
-  "$ROOT_QUERY.bar": Object {
-    "__typename": "bar",
-    "foo": Object {
-      "generated": false,
-      "id": "foo:barfoo",
-      "type": "id",
-      "typename": "foo",
-    },
-    "i": 10,
-    "j": 11,
-  },
-  "$ROOT_QUERY.foo": Object {
-    "__typename": "foo",
-    "bar": Object {
-      "generated": false,
-      "id": "bar:foobar",
-      "type": "id",
-      "typename": "bar",
-    },
-    "c": 3,
-    "d": 4,
-  },
-  "ROOT_QUERY": Object {
-    "a": 1,
-    "b": 2,
-    "bar": Object {
-      "generated": true,
-      "id": "$ROOT_QUERY.bar",
-      "type": "id",
-      "typename": "bar",
-    },
-    "foo": Object {
-      "generated": true,
-      "id": "$ROOT_QUERY.foo",
-      "type": "id",
-      "typename": "foo",
-    },
-    "g": 8,
-    "h": 9,
-  },
-  "bar:foobar": Object {
-    "__typename": "bar",
-    "e": 5,
-    "f": 6,
-    "id": "foobar",
-  },
-  "foo:barfoo": Object {
-    "__typename": "foo",
-    "_id": "barfoo",
-    "k": 12,
-    "l": 13,
-  },
-}
-`;
-
-exports[`ApolloClient write then read will write data locally which will then be read back 1`] = `
-Object {
-  "$foo.bar": Object {
-    "__typename": "Bar",
-    "d": 8,
-    "e": 9,
-    "f": 6,
-  },
-  "foo": Object {
-    "__typename": "Foo",
-    "a": 7,
-    "b": 2,
-    "bar": Object {
-      "generated": true,
-      "id": "$foo.bar",
-      "type": "id",
-      "typename": "Bar",
-    },
-    "c": 3,
-  },
-}
-`;
-
-exports[`ApolloClient write then read will write data to a specific id 1`] = `
-Object {
-  "$ROOT_QUERY.foo": Object {
-    "__typename": "foo",
-    "bar": Object {
-      "generated": false,
-      "id": "foobar",
-      "type": "id",
-      "typename": "bar",
-    },
-    "c": 3,
-    "d": 4,
-  },
-  "ROOT_QUERY": Object {
-    "a": 1,
-    "b": 2,
-    "foo": Object {
-      "generated": true,
-      "id": "$ROOT_QUERY.foo",
-      "type": "id",
-      "typename": "foo",
-    },
-  },
-  "foobar": Object {
-    "__typename": "bar",
-    "e": 5,
-    "f": 6,
-    "key": "foobar",
-  },
-}
-`;
-
-exports[`ApolloClient writeFragment will write some deeply nested data into the store at any id 1`] = `
-Object {
-  "bar": Object {
-    "__typename": "Bar",
-    "i": 7,
-  },
-  "foo": Object {
-    "__typename": "Foo",
-    "e": 4,
-    "h": Object {
-      "generated": false,
-      "id": "bar",
-      "type": "id",
-      "typename": "Bar",
-    },
-  },
-}
-`;
-
-exports[`ApolloClient writeFragment will write some deeply nested data into the store at any id 2`] = `
-Object {
-  "bar": Object {
-    "__typename": "Bar",
-    "i": 7,
-    "j": 8,
-    "k": 9,
-  },
-  "foo": Object {
-    "__typename": "Foo",
-    "e": 4,
-    "f": 5,
-    "g": 6,
-    "h": Object {
-      "generated": false,
-      "id": "bar",
-      "type": "id",
-      "typename": "Bar",
-    },
-  },
-}
-`;
-
-exports[`ApolloClient writeFragment will write some deeply nested data into the store at any id 3`] = `
-Object {
-  "bar": Object {
-    "__typename": "Bar",
-    "i": 10,
-    "j": 8,
-    "k": 9,
-  },
-  "foo": Object {
-    "__typename": "Foo",
-    "e": 4,
-    "f": 5,
-    "g": 6,
-    "h": Object {
-      "generated": false,
-      "id": "bar",
-      "type": "id",
-      "typename": "Bar",
-    },
-  },
-}
-`;
-
-exports[`ApolloClient writeFragment will write some deeply nested data into the store at any id 4`] = `
-Object {
-  "bar": Object {
-    "__typename": "Bar",
-    "i": 10,
-    "j": 11,
-    "k": 12,
-  },
-  "foo": Object {
-    "__typename": "Foo",
-    "e": 4,
-    "f": 5,
-    "g": 6,
-    "h": Object {
-      "generated": false,
-      "id": "bar",
-      "type": "id",
-      "typename": "Bar",
-    },
-  },
-}
-`;
-
-exports[`ApolloClient writeFragment will write some deeply nested data into the store at any id 5`] = `
-Object {
-  "bar": Object {
-    "__typename": "Bar",
-    "i": 7,
-    "j": 8,
-    "k": 9,
-  },
-  "foo": Object {
-    "__typename": "Foo",
-    "e": 4,
-    "f": 5,
-    "g": 6,
-    "h": Object {
-      "generated": false,
-      "id": "bar",
-      "type": "id",
-      "typename": "Bar",
-    },
-  },
-}
-`;
-
-exports[`ApolloClient writeFragment will write some deeply nested data into the store at any id 6`] = `
-Object {
-  "bar": Object {
-    "__typename": "Bar",
-    "i": 10,
-    "j": 11,
-    "k": 12,
-  },
-  "foo": Object {
-    "__typename": "Foo",
-    "e": 4,
-    "f": 5,
-    "g": 6,
-    "h": Object {
-      "generated": false,
-      "id": "bar",
-      "type": "id",
-      "typename": "Bar",
-    },
-  },
-}
-`;
-
-exports[`ApolloClient writeQuery will write some deeply nested data to the store 1`] = `
-Object {
-  "$ROOT_QUERY.d": Object {
-    "__typename": "D",
-    "e": 4,
-  },
-  "ROOT_QUERY": Object {
-    "a": 1,
-    "d": Object {
-      "generated": true,
-      "id": "$ROOT_QUERY.d",
-      "type": "id",
-      "typename": "D",
-    },
-  },
-}
-`;
-
-exports[`ApolloClient writeQuery will write some deeply nested data to the store 2`] = `
-Object {
-  "$ROOT_QUERY.d": Object {
-    "__typename": "D",
-    "e": 4,
-    "h": Object {
-      "generated": true,
-      "id": "$ROOT_QUERY.d.h",
-      "type": "id",
-      "typename": "H",
-    },
-  },
-  "$ROOT_QUERY.d.h": Object {
-    "__typename": "H",
-    "i": 7,
-  },
-  "ROOT_QUERY": Object {
-    "a": 1,
-    "d": Object {
-      "generated": true,
-      "id": "$ROOT_QUERY.d",
-      "type": "id",
-      "typename": "D",
-    },
-  },
-}
-`;
-
-exports[`ApolloClient writeQuery will write some deeply nested data to the store 3`] = `
-Object {
-  "$ROOT_QUERY.d": Object {
-    "__typename": "D",
-    "e": 4,
-    "f": 5,
-    "g": 6,
-    "h": Object {
-      "generated": true,
-      "id": "$ROOT_QUERY.d.h",
-      "type": "id",
-      "typename": "H",
-    },
-  },
-  "$ROOT_QUERY.d.h": Object {
-    "__typename": "H",
-    "i": 7,
-    "j": 8,
-    "k": 9,
-  },
-  "ROOT_QUERY": Object {
-    "a": 1,
-    "b": 2,
-    "c": 3,
-    "d": Object {
-      "generated": true,
-      "id": "$ROOT_QUERY.d",
-      "type": "id",
-      "typename": "D",
-    },
-  },
-}
-`;
diff --git a/packages/apollo-client/src/__tests__/__snapshots__/client.ts.snap b/packages/apollo-client/src/__tests__/__snapshots__/client.ts.snap
deleted file mode 100644
--- a/packages/apollo-client/src/__tests__/__snapshots__/client.ts.snap
+++ /dev/null
@@ -1,43 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[
-  `@connect should run a query with the connection directive and filter arguments and write the result to the correct store key 1`
-] = `
-Object {
-  "ROOT_QUERY": Object {
-    "abc({\\"order\\":\\"popularity\\"})": Array [
-      Object {
-        "generated": true,
-        "id": "ROOT_QUERY.abc({\\"order\\":\\"popularity\\"}).0",
-        "type": "id",
-        "typename": "Book",
-      },
-    ],
-  },
-  "ROOT_QUERY.abc({\\"order\\":\\"popularity\\"}).0": Object {
-    "__typename": "Book",
-    "name": "abcd",
-  },
-}
-`;
-
-exports[
-  `@connect should run a query with the connection directive and write the result to the store key defined in the directive 1`
-] = `
-Object {
-  "ROOT_QUERY": Object {
-    "abc": Array [
-      Object {
-        "generated": true,
-        "id": "ROOT_QUERY.abc.0",
-        "type": "id",
-        "typename": "Book",
-      },
-    ],
-  },
-  "ROOT_QUERY.abc.0": Object {
-    "__typename": "Book",
-    "name": "abcd",
-  },
-}
-`;
diff --git a/packages/apollo-client/tsconfig.test.json b/packages/apollo-client/tsconfig.test.json
deleted file mode 100644
--- a/packages/apollo-client/tsconfig.test.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
-  "extends": "./tsconfig",
-  "compilerOptions": {
-    "module": "commonjs",
-  },
-  "exclude": [
-    "src/__tests__/ApolloClient.ts",
-    "src/__tests__/client.ts",
-    "src/__tests__/fetchMore.ts",
-    "src/__tests__/graphqlSubscriptions.ts",
-    "src/__tests__/mutationResults.ts",
-    "src/__tests__/optimistic.ts",
-    "src/core/__tests__/ObservableQuery.ts",
-    "src/core/__tests__/QueryManager/index.ts",
-    "src/core/__tests__/QueryManager/links.ts",
-    "src/core/__tests__/QueryManager/live.ts",
-    "src/core/__tests__/QueryManager/multiple-results.ts",
-    "src/core/__tests__/QueryManager/recycler.ts",
-    "src/core/__tests__/fetchPolicies.ts",
-    "src/data/__tests__/queries.ts",
-    "src/errors/__tests__/ApolloError.ts",
-  ]
-}
diff --git a/packages/apollo-utilities/src/util/__tests__/isEqual.ts b/packages/apollo-utilities/src/util/__tests__/isEqual.ts
deleted file mode 100644
--- a/packages/apollo-utilities/src/util/__tests__/isEqual.ts
+++ /dev/null
@@ -1,174 +0,0 @@
-import { isEqual } from '../isEqual';
-
-describe('isEqual', () => {
-  it('should return true for equal primitive values', () => {
-    expect(isEqual(undefined, undefined)).toBe(true);
-    expect(isEqual(null, null)).toBe(true);
-    expect(isEqual(true, true)).toBe(true);
-    expect(isEqual(false, false)).toBe(true);
-    expect(isEqual(-1, -1)).toBe(true);
-    expect(isEqual(+1, +1)).toBe(true);
-    expect(isEqual(42, 42)).toBe(true);
-    expect(isEqual(0, 0)).toBe(true);
-    expect(isEqual(0.5, 0.5)).toBe(true);
-    expect(isEqual('hello', 'hello')).toBe(true);
-    expect(isEqual('world', 'world')).toBe(true);
-  });
-
-  it('should return false for not equal primitive values', () => {
-    expect(!isEqual(undefined, null)).toBe(true);
-    expect(!isEqual(null, undefined)).toBe(true);
-    expect(!isEqual(true, false)).toBe(true);
-    expect(!isEqual(false, true)).toBe(true);
-    expect(!isEqual(-1, +1)).toBe(true);
-    expect(!isEqual(+1, -1)).toBe(true);
-    expect(!isEqual(42, 42.00000000000001)).toBe(true);
-    expect(!isEqual(0, 0.5)).toBe(true);
-    expect(!isEqual('hello', 'world')).toBe(true);
-    expect(!isEqual('world', 'hello')).toBe(true);
-  });
-
-  it('should return false when comparing primitives with objects', () => {
-    expect(!isEqual({}, null)).toBe(true);
-    expect(!isEqual(null, {})).toBe(true);
-    expect(!isEqual({}, true)).toBe(true);
-    expect(!isEqual(true, {})).toBe(true);
-    expect(!isEqual({}, 42)).toBe(true);
-    expect(!isEqual(42, {})).toBe(true);
-    expect(!isEqual({}, 'hello')).toBe(true);
-    expect(!isEqual('hello', {})).toBe(true);
-  });
-
-  it('should correctly compare shallow objects', () => {
-    expect(isEqual({}, {})).toBe(true);
-    expect(isEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3 })).toBe(true);
-    expect(!isEqual({ a: 1, b: 2, c: 3 }, { a: 3, b: 2, c: 1 })).toBe(true);
-    expect(!isEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 })).toBe(true);
-    expect(!isEqual({ a: 1, b: 2 }, { a: 1, b: 2, c: 3 })).toBe(true);
-  });
-
-  it('should correctly compare deep objects', () => {
-    expect(isEqual({ x: {} }, { x: {} })).toBe(true);
-    expect(
-      isEqual({ x: { a: 1, b: 2, c: 3 } }, { x: { a: 1, b: 2, c: 3 } }),
-    ).toBe(true);
-    expect(
-      !isEqual({ x: { a: 1, b: 2, c: 3 } }, { x: { a: 3, b: 2, c: 1 } }),
-    ).toBe(true);
-    expect(!isEqual({ x: { a: 1, b: 2, c: 3 } }, { x: { a: 1, b: 2 } })).toBe(
-      true,
-    );
-    expect(!isEqual({ x: { a: 1, b: 2 } }, { x: { a: 1, b: 2, c: 3 } })).toBe(
-      true,
-    );
-  });
-
-  it('should correctly compare deep objects without object prototype ', () => {
-    // Solves https://github.com/apollographql/apollo-client/issues/2132
-    const objNoProto = Object.create(null);
-    objNoProto.a = { b: 2, c: [3, 4] };
-    objNoProto.e = Object.create(null);
-    objNoProto.e.f = 5;
-    expect(isEqual(objNoProto, { a: { b: 2, c: [3, 4] }, e: { f: 5 } })).toBe(
-      true,
-    );
-    expect(!isEqual(objNoProto, { a: { b: 2, c: [3, 4] }, e: { f: 6 } })).toBe(
-      true,
-    );
-    expect(!isEqual(objNoProto, { a: { b: 2, c: [3, 4] }, e: null })).toBe(
-      true,
-    );
-    expect(!isEqual(objNoProto, { a: { b: 2, c: [3] }, e: { f: 5 } })).toBe(
-      true,
-    );
-    expect(!isEqual(objNoProto, null)).toBe(true);
-  });
-
-  it('should correctly handle modified prototypes', () => {
-    Array.prototype.foo = null;
-    expect(isEqual([1, 2, 3], [1, 2, 3])).toBe(true);
-    expect(!isEqual([1, 2, 3], [1, 2, 4])).toBe(true);
-    delete Array.prototype.foo;
-  });
-
-  describe('comparing objects with circular refs', () => {
-    // copied with slight modification from lodash test suite
-    it('should compare objects with circular references', () => {
-      const object1 = {},
-        object2 = {};
-
-      object1.a = object1;
-      object2.a = object2;
-
-      expect(isEqual(object1, object2)).toBe(true);
-
-      object1.b = 0;
-      object2.b = Object(0);
-
-      expect(isEqual(object1, object2)).toBe(true);
-
-      object1.c = Object(1);
-      object2.c = Object(2);
-
-      expect(isEqual(object1, object2)).toBe(false);
-
-      object1 = { a: 1, b: 2, c: 3 };
-      object1.b = object1;
-      object2 = { a: 1, b: { a: 1, b: 2, c: 3 }, c: 3 };
-
-      expect(isEqual(object1, object2)).toBe(false);
-    });
-
-    it('should have transitive equivalence for circular references of objects', () => {
-      const object1 = {},
-        object2 = { a: object1 },
-        object3 = { a: object2 };
-
-      object1.a = object1;
-
-      expect(isEqual(object1, object2)).toBe(true);
-      expect(isEqual(object2, object3)).toBe(true);
-      expect(isEqual(object1, object3)).toBe(true);
-    });
-
-    it('should compare objects with multiple circular references', () => {
-      const array1 = [{}],
-        array2 = [{}];
-
-      (array1[0].a = array1).push(array1);
-      (array2[0].a = array2).push(array2);
-
-      expect(isEqual(array1, array2)).toBe(true);
-
-      array1[0].b = 0;
-      array2[0].b = Object(0);
-
-      expect(isEqual(array1, array2)).toBe(true);
-
-      array1[0].c = Object(1);
-      array2[0].c = Object(2);
-
-      expect(isEqual(array1, array2)).toBe(false);
-    });
-
-    it('should compare objects with complex circular references', () => {
-      const object1 = {
-        foo: { b: { c: { d: {} } } },
-        bar: { a: 2 },
-      };
-
-      const object2 = {
-        foo: { b: { c: { d: {} } } },
-        bar: { a: 2 },
-      };
-
-      object1.foo.b.c.d = object1;
-      object1.bar.b = object1.foo.b;
-
-      object2.foo.b.c.d = object2;
-      object2.bar.b = object2.foo.b;
-
-      expect(isEqual(object1, object2)).toBe(true);
-    });
-  });
-});
diff --git a/packages/apollo-utilities/src/util/__tests__/mergeDeep.ts b/packages/apollo-utilities/src/util/__tests__/mergeDeep.ts
deleted file mode 100644
--- a/packages/apollo-utilities/src/util/__tests__/mergeDeep.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-import { mergeDeep, mergeDeepArray } from '../mergeDeep';
-
-describe('mergeDeep', function() {
-  it('should return an object if first argument falsy', function() {
-    expect(mergeDeep()).toEqual({});
-    expect(mergeDeep(null)).toEqual({});
-    expect(mergeDeep(null, { foo: 42 })).toEqual({ foo: 42 });
-  });
-
-  it('should preserve identity for single arguments', function() {
-    const arg = Object.create(null);
-    expect(mergeDeep(arg)).toBe(arg);
-  });
-
-  it('should preserve identity when merging non-conflicting objects', function() {
-    const a = { a: { name: 'ay' } };
-    const b = { b: { name: 'bee' } };
-    const c = mergeDeep(a, b);
-    expect(c.a).toBe(a.a);
-    expect(c.b).toBe(b.b);
-    expect(c).toEqual({
-      a: { name: 'ay' },
-      b: { name: 'bee' },
-    });
-  });
-
-  it('should shallow-copy conflicting fields', function() {
-    const a = { conflict: { fromA: [1, 2, 3] } };
-    const b = { conflict: { fromB: [4, 5] } };
-    const c = mergeDeep(a, b);
-    expect(c.conflict).not.toBe(a.conflict);
-    expect(c.conflict).not.toBe(b.conflict);
-    expect(c.conflict.fromA).toBe(a.conflict.fromA);
-    expect(c.conflict.fromB).toBe(b.conflict.fromB);
-    expect(c).toEqual({
-      conflict: {
-        fromA: [1, 2, 3],
-        fromB: [4, 5],
-      },
-    });
-  });
-
-  it('should resolve conflicts among more than two objects', function() {
-    const sources = [];
-
-    for (let i = 0; i < 100; ++i) {
-      sources.push({
-        ['unique' + i]: { value: i },
-        conflict: {
-          ['from' + i]: { value: i },
-          nested: {
-            ['nested' + i]: { value: i },
-          },
-        },
-      });
-    }
-
-    const merged = mergeDeep(...sources);
-
-    sources.forEach((source, i) => {
-      expect(merged['unique' + i].value).toBe(i);
-      expect(source['unique' + i]).toBe(merged['unique' + i]);
-
-      expect(merged.conflict).not.toBe(source.conflict);
-      expect(merged.conflict['from' + i].value).toBe(i);
-      expect(merged.conflict['from' + i]).toBe(source.conflict['from' + i]);
-
-      expect(merged.conflict.nested).not.toBe(source.conflict.nested);
-      expect(merged.conflict.nested['nested' + i].value).toBe(i);
-      expect(merged.conflict.nested['nested' + i]).toBe(
-        source.conflict.nested['nested' + i],
-      );
-    });
-  });
-
-  it('can merge array elements', function() {
-    const a = [{ a: 1 }, { a: 'ay' }, 'a'];
-    const b = [{ b: 2 }, { b: 'bee' }, 'b'];
-    const c = [{ c: 3 }, { c: 'cee' }, 'c'];
-    const d = { 1: { d: 'dee' } };
-
-    expect(mergeDeep(a, b, c, d)).toEqual([
-      { a: 1, b: 2, c: 3 },
-      { a: 'ay', b: 'bee', c: 'cee', d: 'dee' },
-      'c',
-    ]);
-  });
-
-  it('lets the last conflicting value win', function() {
-    expect(mergeDeep('a', 'b', 'c')).toBe('c');
-
-    expect(
-      mergeDeep(
-        { a: 'a', conflict: 1 },
-        { b: 'b', conflict: 2 },
-        { c: 'c', conflict: 3 },
-      ),
-    ).toEqual({
-      a: 'a',
-      b: 'b',
-      c: 'c',
-      conflict: 3,
-    });
-
-    expect(mergeDeep(
-      ['a', ['b', 'c'], 'd'],
-      [/*empty*/, ['B'], 'D'],
-    )).toEqual(
-      ['a', ['B', 'c'], 'D'],
-    );
-
-    expect(mergeDeep(
-      ['a', ['b', 'c'], 'd'],
-      ['A', [/*empty*/, 'C']],
-    )).toEqual(
-      ['A', ['b', 'C'], 'd'],
-    );
-  });
-
-  it('mergeDeep returns the intersection of its argument types', function() {
-    const abc = mergeDeep({ str: "hi", a: 1 }, { a: 3, b: 2 }, { b: 1, c: 2 });
-    // The point of this test is that the following lines type-check without
-    // resorting to any `any` loopholes:
-    expect(abc.str.slice(0)).toBe("hi");
-    expect(abc.a * 2).toBe(6);
-    expect(abc.b - 0).toBe(1);
-    expect(abc.c / 2).toBe(1);
-  });
-
-  it('mergeDeepArray returns the supertype of its argument types', function() {
-    class F {
-      check() { return "ok" };
-    }
-    const fs: F[] = [new F, new F, new F];
-    // Although mergeDeepArray doesn't have the same tuple type awareness as
-    // mergeDeep, it does infer that F should be the return type here:
-    expect(mergeDeepArray(fs).check()).toBe("ok");
-  });
-});
diff --git a/packages/apollo-utilities/src/util/__tests__/warnOnce.ts b/packages/apollo-utilities/src/util/__tests__/warnOnce.ts
deleted file mode 100644
--- a/packages/apollo-utilities/src/util/__tests__/warnOnce.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { warnOnceInDevelopment } from '../warnOnce';
-
-let lastWarning: string | null;
-let keepEnv: string | undefined;
-let numCalls = 0;
-let oldConsoleWarn: any;
-
-describe('warnOnce', () => {
-  beforeEach(() => {
-    keepEnv = process.env.NODE_ENV;
-    numCalls = 0;
-    lastWarning = null;
-    oldConsoleWarn = console.warn;
-    console.warn = (msg: any) => {
-      numCalls++;
-      lastWarning = msg;
-    };
-  });
-  afterEach(() => {
-    process.env.NODE_ENV = keepEnv;
-    console.warn = oldConsoleWarn;
-  });
-  it('actually warns', () => {
-    process.env.NODE_ENV = 'development';
-    warnOnceInDevelopment('hi');
-    expect(lastWarning).toBe('hi');
-    expect(numCalls).toEqual(1);
-  });
-
-  it('does not warn twice', () => {
-    process.env.NODE_ENV = 'development';
-    warnOnceInDevelopment('ho');
-    warnOnceInDevelopment('ho');
-    expect(lastWarning).toEqual('ho');
-    expect(numCalls).toEqual(1);
-  });
-
-  it('warns two different things once each', () => {
-    process.env.NODE_ENV = 'development';
-    warnOnceInDevelopment('slow');
-    expect(lastWarning).toEqual('slow');
-    warnOnceInDevelopment('mo');
-    expect(lastWarning).toEqual('mo');
-    expect(numCalls).toEqual(2);
-  });
-
-  it('does not warn in production', () => {
-    process.env.NODE_ENV = 'production';
-    warnOnceInDevelopment('lo');
-    warnOnceInDevelopment('lo');
-    expect(numCalls).toEqual(0);
-  });
-
-  it('warns many times in test', () => {
-    process.env.NODE_ENV = 'test';
-    warnOnceInDevelopment('yo');
-    warnOnceInDevelopment('yo');
-    expect(lastWarning).toEqual('yo');
-    expect(numCalls).toEqual(2);
-  });
-});
diff --git a/packages/graphql-anywhere/src/__tests__/__snapshots__/utilities.ts.snap b/packages/graphql-anywhere/src/__tests__/__snapshots__/utilities.ts.snap
deleted file mode 100644
--- a/packages/graphql-anywhere/src/__tests__/__snapshots__/utilities.ts.snap
+++ /dev/null
@@ -1,13 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`utilities with a single query can check propTypes for fragments 1`] = `
-Array [
-  "Warning: Failed prop type: alias missing on {}",
-]
-`;
-
-exports[`utilities with a single query can check propTypes for fragments with variables 1`] = `
-Array [
-  "Warning: Failed prop type: height missing on {\\"alias\\":\\"Bob\\",\\"avatar\\":{\\"square\\":\\"abc\\"}}",
-]
-`;
diff --git a/packages/graphql-anywhere/src/__tests__/directives.ts b/packages/graphql-anywhere/src/__tests__/directives.ts
deleted file mode 100644
--- a/packages/graphql-anywhere/src/__tests__/directives.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import gql from 'graphql-tag';
-
-import graphql from '../';
-
-describe('directives', () => {
-  it('skips a field that has the skip directive', () => {
-    const resolver = () => {
-      throw new Error('should not be called');
-    };
-
-    const query = gql`
-      {
-        a @skip(if: true)
-      }
-    `;
-
-    const result = graphql(resolver, query);
-
-    expect(result).toEqual({});
-  });
-
-  it('includes info about arbitrary directives', () => {
-    const resolver = (fieldName, root, args, context, info) => {
-      const { doSomethingDifferent } = info.directives;
-      let data = root[info.resultKey];
-      if (doSomethingDifferent) {
-        if (doSomethingDifferent.but === 'notTooCrazy') {
-          return data;
-        }
-        return undefined;
-      }
-      return data;
-    };
-
-    const input = {
-      a: 'something',
-      b: 'hidden',
-    };
-
-    const query = gql`
-      {
-        a @doSomethingDifferent(but: notTooCrazy)
-        b @doSomethingDifferent(but: nope)
-      }
-    `;
-
-    const result = graphql(resolver, query, input);
-
-    expect(result).toEqual({ a: 'something' });
-  });
-});
diff --git a/packages/graphql-anywhere/src/__tests__/index.ts b/packages/graphql-anywhere/src/__tests__/index.ts
deleted file mode 100644
--- a/packages/graphql-anywhere/src/__tests__/index.ts
+++ /dev/null
@@ -1,854 +0,0 @@
-import gql from 'graphql-tag';
-
-import { Resolver, ExecInfo } from '..';
-
-const execute = (graphql, r) => () => {
-  it('does basic things', async () => {
-    const resolver = (_, root) => r(root + 'fake');
-
-    const query = gql`
-      {
-        a {
-          b
-          ...frag
-        }
-      }
-
-      fragment frag on X {
-        c
-      }
-    `;
-
-    const result = await graphql(resolver, query, '', null, null);
-
-    expect(result).toEqual({
-      a: {
-        b: 'fakefake',
-
-        c: 'fakefake',
-      },
-    });
-  });
-
-  it('works with enum args', async () => {
-    const resolver = (fieldName, root, args) => r(args.value);
-
-    const query = gql`
-      {
-        a(value: ENUM_VALUE)
-      }
-    `;
-
-    const result = await graphql(resolver, query);
-
-    expect(result).toEqual({ a: 'ENUM_VALUE' });
-  });
-
-  it('works with null args', async () => {
-    const resolver = (fieldName, root, args) => r(args.value);
-
-    const query = gql`
-      {
-        a(value: null)
-      }
-    `;
-
-    const result = await graphql(resolver, query);
-
-    expect(result).toEqual({ a: null });
-  });
-
-  it('traverses arrays returned from the resolver', async () => {
-    const resolver = () => r([1, 2]);
-
-    const query = gql`
-      {
-        a {
-          b
-        }
-      }
-    `;
-
-    const result = await graphql(resolver, query);
-
-    expect(result).toEqual({ a: [{ b: [1, 2] }, { b: [1, 2] }] });
-  });
-
-  it('can traverse an object', async () => {
-    const obj = {
-      a: {
-        b: 'fun',
-
-        c: ['also fun', 'also fun 2'],
-
-        d: 'not fun',
-      },
-    };
-
-    const resolver = (fieldName, root) => r(root[fieldName]);
-
-    const query = gql`
-      {
-        a {
-          b
-          c
-        }
-      }
-    `;
-
-    const result = await graphql(resolver, query, obj, null, null);
-
-    expect(result).toEqual({
-      a: {
-        b: 'fun',
-
-        c: ['also fun', 'also fun 2'],
-      },
-    });
-  });
-
-  it('can traverse nested arrays', async () => {
-    const obj = { a: [{ b: [[{ c: 1 }, { c: 2 }], [{ c: 3 }, { c: 4 }]] }] };
-
-    const resolver = (fieldName, root) => r(root[fieldName]);
-
-    const query = gql`
-      {
-        a {
-          b {
-            c
-          }
-        }
-      }
-    `;
-
-    const result = await graphql(resolver, query, obj, null, null);
-
-    expect(result).toEqual({
-      a: [{ b: [[{ c: 1 }, { c: 2 }], [{ c: 3 }, { c: 4 }]] }],
-    });
-  });
-
-  it('can use arguments, both inline and variables', async () => {
-    const resolver = (fieldName, _, args) => r(args);
-
-    const query = gql`
-      {
-        inline(int: 5, float: 3.14, string: "string")
-        variables(int: $int, float: $float, string: $string)
-      }
-    `;
-
-    const variables = {
-      int: 6,
-
-      float: 6.28,
-
-      string: 'varString',
-    };
-
-    const result = await graphql(resolver, query, null, null, variables);
-
-    expect(result).toEqual({
-      inline: {
-        int: 5,
-
-        float: 3.14,
-
-        string: 'string',
-      },
-
-      variables: {
-        int: 6,
-
-        float: 6.28,
-
-        string: 'varString',
-      },
-    });
-  });
-
-  it('will tolerate missing variables', async () => {
-    const resolver = (fieldName, _, args) => r(args);
-
-    const query = gql`
-      {
-        variables(int: $int, float: $float, string: $string, missing: $missing)
-      }
-    `;
-
-    const variables = {
-      int: 6,
-
-      float: 6.28,
-
-      string: 'varString',
-    };
-
-    const result = await graphql(resolver, query, null, null, variables);
-
-    expect(result).toEqual({
-      variables: {
-        int: 6,
-
-        float: 6.28,
-
-        string: 'varString',
-
-        missing: undefined,
-      },
-    });
-  });
-
-  it('can use skip and include', async () => {
-    const resolver = fieldName => r(fieldName);
-
-    const query = gql`
-      {
-        a {
-          b @skip(if: true)
-          c @include(if: true)
-          d @skip(if: false)
-          e @include(if: false)
-        }
-      }
-    `;
-
-    const result = await graphql(resolver, query, null, null);
-
-    expect(result).toEqual({
-      a: {
-        c: 'c',
-
-        d: 'd',
-      },
-    });
-  });
-
-  it('can use inline and named fragments', async () => {
-    const resolver = fieldName => r(fieldName);
-
-    const query = gql`
-      {
-        a {
-          ... on Type {
-            b
-            c
-          }
-          ...deFrag
-        }
-      }
-
-      fragment deFrag on Type {
-        d
-        e
-      }
-    `;
-
-    const result = await graphql(resolver, query, null, null, null);
-
-    expect(result).toEqual({
-      a: {
-        b: 'b',
-
-        c: 'c',
-
-        d: 'd',
-
-        e: 'e',
-      },
-    });
-  });
-
-  it('can resolve deeply nested fragments', async () => {
-    const resolver = (fieldName, root) => {
-      return r(root[fieldName]);
-    };
-
-    const query = gql`
-      {
-        stringField
-        numberField
-        nullField
-        ... on Item {
-          nestedObj {
-            stringField
-            nullField
-            deepNestedObj {
-              stringField
-              nullField
-            }
-          }
-        }
-        ... on Item {
-          nestedObj {
-            numberField
-            nullField
-            deepNestedObj {
-              numberField
-              nullField
-            }
-          }
-        }
-        ... on Item {
-          nullObject
-        }
-        nestedObj {
-          inlinedObjectStringField
-        }
-      }
-    `;
-
-    const result: any = {
-      id: 'abcd',
-
-      stringField: 'This is a string!',
-
-      numberField: 5,
-
-      nullField: null,
-
-      nestedObj: {
-        id: 'abcde',
-
-        stringField: 'This is a string too!',
-
-        numberField: 6,
-
-        nullField: null,
-
-        deepNestedObj: {
-          stringField: 'This is a deep string',
-
-          numberField: 7,
-
-          nullField: null,
-        },
-
-        inlinedObjectStringField: 'This is a string of an inlined object',
-      },
-
-      nullObject: null,
-    };
-
-    const queryResult = await graphql(resolver, query, result);
-
-    // The result of the query shouldn't contain __data_id fields
-
-    expect(queryResult).toEqual({
-      stringField: 'This is a string!',
-
-      numberField: 5,
-
-      nullField: null,
-
-      nestedObj: {
-        stringField: 'This is a string too!',
-
-        numberField: 6,
-
-        nullField: null,
-
-        deepNestedObj: {
-          stringField: 'This is a deep string',
-
-          numberField: 7,
-
-          nullField: null,
-        },
-
-        inlinedObjectStringField: 'This is a string of an inlined object',
-      },
-
-      nullObject: null,
-    });
-  });
-
-  it('can resolve deeply nested fragments with arrays', async () => {
-    const resolver = (fieldName, root) => {
-      return r(root[fieldName]);
-    };
-
-    const query = gql`
-      {
-        ... on Item {
-          array {
-            id
-            field1
-          }
-        }
-        ... on Item {
-          array {
-            id
-            field2
-          }
-        }
-        ... on Item {
-          array {
-            id
-            field3
-          }
-        }
-      }
-    `;
-
-    const result: any = {
-      array: [
-        {
-          id: 'abcde',
-
-          field1: 1,
-
-          field2: 2,
-
-          field3: 3,
-        },
-      ],
-    };
-
-    const queryResult = await graphql(resolver, query, result);
-
-    expect(queryResult).toEqual({
-      array: [
-        {
-          id: 'abcde',
-
-          field1: 1,
-
-          field2: 2,
-
-          field3: 3,
-        },
-      ],
-    });
-  });
-
-  it('passes info including isLeaf, resultKey, directives, and field', async () => {
-    const leafMap: { [s: string]: ExecInfo } = {};
-
-    const resolver: Resolver = (fieldName, root, args, context, info) => {
-      leafMap[fieldName] = info;
-
-      return r('continue');
-    };
-
-    const query = gql`
-      {
-        alias: a {
-          b
-          hasDirective @skip(if: false) @otherDirective(arg: $x)
-        }
-      }
-    `;
-
-    await graphql(resolver, query, null, null, { x: 'argument' });
-
-    expect(leafMap).toEqual({
-      a: {
-        directives: null,
-
-        isLeaf: false,
-
-        resultKey: 'alias',
-
-        field: expect.any(Object),
-      },
-
-      b: {
-        directives: null,
-
-        isLeaf: true,
-
-        resultKey: 'b',
-
-        field: expect.any(Object),
-      },
-
-      hasDirective: {
-        directives: {
-          skip: { if: false },
-
-          otherDirective: { arg: 'argument' },
-        },
-
-        isLeaf: true,
-
-        resultKey: 'hasDirective',
-
-        field: expect.any(Object),
-      },
-    });
-  });
-
-  it('can filter GraphQL results', async () => {
-    const data = {
-      alias: 'Bob',
-
-      name: 'Wrong',
-
-      height: 1.89,
-
-      avatar: {
-        square: 'abc',
-
-        circle: 'def',
-
-        triangle: 'qwe',
-      },
-    };
-
-    const fragment = gql`
-      fragment PersonDetails on Person {
-        alias: name
-        height(unit: METERS)
-        avatar {
-          square
-          ... on Avatar {
-            circle
-          }
-        }
-      }
-    `;
-
-    const resolver: Resolver = (fieldName, root, args, context, info) => {
-      return r(root[info.resultKey]);
-    };
-
-    const filtered = await graphql(resolver, fragment, data);
-
-    expect(filtered).toEqual({
-      alias: 'Bob',
-
-      height: 1.89,
-
-      avatar: {
-        square: 'abc',
-
-        circle: 'def',
-      },
-    });
-  });
-
-  it('can handle mutations', async () => {
-    const resolver = (fieldName, root, args) => {
-      let value;
-
-      if (fieldName === 'operateOnNumbers') {
-        value = args;
-      } else if (fieldName === 'add') {
-        value = root.a + root.b;
-      } else if (fieldName === 'subtract') {
-        value = root.a - root.b;
-      } else if (fieldName === 'multiply') {
-        value = root.a * root.b;
-      } else if (fieldName === 'divide') {
-        value = root.a / root.b;
-      }
-
-      return r(value);
-    };
-
-    const query = gql`
-      mutation {
-        operateOnNumbers(a: 10, b: 2) {
-          add
-          subtract
-          multiply
-          divide
-        }
-      }
-    `;
-
-    const result = await graphql(resolver, query, '', null, null);
-
-    expect(result).toEqual({
-      operateOnNumbers: {
-        add: 12,
-
-        subtract: 8,
-
-        multiply: 20,
-
-        divide: 5,
-      },
-    });
-  });
-
-  it('does not error on subscriptions', async () => {
-    const data = {
-      user: {
-        id: 1,
-
-        name: 'Some User',
-
-        height: 1.89,
-      },
-    };
-
-    const resolver = (fieldName, root) => r(root[fieldName]);
-
-    const query = gql`
-      subscription {
-        user {
-          id
-          name
-          height
-        }
-      }
-    `;
-
-    const result = await graphql(resolver, query, data);
-
-    expect(result).toEqual({
-      user: {
-        id: 1,
-
-        name: 'Some User',
-
-        height: 1.89,
-      },
-    });
-  });
-
-  it('can handle documents with multiple fragments', async () => {
-    const data = {
-      user: {
-        id: 1,
-
-        name: 'Some User',
-
-        height: 1.89,
-      },
-    };
-
-    const resolver = (fieldName, root) => r(root[fieldName]);
-
-    const query = gql`
-      fragment A on User {
-        name
-      }
-
-      fragment B on User {
-        height
-      }
-
-      query {
-        user {
-          id
-          ...A
-          ...B
-        }
-      }
-    `;
-
-    const result = await graphql(resolver, query, data);
-
-    expect(result).toEqual({
-      user: {
-        id: 1,
-
-        name: 'Some User',
-
-        height: 1.89,
-      },
-    });
-  });
-
-  describe('examples', () => {
-    it('readme example', async () => {
-      // I don't need all this stuff!
-
-      const gitHubAPIResponse = {
-        url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347',
-
-        title: 'Found a bug',
-
-        body: "I'm having a problem with this.",
-
-        user: {
-          login: 'octocat',
-
-          avatar_url: 'https://github.com/images/error/octocat_happy.gif',
-
-          url: 'https://api.github.com/users/octocat',
-        },
-
-        labels: [
-          {
-            url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug',
-
-            name: 'bug',
-
-            color: 'f29513',
-          },
-        ],
-      };
-
-      // Write a query that gets just the fields we want
-
-      const query = gql`
-        {
-          title
-          user {
-            login
-          }
-          labels {
-            name
-          }
-        }
-      `;
-
-      // Define a resolver that just returns a property
-
-      const resolver = (fieldName, root) => root[fieldName];
-
-      // Filter the data!
-
-      const result = await graphql(resolver, query, gitHubAPIResponse);
-
-      expect(result).toEqual({
-        title: 'Found a bug',
-
-        user: { login: 'octocat' },
-
-        labels: [{ name: 'bug' }],
-      });
-    });
-
-    it('readme example 2', async () => {
-      // Write a query where the fields are types, but we alias them
-
-      const query = gql`
-        {
-          author {
-            name: string
-            age: int
-            address {
-              state: string
-            }
-          }
-        }
-      `;
-
-      // Define a resolver that uses the field name to determine the type
-
-      // Note that we get the actual name, not the alias, but the alias
-
-      // is used to determine the location in the response
-
-      const resolver = fieldName =>
-        ({ string: 'This is a string', int: 5 }[fieldName] || 'continue');
-
-      // Generate the object!
-
-      const result = await graphql(resolver, query);
-
-      expect(result).toEqual({
-        author: {
-          name: 'This is a string',
-
-          age: 5,
-
-          address: { state: 'This is a string' },
-        },
-      });
-    });
-
-    it('read from Redux normalized store', async () => {
-      const data = {
-        result: [1, 2],
-
-        entities: {
-          articles: {
-            1: { id: 1, title: 'Some Article', author: 1 },
-
-            2: { id: 2, title: 'Other Article', author: 1 },
-          },
-
-          users: { 1: { id: 1, name: 'Dan' } },
-        },
-      };
-
-      const query = gql`
-        {
-          result {
-            title
-            author {
-              name
-            }
-          }
-        }
-      `;
-
-      const schema = { articles: { author: 'users' } };
-
-      // This resolver is a bit more complex than others, since it has to
-
-      // correctly handle the root object, values by ID, and scalar leafs.
-
-      const resolver = (fieldName, rootValue, args, context): any => {
-        if (!rootValue) {
-          return context.result.map(id => {
-            return {
-              ...context.entities.articles[id],
-
-              __typename: 'articles',
-            };
-          });
-        }
-
-        const typename = rootValue.__typename;
-
-        // If this field is a reference according to the schema
-
-        if (typename && schema[typename] && schema[typename][fieldName]) {
-          // Get the target type, and get it from entities by ID
-
-          const targetType: string = schema[typename][fieldName];
-
-          return {
-            ...context.entities[targetType][rootValue[fieldName]],
-
-            __typename: targetType,
-          };
-        }
-
-        // This field is just a scalar
-
-        return rootValue[fieldName];
-      };
-
-      const result = await graphql(
-        resolver,
-        query,
-        null,
-        data, // pass data as context since we have to access it all the time
-      );
-
-      // This is the non-normalized data, with only the fields we asked for in our query!
-
-      expect(result).toEqual({
-        result: [
-          {
-            title: 'Some Article',
-
-            author: { name: 'Dan' },
-          },
-
-          {
-            title: 'Other Article',
-
-            author: { name: 'Dan' },
-          },
-        ],
-      });
-    });
-  });
-};
-
-describe('basic operations done sync', execute(require('../').default, x => x));
-
-describe(
-  'basic operations done async',
-  execute(require('../async').graphql, x => Promise.resolve(x)),
-);
diff --git a/packages/graphql-anywhere/src/__tests__/mapper.ts b/packages/graphql-anywhere/src/__tests__/mapper.ts
deleted file mode 100644
--- a/packages/graphql-anywhere/src/__tests__/mapper.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import graphql from '../';
-import gql from 'graphql-tag';
-
-import { cloneElement, createElement } from 'react';
-import { renderToStaticMarkup } from 'react-dom/server';
-
-describe('result mapper', () => {
-  it('can deal with promises', () => {
-    const resolver = (_, root) => {
-      return new Promise(res => {
-        setTimeout(() => {
-          Promise.resolve(root).then(val => res(val + 'fake'));
-        }, 10);
-      });
-    };
-
-    function promiseForObject(object): Promise<{ [key: string]: any }> {
-      const keys = Object.keys(object);
-      const valuesAndPromises = keys.map(name => object[name]);
-
-      return Promise.all(valuesAndPromises).then(values =>
-        values.reduce((resolvedObject, value, i) => {
-          resolvedObject[keys[i]] = value;
-          return resolvedObject;
-        }, Object.create(null)),
-      );
-    }
-
-    const query = gql`
-      {
-        a {
-          b
-          c
-        }
-      }
-    `;
-
-    const result = graphql(resolver, query, '', null, null, {
-      resultMapper: promiseForObject,
-    });
-
-    return result.then(value => {
-      expect(value).toEqual({
-        a: {
-          b: 'fakefake',
-          c: 'fakefake',
-        },
-      });
-    });
-  });
-
-  it('can construct React elements', () => {
-    const resolver = (fieldName, root, args) => {
-      if (fieldName === 'text') {
-        return args.value;
-      }
-
-      return createElement(fieldName, args);
-    };
-
-    const reactMapper = (childObj, root) => {
-      const reactChildren = Object.keys(childObj).map(key => childObj[key]);
-
-      if (root) {
-        return cloneElement(root, root.props, ...reactChildren);
-      }
-
-      return reactChildren[0];
-    };
-
-    function gqlToReact(document): any {
-      return graphql(resolver, document, '', null, null, {
-        resultMapper: reactMapper,
-      });
-    }
-
-    const query = gql`
-      {
-        div {
-          s1: span(id: "my-id") {
-            text(value: "This is text")
-          }
-          s2: span
-        }
-      }
-    `;
-
-    expect(renderToStaticMarkup(gqlToReact(query))).toBe(
-      '<div><span id="my-id">This is text</span><span></span></div>',
-    );
-  });
-});
diff --git a/packages/graphql-anywhere/src/__tests__/matcher.ts b/packages/graphql-anywhere/src/__tests__/matcher.ts
deleted file mode 100644
--- a/packages/graphql-anywhere/src/__tests__/matcher.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import gql from 'graphql-tag';
-
-import graphql, { FragmentMatcher } from '../';
-
-describe('fragment matcher', () => {
-  it('does basic things', () => {
-    const resolver = fieldName => fieldName;
-
-    const query = gql`
-      {
-        a {
-          b
-          ...yesFrag
-          ...noFrag
-          ... on Yes {
-            e
-          }
-          ... on No {
-            f
-          }
-        }
-      }
-
-      fragment yesFrag on Yes {
-        c
-      }
-
-      fragment noFrag on No {
-        d
-      }
-    `;
-
-    const fragmentMatcher: FragmentMatcher = (_, typeCondition) =>
-      typeCondition === 'Yes';
-
-    const resultWithMatcher = graphql(resolver, query, '', null, null, {
-      fragmentMatcher,
-    });
-
-    expect(resultWithMatcher).toEqual({
-      a: {
-        b: 'b',
-        c: 'c',
-        e: 'e',
-      },
-    });
-
-    const resultNoMatcher = graphql(resolver, query, '', null, null);
-
-    expect(resultNoMatcher).toEqual({
-      a: {
-        b: 'b',
-        c: 'c',
-        d: 'd',
-        e: 'e',
-        f: 'f',
-      },
-    });
-  });
-});
diff --git a/packages/graphql-anywhere/src/__tests__/utilities.ts b/packages/graphql-anywhere/src/__tests__/utilities.ts
deleted file mode 100644
--- a/packages/graphql-anywhere/src/__tests__/utilities.ts
+++ /dev/null
@@ -1,561 +0,0 @@
-import gql, { disableFragmentWarnings } from 'graphql-tag';
-import { checkPropTypes } from 'prop-types';
-
-// Turn off warnings for repeated fragment names
-disableFragmentWarnings();
-
-import { filter, check, propType } from '../utilities';
-
-describe('utilities', () => {
-  describe('with a single query', () => {
-    const doc = gql`
-      {
-        alias: name
-        height(unit: METERS)
-        avatar {
-          square
-        }
-      }
-    `;
-    const fragment = gql`
-      fragment foo on Foo {
-        alias: name
-        height(unit: METERS)
-        avatar {
-          square
-        }
-      }
-    `;
-    const fragmentWithAVariable = gql`
-      fragment foo on Foo {
-        alias: name
-        height(unit: METERS)
-        avatar @include(if: $foo) {
-          square
-        }
-      }
-    `;
-    const data = {
-      alias: 'Bob',
-      name: 'Wrong',
-      height: 1.89,
-      avatar: {
-        square: 'abc',
-        circle: 'def',
-        triangle: 'qwe',
-      },
-    };
-    const filteredData = {
-      alias: 'Bob',
-      height: 1.89,
-      avatar: {
-        square: 'abc',
-      },
-    };
-    const arrayData = [
-      {
-        alias: 'Bob',
-        name: 'Wrong',
-        height: 1.89,
-        avatar: {
-          square: 'abc',
-          circle: 'def',
-          triangle: 'qwe',
-        },
-      },
-      {
-        alias: 'Tom',
-        name: 'Right',
-        height: 1.77,
-        avatar: {
-          square: 'jkl',
-          circle: 'bnm',
-          triangle: 'uio',
-        },
-      },
-    ];
-    const filteredArrayData = [
-      {
-        alias: 'Bob',
-        height: 1.89,
-        avatar: {
-          square: 'abc',
-        },
-      },
-      {
-        alias: 'Tom',
-        height: 1.77,
-        avatar: {
-          square: 'jkl',
-        },
-      },
-    ];
-
-    beforeEach(() => {
-      checkPropTypes.resetWarningCache();
-      jest.spyOn(global.console, 'error').mockImplementation(() => {});
-    });
-
-    afterEach(() => {
-      global.console.error.mockRestore();
-    });
-
-    it('can filter data', () => {
-      expect(filter(doc, data)).toEqual(filteredData);
-    });
-
-    it('can filter an array of data', () => {
-      expect(filter(doc, arrayData)).toEqual(filteredArrayData);
-    });
-
-    it('can short circuit when data is null', () => {
-      expect(filter(doc, null)).toEqual(null);
-    });
-
-    it('can filter data for fragments', () => {
-      expect(filter(fragment, data)).toEqual(filteredData);
-    });
-
-    it('can filter data for fragments with variables', () => {
-      expect(filter(fragmentWithAVariable, data, { foo: true })).toEqual(
-        filteredData,
-      );
-    });
-
-    it('can generate propTypes for fragments', () => {
-      expect(propType(fragment)).toEqual(expect.any(Function));
-    });
-
-    it('can check propTypes for fragments', () => {
-      const propTypes = {
-        foo: propType(fragment),
-      };
-      checkPropTypes(propTypes, filteredData, 'prop', 'MyComponent');
-      expect(global.console.error).not.toHaveBeenCalled();
-      checkPropTypes(propTypes, { foo: {} }, 'prop', 'MyComponent');
-      expect(global.console.error.mock.calls[0]).toMatchSnapshot();
-    });
-
-    it('can generate propTypes for fragments with variables', () => {
-      expect(propType(fragmentWithAVariable)).toEqual(expect.any(Function));
-    });
-
-    it('can check propTypes for fragments with variables', () => {
-      const mapPropsToVariables = () => null;
-      const propTypes = {
-        foo: propType(fragmentWithAVariable, mapPropsToVariables),
-      };
-      checkPropTypes(propTypes, { foo: filteredData }, 'prop', 'MyComponent');
-      expect(global.console.error).not.toHaveBeenCalled();
-      const badProps = { foo: { ...filteredData } };
-      delete badProps.foo.height;
-      checkPropTypes(propTypes, badProps, 'prop', 'MyComponent');
-      expect(global.console.error).toHaveBeenCalled();
-      expect(global.console.error.mock.calls[0]).toMatchSnapshot();
-    });
-
-    it('makes variable inclusion props optional, when no variables are passed', () => {
-      const propTypes = {
-        foo: propType(fragmentWithAVariable),
-      };
-      const propsWithoutAvatar = { foo: { ...filteredData } };
-      delete propsWithoutAvatar.foo.avatar;
-      checkPropTypes(propTypes, propsWithoutAvatar, 'prop', 'MyComponent');
-      expect(global.console.error).not.toHaveBeenCalled();
-    });
-
-    it('can check matching data', () => {
-      check(doc, filteredData);
-    });
-
-    it('can check matching data for fragments with variables', () => {
-      check(doc, filteredData, { foo: true });
-    });
-
-    it('throws when checking non-matching data for fragments with variables', () => {
-      const badFilteredData = { ...filteredData };
-      delete badFilteredData.avatar;
-      expect(() => {
-        check(doc, badFilteredData);
-      }).toThrow();
-    });
-
-    // This doesn't throw but potentially it should?
-    it('can check overspecified data', () => {
-      check(doc, data);
-    });
-
-    it('throws when checking underspecified data', () => {
-      expect(() => {
-        check(doc, {
-          name: 'Wrong',
-        });
-      }).toThrow();
-
-      expect(() => {
-        check(doc, {
-          alias: 'Bob',
-          height: 1.89,
-        });
-      }).toThrow();
-    });
-  });
-
-  describe('with a single fragment', () => {
-    const doc = gql`
-      fragment PersonDetails on Person {
-        alias: name
-        height(unit: METERS)
-        avatar {
-          square
-        }
-      }
-    `;
-    const data = {
-      alias: 'Bob',
-      name: 'Wrong',
-      height: 1.89,
-      avatar: {
-        square: 'abc',
-        circle: 'def',
-        triangle: 'qwe',
-      },
-    };
-    const filteredData = {
-      alias: 'Bob',
-      height: 1.89,
-      avatar: {
-        square: 'abc',
-      },
-    };
-    const arrayData = [
-      {
-        alias: 'Bob',
-        name: 'Wrong',
-        height: 1.89,
-        avatar: {
-          square: 'abc',
-          circle: 'def',
-          triangle: 'qwe',
-        },
-      },
-      {
-        alias: 'Tom',
-        name: 'Right',
-        height: 1.77,
-        avatar: {
-          square: 'jkl',
-          circle: 'bnm',
-          triangle: 'uio',
-        },
-      },
-    ];
-    const filteredArrayData = [
-      {
-        alias: 'Bob',
-        height: 1.89,
-        avatar: {
-          square: 'abc',
-        },
-      },
-      {
-        alias: 'Tom',
-        height: 1.77,
-        avatar: {
-          square: 'jkl',
-        },
-      },
-    ];
-
-    it('can filter data', () => {
-      expect(filter(doc, data)).toEqual(filteredData);
-    });
-
-    it('can filter an array of data', () => {
-      expect(filter(doc, arrayData)).toEqual(filteredArrayData);
-    });
-
-    it('can check matching data', () => {
-      check(doc, filteredData);
-    });
-
-    // This doesn't throw but potentially it should?
-    it('can check overspecified data', () => {
-      check(doc, data);
-    });
-
-    it('throws when checking underspecified data', () => {
-      expect(() => {
-        check(doc, {
-          name: 'Wrong',
-        });
-      }).toThrow();
-
-      expect(() => {
-        check(doc, {
-          alias: 'Bob',
-          height: 1.89,
-        });
-      }).toThrow();
-    });
-  });
-
-  describe('with a single fragment', () => {
-    const doc = gql`
-      fragment PersonDetails on Person {
-        alias: name
-        height(unit: METERS)
-        avatar {
-          square
-        }
-      }
-    `;
-    const data = {
-      alias: 'Bob',
-      name: 'Wrong',
-      height: 1.89,
-      avatar: {
-        square: 'abc',
-        circle: 'def',
-        triangle: 'qwe',
-      },
-    };
-    const filteredData = {
-      alias: 'Bob',
-      height: 1.89,
-      avatar: {
-        square: 'abc',
-      },
-    };
-    const arrayData = [
-      {
-        alias: 'Bob',
-        name: 'Wrong',
-        height: 1.89,
-        avatar: {
-          square: 'abc',
-          circle: 'def',
-          triangle: 'qwe',
-        },
-      },
-      {
-        alias: 'Tom',
-        name: 'Right',
-        height: 1.77,
-        avatar: {
-          square: 'jkl',
-          circle: 'bnm',
-          triangle: 'uio',
-        },
-      },
-    ];
-    const filteredArrayData = [
-      {
-        alias: 'Bob',
-        height: 1.89,
-        avatar: {
-          square: 'abc',
-        },
-      },
-      {
-        alias: 'Tom',
-        height: 1.77,
-        avatar: {
-          square: 'jkl',
-        },
-      },
-    ];
-
-    it('can filter data', () => {
-      expect(filter(doc, data)).toEqual(filteredData);
-    });
-
-    it('can filter an array of data', () => {
-      expect(filter(doc, arrayData)).toEqual(filteredArrayData);
-    });
-
-    it('can check matching data', () => {
-      check(doc, filteredData);
-    });
-
-    // This doesn't throw but potentially it should?
-    it('can check overspecified data', () => {
-      check(doc, data);
-    });
-
-    it('throws when checking underspecified data', () => {
-      expect(() => {
-        check(doc, {
-          name: 'Wrong',
-        });
-      }).toThrow();
-
-      expect(() => {
-        check(doc, {
-          alias: 'Bob',
-          height: 1.89,
-        });
-      }).toThrow();
-    });
-  });
-
-  describe('with nested fragments', () => {
-    const doc = gql`
-      fragment PersonDetails on Person {
-        alias: name
-        height(unit: METERS)
-        avatar {
-          square
-          ... on Avatar {
-            circle
-          }
-        }
-      }
-    `;
-    const data = {
-      alias: 'Bob',
-      name: 'Wrong',
-      height: 1.89,
-      avatar: {
-        square: 'abc',
-        circle: 'def',
-        triangle: 'qwe',
-      },
-    };
-    const filteredData = {
-      alias: 'Bob',
-      height: 1.89,
-      avatar: {
-        square: 'abc',
-        circle: 'def',
-      },
-    };
-    const arrayData = [
-      {
-        alias: 'Bob',
-        name: 'Wrong',
-        height: 1.89,
-        avatar: {
-          square: 'abc',
-          circle: 'def',
-          triangle: 'qwe',
-        },
-      },
-      {
-        alias: 'Tom',
-        name: 'Right',
-        height: 1.77,
-        avatar: {
-          square: 'jkl',
-          circle: 'bnm',
-          triangle: 'uio',
-        },
-      },
-    ];
-    const filteredArrayData = [
-      {
-        alias: 'Bob',
-        height: 1.89,
-        avatar: {
-          square: 'abc',
-          circle: 'def',
-        },
-      },
-      {
-        alias: 'Tom',
-        height: 1.77,
-        avatar: {
-          square: 'jkl',
-          circle: 'bnm',
-        },
-      },
-    ];
-
-    it('can filter data', () => {
-      expect(filter(doc, data)).toEqual(filteredData);
-    });
-
-    it('can filter an array of data', () => {
-      expect(filter(doc, arrayData)).toEqual(filteredArrayData);
-    });
-
-    it('can check matching data', () => {
-      check(doc, filteredData);
-    });
-
-    // This doesn't throw but potentially it should?
-    it('can check overspecified data', () => {
-      check(doc, data);
-    });
-
-    it('throws when checking underspecified data', () => {
-      expect(() => {
-        check(doc, {
-          name: 'Wrong',
-        });
-      }).toThrow();
-
-      expect(() => {
-        check(doc, [
-          {
-            name: 'Wrong',
-          },
-        ]);
-      }).toThrow();
-
-      expect(() => {
-        check(doc, {
-          alias: 'Bob',
-          height: 1.89,
-        });
-      }).toThrow();
-
-      expect(() => {
-        check(doc, {
-          alias: 'Bob',
-          height: 1.89,
-          avatar: {
-            // missing the correct field
-            triangle: 'qwe',
-          },
-        });
-      }).toThrow();
-    });
-
-    describe('if the nested fragment has not matched', () => {
-      it('can filter data', () => {
-        const filtered = filter(doc, {
-          alias: 'Bob',
-          name: 'Wrong',
-          height: 1.89,
-          avatar: {
-            square: 'abc',
-            // there is no circle field here, but we can't know if that's not
-            // because avatar is not an Avatar
-            triangle: 'qwe',
-          },
-        });
-
-        expect(filtered).toEqual({
-          alias: 'Bob',
-          height: 1.89,
-          avatar: {
-            square: 'abc',
-          },
-        });
-      });
-
-      it('does not throw when checking', () => {
-        check(doc, {
-          alias: 'Wrong',
-          height: 1.89,
-          avatar: {
-            square: 'abc',
-            // there is no circle field here, but we can't know if that's not
-            // because avatar is not an Avatar
-          },
-        });
-      });
-    });
-  });
-});
diff --git a/packages/apollo-client/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts
similarity index 94%
rename from packages/apollo-client/src/__tests__/ApolloClient.ts
rename to src/__tests__/ApolloClient.ts
--- a/packages/apollo-client/src/__tests__/ApolloClient.ts
+++ b/src/__tests__/ApolloClient.ts
@@ -1,16 +1,30 @@
 import gql from 'graphql-tag';
-import { ApolloLink, Observable } from 'apollo-link';
-import { InMemoryCache } from 'apollo-cache-inmemory';
-import { stripSymbols } from 'apollo-utilities';
-import { withWarning } from '../util/wrap';
-import ApolloClient from '../';
+
+import { Observable } from '../utilities/observables/Observable';
+import { makeReference } from '../utilities/graphql/storeUtils';
+import { ApolloLink } from '../link/core/ApolloLink';
+import { HttpLink } from '../link/http/HttpLink';
+import { InMemoryCache } from '../cache/inmemory/inMemoryCache';
+import { stripSymbols } from '../utilities/testing/stripSymbols';
+import { withWarning } from '../utilities/testing/wrap';
+import { ApolloClient } from '../';
 import { DefaultOptions } from '../ApolloClient';
 import { FetchPolicy, QueryOptions } from '../core/watchQueryOptions';
-import { DataProxy } from 'apollo-cache';
 
 describe('ApolloClient', () => {
   describe('constructor', () => {
-    it('will throw an error if link is not passed in', () => {
+    let oldFetch: any;
+
+    beforeEach(() => {
+      oldFetch = window.fetch;
+      window.fetch = () => null;
+    })
+
+    afterEach(() => {
+      window.fetch = oldFetch;
+    });
+
+    it('will throw an error if `uri` or `link` is not passed in', () => {
       expect(() => {
         new ApolloClient({ cache: new InMemoryCache() } as any);
       }).toThrowErrorMatchingSnapshot();
@@ -21,6 +35,28 @@ describe('ApolloClient', () => {
         new ApolloClient({ link: ApolloLink.empty() } as any);
       }).toThrowErrorMatchingSnapshot();
     });
+
+    it('should create an `HttpLink` instance if `uri` is provided', () => {
+      const uri = 'http://localhost:4000';
+      const client = new ApolloClient({
+        cache: new InMemoryCache(),
+        uri,
+      });
+
+      expect(client.link).toBeDefined();
+      expect((client.link as HttpLink).options.uri).toEqual(uri);
+    });
+
+    it('should accept `link` over `uri` if both are provided', () => {
+      const uri1 = 'http://localhost:3000';
+      const uri2 = 'http://localhost:4000';
+      const client = new ApolloClient({
+        cache: new InMemoryCache(),
+        uri: uri1,
+        link: new HttpLink({ uri: uri2 })
+      });
+      expect((client.link as HttpLink).options.uri).toEqual(uri2);
+    });
   });
 
   describe('readQuery', () => {
@@ -82,22 +118,14 @@ describe('ApolloClient', () => {
             a: 1,
             b: 2,
             c: 3,
-            d: {
-              type: 'id',
-              id: 'foo',
-              generated: false,
-            },
+            d: makeReference('foo'),
           },
           foo: {
             __typename: 'Foo',
             e: 4,
             f: 5,
             g: 6,
-            h: {
-              type: 'id',
-              id: 'bar',
-              generated: false,
-            },
+            h: makeReference('bar'),
           },
           bar: {
             __typename: 'Bar',
@@ -339,22 +367,14 @@ describe('ApolloClient', () => {
             a: 1,
             b: 2,
             c: 3,
-            d: {
-              type: 'id',
-              id: 'foo',
-              generated: false,
-            },
+            d: makeReference('foo'),
           },
           foo: {
             __typename: 'Foo',
             e: 4,
             f: 5,
             g: 6,
-            h: {
-              type: 'id',
-              id: 'bar',
-              generated: false,
-            },
+            h: makeReference('bar'),
           },
           bar: {
             __typename: 'Bar',
@@ -599,6 +619,7 @@ describe('ApolloClient', () => {
 
       expect((client.cache as InMemoryCache).extract()).toEqual({
         ROOT_QUERY: {
+          __typename: "Query",
           a: 1,
         },
       });
@@ -615,6 +636,7 @@ describe('ApolloClient', () => {
 
       expect((client.cache as InMemoryCache).extract()).toEqual({
         ROOT_QUERY: {
+          __typename: "Query",
           a: 1,
           b: 2,
           c: 3,
@@ -634,6 +656,7 @@ describe('ApolloClient', () => {
 
       expect((client.cache as InMemoryCache).extract()).toEqual({
         ROOT_QUERY: {
+          __typename: "Query",
           a: 4,
           b: 5,
           c: 6,
@@ -742,6 +765,7 @@ describe('ApolloClient', () => {
 
       expect((client.cache as InMemoryCache).extract()).toEqual({
         ROOT_QUERY: {
+          __typename: "Query",
           'field({"literal":true,"value":42})': 1,
           'field({"literal":false,"value":42})': 2,
         },
@@ -785,6 +809,7 @@ describe('ApolloClient', () => {
 
       expect((client.cache as InMemoryCache).extract()).toEqual({
         ROOT_QUERY: {
+          __typename: "Query",
           'field({"literal":true,"value":42})': 2,
           'field({"literal":false,"value":-1})': 1,
         },
@@ -794,7 +819,10 @@ describe('ApolloClient', () => {
     it('should warn when the data provided does not match the query shape', () => {
       const client = new ApolloClient({
         link: ApolloLink.empty(),
-        cache: new InMemoryCache(),
+        cache: new InMemoryCache({
+          // Passing an empty map enables the warning:
+          possibleTypes: {},
+        }),
       });
 
       return withWarning(() => {
@@ -1073,7 +1101,10 @@ describe('ApolloClient', () => {
     it('should warn when the data provided does not match the fragment shape', () => {
       const client = new ApolloClient({
         link: ApolloLink.empty(),
-        cache: new InMemoryCache(),
+        cache: new InMemoryCache({
+          // Passing an empty map enables the warning:
+          possibleTypes: {},
+        }),
       });
 
       return withWarning(() => {
@@ -1160,7 +1191,7 @@ describe('ApolloClient', () => {
               ++count;
               if (count === 1) {
                 expect(stripSymbols(nextResult.data)).toEqual(data);
-                expect(stripSymbols(observable.currentResult().data)).toEqual(
+                expect(stripSymbols(observable.getCurrentResult().data)).toEqual(
                   data,
                 );
 
@@ -1212,7 +1243,7 @@ describe('ApolloClient', () => {
               count++;
               if (count === 1) {
                 expect(stripSymbols(nextResult.data)).toEqual(data);
-                expect(stripSymbols(observable.currentResult().data)).toEqual(
+                expect(stripSymbols(observable.getCurrentResult().data)).toEqual(
                   data,
                 );
 
@@ -1284,7 +1315,7 @@ describe('ApolloClient', () => {
               count++;
               if (count === 1) {
                 expect(stripSymbols(result.data)).toEqual(data);
-                expect(stripSymbols(observable.currentResult().data)).toEqual(
+                expect(stripSymbols(observable.getCurrentResult().data)).toEqual(
                   data,
                 );
                 const bestFriends = result.data.people.friends.filter(
@@ -1335,12 +1366,10 @@ describe('ApolloClient', () => {
               count++;
               if (count === 1) {
                 expect(stripSymbols(result.data)).toEqual(data);
-                expect(stripSymbols(observable.currentResult().data)).toEqual(
+                expect(stripSymbols(observable.getCurrentResult().data)).toEqual(
                   data,
                 );
                 const friends = result.data.people.friends;
-                friends[0].type = 'okayest';
-                friends[1].type = 'okayest';
 
                 // this should re call next
                 client.writeFragment({
@@ -1349,11 +1378,15 @@ describe('ApolloClient', () => {
                     fragment bestFriends on Person {
                       friends {
                         id
+                        type
                       }
                     }
                   `,
                   data: {
-                    friends,
+                    friends: [
+                      { ...friends[0], type: 'okayest' },
+                      { ...friends[1], type: 'okayest' },
+                    ],
                     __typename: 'Person',
                   },
                 });
@@ -1521,6 +1554,7 @@ describe('ApolloClient', () => {
         query,
         data: {
           obj: {
+            __typename: 'Obj',
             field: {
               field2: 1,
               __typename: 'Field',
@@ -1531,7 +1565,7 @@ describe('ApolloClient', () => {
       });
 
       client.writeData({
-        id: '$ROOT_QUERY.obj',
+        id: 'Obj:uniqueId',
         data: {
           field: {
             field2: 2,
@@ -1544,7 +1578,7 @@ describe('ApolloClient', () => {
         .query({ query })
         .then(({ data }: any) => {
           console.warn = originalWarn;
-          expect(data.obj.__typename).toEqual('__ClientData');
+          expect(data.obj.__typename).toEqual('Obj');
           expect(data.obj.field.__typename).toEqual('Field');
         })
         .catch(e => console.log(e));
@@ -1555,19 +1589,21 @@ describe('ApolloClient', () => {
     it('will write data locally which will then be read back', () => {
       const client = new ApolloClient({
         link: ApolloLink.empty(),
-        cache: new InMemoryCache().restore({
+        cache: new InMemoryCache({
+          dataIdFromObject(object) {
+            if (typeof object.__typename === 'string') {
+              return object.__typename.toLowerCase();
+            }
+          },
+        }).restore({
           foo: {
             __typename: 'Foo',
             a: 1,
             b: 2,
             c: 3,
-            bar: {
-              type: 'id',
-              id: '$foo.bar',
-              generated: true,
-            },
+            bar: makeReference('bar'),
           },
-          '$foo.bar': {
+          bar: {
             __typename: 'Bar',
             d: 4,
             e: 5,
@@ -1677,7 +1713,7 @@ describe('ApolloClient', () => {
       });
 
       client.writeFragment({
-        id: '$foo.bar',
+        id: 'bar',
         fragment: gql`
           fragment y on Bar {
             e
@@ -1841,49 +1877,30 @@ describe('ApolloClient', () => {
 
       expect((client.cache as InMemoryCache).extract()).toEqual({
         ROOT_QUERY: {
+          __typename: "Query",
           a: 1,
           b: 2,
           g: 8,
           h: 9,
           bar: {
-            type: 'id',
-            id: '$ROOT_QUERY.bar',
-            generated: true,
+            i: 10,
+            j: 11,
+            foo: {
+              _id: 'barfoo',
+              k: 12,
+              l: 13,
+            },
           },
           foo: {
-            type: 'id',
-            id: '$ROOT_QUERY.foo',
-            generated: true,
-          },
-        },
-        '$ROOT_QUERY.foo': {
-          c: 3,
-          d: 4,
-          bar: {
-            type: 'id',
-            id: '$ROOT_QUERY.foo.bar',
-            generated: true,
-          },
-        },
-        '$ROOT_QUERY.bar': {
-          i: 10,
-          j: 11,
-          foo: {
-            type: 'id',
-            id: '$ROOT_QUERY.bar.foo',
-            generated: true,
+            c: 3,
+            d: 4,
+            bar: {
+              id: 'foobar',
+              e: 5,
+              f: 6,
+            },
           },
         },
-        '$ROOT_QUERY.foo.bar': {
-          id: 'foobar',
-          e: 5,
-          f: 6,
-        },
-        '$ROOT_QUERY.bar.foo': {
-          _id: 'barfoo',
-          k: 12,
-          l: 13,
-        },
       });
     });
 
@@ -2057,28 +2074,19 @@ describe('ApolloClient', () => {
 
       expect((client.cache as InMemoryCache).extract()).toEqual({
         ROOT_QUERY: {
+          __typename: "Query",
           a: 1,
           b: 2,
           foo: {
-            type: 'id',
-            id: '$ROOT_QUERY.foo',
-            generated: true,
-          },
-        },
-        '$ROOT_QUERY.foo': {
-          c: 3,
-          d: 4,
-          bar: {
-            type: 'id',
-            id: '$ROOT_QUERY.foo.bar',
-            generated: true,
+            c: 3,
+            d: 4,
+            bar: {
+              id: 'foobar',
+              e: 5,
+              f: 6,
+            },
           },
         },
-        '$ROOT_QUERY.foo.bar': {
-          id: 'foobar',
-          e: 5,
-          f: 6,
-        },
       });
     });
 
@@ -2115,28 +2123,19 @@ describe('ApolloClient', () => {
 
       expect((client.cache as InMemoryCache).extract()).toEqual({
         ROOT_QUERY: {
+          __typename: "Query",
           a: 1,
           b: 2,
           foo: {
-            type: 'id',
-            id: '$ROOT_QUERY.foo',
-            generated: true,
-          },
-        },
-        '$ROOT_QUERY.foo': {
-          c: 3,
-          d: 4,
-          bar: {
-            type: 'id',
-            id: '$ROOT_QUERY.foo.bar',
-            generated: true,
+            c: 3,
+            d: 4,
+            bar: {
+              _id: 'foobar',
+              e: 5,
+              f: 6,
+            },
           },
         },
-        '$ROOT_QUERY.foo.bar': {
-          _id: 'foobar',
-          e: 5,
-          f: 6,
-        },
       });
     });
 
@@ -2374,8 +2373,6 @@ describe('ApolloClient', () => {
         defaultOptions,
       });
 
-      client.initQueryManager();
-
       let queryOptions: QueryOptions = {
         query: gql`
           {
@@ -2429,6 +2426,7 @@ describe('ApolloClient', () => {
 
       expect((client.cache as any).data.data).toEqual({
         ROOT_QUERY: {
+          __typename: "Query",
           a: 1,
         },
       });
diff --git a/src/__tests__/__snapshots__/ApolloClient.ts.snap b/src/__tests__/__snapshots__/ApolloClient.ts.snap
new file mode 100644
--- /dev/null
+++ b/src/__tests__/__snapshots__/ApolloClient.ts.snap
@@ -0,0 +1,377 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ApolloClient constructor will throw an error if \`uri\` or \`link\` is not passed in 1`] = `
+"To initialize Apollo Client, you must specify 'uri' or 'link' and 'cache' properties in the options object. 
+For more information, please visit: https://www.apollographql.com/docs/react/"
+`;
+
+exports[`ApolloClient constructor will throw an error if cache is not passed in 1`] = `
+"To initialize Apollo Client, you must specify 'uri' or 'link' and 'cache' properties in the options object. 
+For more information, please visit: https://www.apollographql.com/docs/react/"
+`;
+
+exports[`ApolloClient write then read will not use a default id getter if either _id or id is present when __typename is not also present 1`] = `
+Object {
+  "ROOT_QUERY": Object {
+    "__typename": "Query",
+    "a": 1,
+    "b": 2,
+    "bar": Object {
+      "foo": Object {
+        "_id": "barfoo",
+        "k": 12,
+        "l": 13,
+      },
+      "i": 10,
+      "j": 11,
+    },
+    "foo": Object {
+      "bar": Object {
+        "__ref": "bar:foobar",
+      },
+      "c": 3,
+      "d": 4,
+    },
+    "g": 8,
+    "h": 9,
+  },
+  "bar:foobar": Object {
+    "__typename": "bar",
+    "e": 5,
+    "f": 6,
+    "id": "foobar",
+  },
+}
+`;
+
+exports[`ApolloClient write then read will not use a default id getter if id and _id are not present 1`] = `
+Object {
+  "ROOT_QUERY": Object {
+    "__typename": "Query",
+    "a": 1,
+    "b": 2,
+    "bar": Object {
+      "__typename": "bar",
+      "foo": Object {
+        "__typename": "foo",
+        "k": 12,
+        "l": 13,
+      },
+      "i": 10,
+      "j": 11,
+    },
+    "foo": Object {
+      "__typename": "foo",
+      "bar": Object {
+        "__typename": "bar",
+        "e": 5,
+        "f": 6,
+      },
+      "c": 3,
+      "d": 4,
+    },
+    "g": 8,
+    "h": 9,
+  },
+}
+`;
+
+exports[`ApolloClient write then read will use a default id getter if __typename and _id are present 1`] = `
+Object {
+  "ROOT_QUERY": Object {
+    "__typename": "Query",
+    "a": 1,
+    "b": 2,
+    "foo": Object {
+      "__typename": "foo",
+      "bar": Object {
+        "__ref": "bar:foobar",
+      },
+      "c": 3,
+      "d": 4,
+    },
+  },
+  "bar:foobar": Object {
+    "__typename": "bar",
+    "_id": "foobar",
+    "e": 5,
+    "f": 6,
+  },
+}
+`;
+
+exports[`ApolloClient write then read will use a default id getter if __typename and id are present 1`] = `
+Object {
+  "ROOT_QUERY": Object {
+    "__typename": "Query",
+    "a": 1,
+    "b": 2,
+    "foo": Object {
+      "__typename": "foo",
+      "bar": Object {
+        "__ref": "bar:foobar",
+      },
+      "c": 3,
+      "d": 4,
+    },
+  },
+  "bar:foobar": Object {
+    "__typename": "bar",
+    "e": 5,
+    "f": 6,
+    "id": "foobar",
+  },
+}
+`;
+
+exports[`ApolloClient write then read will use a default id getter if one is not specified and __typename is present along with either _id or id 1`] = `
+Object {
+  "ROOT_QUERY": Object {
+    "__typename": "Query",
+    "a": 1,
+    "b": 2,
+    "bar": Object {
+      "__typename": "bar",
+      "foo": Object {
+        "__ref": "foo:barfoo",
+      },
+      "i": 10,
+      "j": 11,
+    },
+    "foo": Object {
+      "__typename": "foo",
+      "bar": Object {
+        "__ref": "bar:foobar",
+      },
+      "c": 3,
+      "d": 4,
+    },
+    "g": 8,
+    "h": 9,
+  },
+  "bar:foobar": Object {
+    "__typename": "bar",
+    "e": 5,
+    "f": 6,
+    "id": "foobar",
+  },
+  "foo:barfoo": Object {
+    "__typename": "foo",
+    "_id": "barfoo",
+    "k": 12,
+    "l": 13,
+  },
+}
+`;
+
+exports[`ApolloClient write then read will write data locally which will then be read back 1`] = `
+Object {
+  "bar": Object {
+    "__typename": "Bar",
+    "d": 8,
+    "e": 9,
+    "f": 6,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "a": 7,
+    "b": 2,
+    "bar": Object {
+      "__ref": "bar",
+    },
+    "c": 3,
+  },
+}
+`;
+
+exports[`ApolloClient write then read will write data to a specific id 1`] = `
+Object {
+  "ROOT_QUERY": Object {
+    "__typename": "Query",
+    "a": 1,
+    "b": 2,
+    "foo": Object {
+      "__typename": "foo",
+      "bar": Object {
+        "__ref": "foobar",
+      },
+      "c": 3,
+      "d": 4,
+    },
+  },
+  "foobar": Object {
+    "__typename": "bar",
+    "e": 5,
+    "f": 6,
+    "key": "foobar",
+  },
+}
+`;
+
+exports[`ApolloClient writeFragment will write some deeply nested data into the store at any id 1`] = `
+Object {
+  "bar": Object {
+    "__typename": "Bar",
+    "i": 7,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "e": 4,
+    "h": Object {
+      "__ref": "bar",
+    },
+  },
+}
+`;
+
+exports[`ApolloClient writeFragment will write some deeply nested data into the store at any id 2`] = `
+Object {
+  "bar": Object {
+    "__typename": "Bar",
+    "i": 7,
+    "j": 8,
+    "k": 9,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "e": 4,
+    "f": 5,
+    "g": 6,
+    "h": Object {
+      "__ref": "bar",
+    },
+  },
+}
+`;
+
+exports[`ApolloClient writeFragment will write some deeply nested data into the store at any id 3`] = `
+Object {
+  "bar": Object {
+    "__typename": "Bar",
+    "i": 10,
+    "j": 8,
+    "k": 9,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "e": 4,
+    "f": 5,
+    "g": 6,
+    "h": Object {
+      "__ref": "bar",
+    },
+  },
+}
+`;
+
+exports[`ApolloClient writeFragment will write some deeply nested data into the store at any id 4`] = `
+Object {
+  "bar": Object {
+    "__typename": "Bar",
+    "i": 10,
+    "j": 11,
+    "k": 12,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "e": 4,
+    "f": 5,
+    "g": 6,
+    "h": Object {
+      "__ref": "bar",
+    },
+  },
+}
+`;
+
+exports[`ApolloClient writeFragment will write some deeply nested data into the store at any id 5`] = `
+Object {
+  "bar": Object {
+    "__typename": "Bar",
+    "i": 7,
+    "j": 8,
+    "k": 9,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "e": 4,
+    "f": 5,
+    "g": 6,
+    "h": Object {
+      "__ref": "bar",
+    },
+  },
+}
+`;
+
+exports[`ApolloClient writeFragment will write some deeply nested data into the store at any id 6`] = `
+Object {
+  "bar": Object {
+    "__typename": "Bar",
+    "i": 10,
+    "j": 11,
+    "k": 12,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "e": 4,
+    "f": 5,
+    "g": 6,
+    "h": Object {
+      "__ref": "bar",
+    },
+  },
+}
+`;
+
+exports[`ApolloClient writeQuery will write some deeply nested data to the store 1`] = `
+Object {
+  "ROOT_QUERY": Object {
+    "__typename": "Query",
+    "a": 1,
+    "d": Object {
+      "__typename": "D",
+      "e": 4,
+    },
+  },
+}
+`;
+
+exports[`ApolloClient writeQuery will write some deeply nested data to the store 2`] = `
+Object {
+  "ROOT_QUERY": Object {
+    "__typename": "Query",
+    "a": 1,
+    "d": Object {
+      "__typename": "D",
+      "h": Object {
+        "__typename": "H",
+        "i": 7,
+      },
+    },
+  },
+}
+`;
+
+exports[`ApolloClient writeQuery will write some deeply nested data to the store 3`] = `
+Object {
+  "ROOT_QUERY": Object {
+    "__typename": "Query",
+    "a": 1,
+    "b": 2,
+    "c": 3,
+    "d": Object {
+      "__typename": "D",
+      "e": 4,
+      "f": 5,
+      "g": 6,
+      "h": Object {
+        "__typename": "H",
+        "i": 7,
+        "j": 8,
+        "k": 9,
+      },
+    },
+  },
+}
+`;
diff --git a/src/__tests__/__snapshots__/client.ts.snap b/src/__tests__/__snapshots__/client.ts.snap
new file mode 100644
--- /dev/null
+++ b/src/__tests__/__snapshots__/client.ts.snap
@@ -0,0 +1,43 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`@connection should run a query with the @connection directive and write the result to the store key defined in the directive 1`] = `
+Object {
+  "ROOT_QUERY": Object {
+    "__typename": "Query",
+    "books:abc": Array [
+      Object {
+        "__typename": "Book",
+        "name": "abcd",
+      },
+    ],
+  },
+}
+`;
+
+exports[`@connection should run a query with the connection directive and filter arguments and write the result to the correct store key 1`] = `
+Object {
+  "ROOT_QUERY": Object {
+    "__typename": "Query",
+    "books:abc({\\"order\\":\\"popularity\\"})": Array [
+      Object {
+        "__typename": "Book",
+        "name": "abcd",
+      },
+    ],
+  },
+}
+`;
+
+exports[`@connection should support cache field policies that filter key arguments 1`] = `
+Object {
+  "ROOT_QUERY": Object {
+    "__typename": "Query",
+    "books:{\\"order\\":\\"popularity\\"}": Array [
+      Object {
+        "__typename": "Book",
+        "name": "abcd",
+      },
+    ],
+  },
+}
+`;
diff --git a/packages/apollo-client/src/__tests__/__snapshots__/graphqlSubscriptions.ts.snap b/src/__tests__/__snapshots__/graphqlSubscriptions.ts.snap
similarity index 100%
rename from packages/apollo-client/src/__tests__/__snapshots__/graphqlSubscriptions.ts.snap
rename to src/__tests__/__snapshots__/graphqlSubscriptions.ts.snap
diff --git a/packages/apollo-client/src/__tests__/client.ts b/src/__tests__/client.ts
similarity index 79%
rename from packages/apollo-client/src/__tests__/client.ts
rename to src/__tests__/client.ts
--- a/packages/apollo-client/src/__tests__/client.ts
+++ b/src/__tests__/client.ts
@@ -1,30 +1,24 @@
 import { cloneDeep, assign } from 'lodash';
 import { GraphQLError, ExecutionResult, DocumentNode } from 'graphql';
 import gql from 'graphql-tag';
-import { ApolloLink, Observable } from 'apollo-link';
-import {
-  InMemoryCache,
-  IntrospectionFragmentMatcher,
-  FragmentMatcherInterface,
-} from 'apollo-cache-inmemory';
-import { stripSymbols } from 'apollo-utilities';
-
-import { QueryManager } from '../core/QueryManager';
-import { WatchQueryOptions, FetchPolicy } from '../core/watchQueryOptions';
 
+import { Observable } from '../utilities/observables/Observable';
+import { ApolloLink } from '../link/core/ApolloLink';
+import { InMemoryCache } from '../cache/inmemory/inMemoryCache';
+import { PossibleTypesMap } from '../cache/inmemory/types';
+import { stripSymbols } from '../utilities/testing/stripSymbols';
+import { WatchQueryOptions, FetchPolicy } from '../core/watchQueryOptions';
 import { ApolloError } from '../errors/ApolloError';
-
-import ApolloClient, { printAST } from '..';
-
-import subscribeAndCount from '../util/subscribeAndCount';
-import { withWarning } from '../util/wrap';
-
-import { mockSingleLink } from '../__mocks__/mockLinks';
+import { ApolloClient } from '..';
+import subscribeAndCount from '../utilities/testing/subscribeAndCount';
+import { withWarning } from '../utilities/testing/wrap';
+import { itAsync } from '../utilities/testing/itAsync';
+import { mockSingleLink } from '../utilities/testing/mocking/mockLink';
 
 describe('client', () => {
   it('can be loaded via require', () => {
     /* tslint:disable */
-    const ApolloClientRequire = require('../').default;
+    const ApolloClientRequire = require('../').ApolloClient;
     /* tslint:enable */
 
     const client = new ApolloClientRequire({
@@ -85,7 +79,7 @@ describe('client', () => {
     );
   });
 
-  it('should allow for a single query to take place', () => {
+  itAsync('should allow for a single query to take place', (resolve, reject) => {
     const query = gql`
       query people {
         allPeople(first: 1) {
@@ -110,10 +104,10 @@ describe('client', () => {
       },
     };
 
-    return clientRoundtrip(query, { data });
+    return clientRoundtrip(resolve, reject, query, { data });
   });
 
-  it('should allow a single query with an apollo-link enabled network interface', done => {
+  itAsync('should allow a single query with an apollo-link enabled network interface', (resolve, reject) => {
     const query = gql`
       query people {
         allPeople(first: 1) {
@@ -149,11 +143,11 @@ describe('client', () => {
 
     client.query({ query, variables }).then(actualResult => {
       expect(stripSymbols(actualResult.data)).toEqual(data);
-      done();
+      resolve();
     });
   });
 
-  it('should allow for a single query with complex default variables to take place', () => {
+  itAsync('should allow for a single query with complex default variables to take place', (resolve, reject) => {
     const query = gql`
       query stuff(
         $test: Input = { key1: ["value", "value2"], key2: { key3: 4 } }
@@ -186,7 +180,7 @@ describe('client', () => {
     const link = mockSingleLink({
       request: { query, variables },
       result: { data: result },
-    });
+    }).setOnError(reject);
 
     const client = new ApolloClient({
       link,
@@ -201,10 +195,13 @@ describe('client', () => {
       expect(stripSymbols(actualResult.data)).toEqual(result);
     });
 
-    return Promise.all([basic, withDefault]);
+    return Promise.all([
+      basic,
+      withDefault,
+    ]).then(resolve, reject);
   });
 
-  it('should allow for a single query with default values that get overridden with variables', () => {
+  itAsync('should allow for a single query with default values that get overridden with variables', (resolve, reject) => {
     const query = gql`
       query people($first: Int = 1) {
         allPeople(first: $first) {
@@ -241,16 +238,13 @@ describe('client', () => {
       },
     };
 
-    const link = mockSingleLink(
-      {
-        request: { query, variables },
-        result: { data: result },
-      },
-      {
-        request: { query, variables: override },
-        result: { data: overriddenResult },
-      },
-    );
+    const link = mockSingleLink({
+      request: { query, variables },
+      result: { data: result },
+    }, {
+      request: { query, variables: override },
+      result: { data: overriddenResult },
+    }).setOnError(reject);
 
     const client = new ApolloClient({
       link,
@@ -273,10 +267,14 @@ describe('client', () => {
         );
       });
 
-    return Promise.all([basic, withDefault, withOverride]);
+    return Promise.all([
+      basic,
+      withDefault,
+      withOverride,
+    ]).then(resolve, reject);
   });
 
-  it('should allow fragments on root query', () => {
+  itAsync('should allow fragments on root query', (resolve, reject) => {
     const query = gql`
       query {
         ...QueryFragment
@@ -300,10 +298,10 @@ describe('client', () => {
       __typename: 'Query',
     };
 
-    return clientRoundtrip(query, { data }, null);
+    return clientRoundtrip(resolve, reject, query, { data }, null);
   });
 
-  it('should allow fragments on root query with ifm', () => {
+  itAsync('should allow fragments on root query with ifm', (resolve, reject) => {
     const query = gql`
       query {
         ...QueryFragment
@@ -327,28 +325,12 @@ describe('client', () => {
       __typename: 'Query',
     };
 
-    const ifm = new IntrospectionFragmentMatcher({
-      introspectionQueryResultData: {
-        __schema: {
-          types: [
-            {
-              kind: 'UNION',
-              name: 'Query',
-              possibleTypes: [
-                {
-                  name: 'Record',
-                },
-              ],
-            },
-          ],
-        },
-      },
+    return clientRoundtrip(resolve, reject, query, { data }, null, {
+      Query: ['Record'],
     });
-
-    return clientRoundtrip(query, { data }, null, ifm);
   });
 
-  it('should merge fragments on root query', () => {
+  itAsync('should merge fragments on root query', (resolve, reject) => {
     // The fragment should be used after the selected fields for the query.
     // Otherwise, the results aren't merged.
     // see: https://github.com/apollographql/apollo-client/issues/1479
@@ -378,28 +360,12 @@ describe('client', () => {
       __typename: 'Query',
     };
 
-    const ifm = new IntrospectionFragmentMatcher({
-      introspectionQueryResultData: {
-        __schema: {
-          types: [
-            {
-              kind: 'UNION',
-              name: 'Query',
-              possibleTypes: [
-                {
-                  name: 'Record',
-                },
-              ],
-            },
-          ],
-        },
-      },
+    return clientRoundtrip(resolve, reject, query, { data }, null, {
+      Query: ['Record'],
     });
-
-    return clientRoundtrip(query, { data }, null, ifm);
   });
 
-  it('store can be rehydrated from the server', () => {
+  itAsync('store can be rehydrated from the server', (resolve, reject) => {
     const query = gql`
       query people {
         allPeople(first: 1) {
@@ -423,27 +389,17 @@ describe('client', () => {
     const link = mockSingleLink({
       request: { query },
       result: { data },
-    });
+    }).setOnError(reject);
 
     const initialState: any = {
       data: {
-        'ROOT_QUERY.allPeople({"first":"1"}).people.0': {
-          name: 'Luke Skywalker',
-        },
-        'ROOT_QUERY.allPeople({"first":1})': {
-          people: [
-            {
-              type: 'id',
-              generated: true,
-              id: 'ROOT_QUERY.allPeople({"first":"1"}).people.0',
-            },
-          ],
-        },
         ROOT_QUERY: {
           'allPeople({"first":1})': {
-            type: 'id',
-            id: 'ROOT_QUERY.allPeople({"first":1})',
-            generated: true,
+            people: [
+              {
+                name: 'Luke Skywalker',
+              },
+            ],
           },
         },
         optimistic: [],
@@ -464,10 +420,10 @@ describe('client', () => {
       expect(finalState.data).toEqual(
         (client.cache as InMemoryCache).extract(),
       );
-    });
+    }).then(resolve, reject);
   });
 
-  it('store can be rehydrated from the server using the shadow method', () => {
+  itAsync('store can be rehydrated from the server using the shadow method', (resolve, reject) => {
     const query = gql`
       query people {
         allPeople(first: 1) {
@@ -491,27 +447,17 @@ describe('client', () => {
     const link = mockSingleLink({
       request: { query },
       result: { data },
-    });
+    }).setOnError(reject);
 
     const initialState: any = {
       data: {
-        'ROOT_QUERY.allPeople({"first":"1"}).people.0': {
-          name: 'Luke Skywalker',
-        },
-        'ROOT_QUERY.allPeople({"first":1})': {
-          people: [
-            {
-              type: 'id',
-              generated: true,
-              id: 'ROOT_QUERY.allPeople({"first":"1"}).people.0',
-            },
-          ],
-        },
         ROOT_QUERY: {
           'allPeople({"first":1})': {
-            type: 'id',
-            id: 'ROOT_QUERY.allPeople({"first":1})',
-            generated: true,
+            people: [
+              {
+                name: 'Luke Skywalker',
+              },
+            ],
           },
         },
         optimistic: [],
@@ -530,10 +476,10 @@ describe('client', () => {
     return client.query({ query }).then(result => {
       expect(stripSymbols(result.data)).toEqual(data);
       expect(finalState.data).toEqual(client.extract());
-    });
+    }).then(resolve, reject);
   });
 
-  it('stores shadow of restore returns the same result as accessing the method directly on the cache', () => {
+  itAsync('stores shadow of restore returns the same result as accessing the method directly on the cache', (resolve, reject) => {
     const query = gql`
       query people {
         allPeople(first: 1) {
@@ -557,7 +503,7 @@ describe('client', () => {
     const link = mockSingleLink({
       request: { query },
       result: { data },
-    });
+    }).setOnError(reject);
 
     const initialState: any = {
       data: {
@@ -594,9 +540,11 @@ describe('client', () => {
     expect(client.restore(initialState.data)).toEqual(
       client.cache.restore(initialState.data),
     );
+
+    resolve();
   });
 
-  it('should return errors correctly for a single query', () => {
+  itAsync('should return errors correctly for a single query', (resolve, reject) => {
     const query = gql`
       query people {
         allPeople(first: 1) {
@@ -617,7 +565,7 @@ describe('client', () => {
     const link = mockSingleLink({
       request: { query },
       result: { errors },
-    });
+    }).setOnError(reject);
 
     const client = new ApolloClient({
       link,
@@ -626,10 +574,10 @@ describe('client', () => {
 
     return client.query({ query }).catch((error: ApolloError) => {
       expect(error.graphQLErrors).toEqual(errors);
-    });
+    }).then(resolve, reject);
   });
 
-  it('should return GraphQL errors correctly for a single query with an apollo-link enabled network interface', done => {
+  itAsync('should return GraphQL errors correctly for a single query with an apollo-link enabled network interface', (resolve, reject) => {
     const query = gql`
       query people {
         allPeople(first: 1) {
@@ -672,11 +620,11 @@ describe('client', () => {
 
     client.query({ query }).catch((error: ApolloError) => {
       expect(error.graphQLErrors).toEqual(errors);
-      done();
+      resolve();
     });
   });
 
-  xit('should pass a network error correctly on a query using an observable network interface with a warning', done => {
+  itAsync.skip('should pass a network error correctly on a query using an observable network interface with a warning', (resolve, reject) => {
     withWarning(() => {
       const query = gql`
         query people {
@@ -706,12 +654,12 @@ describe('client', () => {
       client.query({ query }).catch((error: ApolloError) => {
         expect(error.networkError).toBeDefined();
         expect(error.networkError!.message).toEqual(networkError.message);
-        done();
+        resolve();
       });
     }, /deprecated/);
   });
 
-  it('should pass a network error correctly on a query with apollo-link network interface', done => {
+  itAsync('should pass a network error correctly on a query with apollo-link network interface', (resolve, reject) => {
     const query = gql`
       query people {
         allPeople(first: 1) {
@@ -740,7 +688,7 @@ describe('client', () => {
     client.query({ query }).catch((error: ApolloError) => {
       expect(error.networkError).toBeDefined();
       expect(error.networkError!.message).toEqual(networkError.message);
-      done();
+      resolve();
     });
   });
 
@@ -777,7 +725,7 @@ describe('client', () => {
     });
   });
 
-  xit('should surface errors in observer.next as uncaught', done => {
+  itAsync.skip('should surface errors in observer.next as uncaught', (resolve, reject) => {
     const expectedError = new Error('this error should not reach the store');
     const listeners = process.listeners('uncaughtException');
     const oldHandler = listeners[listeners.length - 1];
@@ -787,9 +735,9 @@ describe('client', () => {
       if (typeof oldHandler === 'function')
         process.addListener('uncaughtException', oldHandler);
       if (e === expectedError) {
-        done();
+        resolve();
       } else {
-        done.fail(e);
+        reject(e);
       }
     };
     process.removeListener('uncaughtException', oldHandler);
@@ -818,7 +766,7 @@ describe('client', () => {
     const link = mockSingleLink({
       request: { query },
       result: { data },
-    });
+    }).setOnError(reject);
 
     const client = new ApolloClient({
       link,
@@ -834,7 +782,7 @@ describe('client', () => {
     });
   });
 
-  xit('should surfaces errors in observer.error as uncaught', done => {
+  itAsync.skip('should surfaces errors in observer.error as uncaught', (resolve, reject) => {
     const expectedError = new Error('this error should not reach the store');
     const listeners = process.listeners('uncaughtException');
     const oldHandler = listeners[listeners.length - 1];
@@ -842,9 +790,9 @@ describe('client', () => {
       process.removeListener('uncaughtException', handleUncaught);
       process.addListener('uncaughtException', oldHandler);
       if (e === expectedError) {
-        done();
+        resolve();
       } else {
-        done.fail(e);
+        reject(e);
       }
     };
     process.removeListener('uncaughtException', oldHandler);
@@ -863,7 +811,7 @@ describe('client', () => {
     const link = mockSingleLink({
       request: { query },
       result: {},
-    });
+    }).setOnError(reject);
 
     const client = new ApolloClient({
       link,
@@ -873,7 +821,7 @@ describe('client', () => {
     const handle = client.watchQuery({ query });
     handle.subscribe({
       next() {
-        done.fail(new Error('did not expect next to be called'));
+        reject(new Error('did not expect next to be called'));
       },
       error() {
         throw expectedError;
@@ -881,7 +829,7 @@ describe('client', () => {
     });
   });
 
-  it('should allow for subscribing to a request', done => {
+  itAsync('should allow for subscribing to a request', (resolve, reject) => {
     const query = gql`
       query people {
         allPeople(first: 1) {
@@ -905,7 +853,7 @@ describe('client', () => {
     const link = mockSingleLink({
       request: { query },
       result: { data },
-    });
+    }).setOnError(reject);
 
     const client = new ApolloClient({
       link,
@@ -917,12 +865,12 @@ describe('client', () => {
     handle.subscribe({
       next(result) {
         expect(stripSymbols(result.data)).toEqual(data);
-        done();
+        resolve();
       },
     });
   });
 
-  it('should be able to transform queries', () => {
+  itAsync('should be able to transform queries', (resolve, reject) => {
     const query = gql`
       query {
         author {
@@ -955,16 +903,13 @@ describe('client', () => {
       },
     };
 
-    const link = mockSingleLink(
-      {
-        request: { query },
-        result: { data: result },
-      },
-      {
-        request: { query: transformedQuery },
-        result: { data: transformedResult },
-      },
-    );
+    const link = mockSingleLink({
+      request: { query },
+      result: { data: result },
+    }, {
+      request: { query: transformedQuery },
+      result: { data: transformedResult },
+    }, false).setOnError(reject);
 
     const client = new ApolloClient({
       link,
@@ -973,10 +918,10 @@ describe('client', () => {
 
     return client.query({ query }).then(actualResult => {
       expect(stripSymbols(actualResult.data)).toEqual(transformedResult);
-    });
+    }).then(resolve, reject);
   });
 
-  it('should be able to transform queries on network-only fetches', () => {
+  itAsync('should be able to transform queries on network-only fetches', (resolve, reject) => {
     const query = gql`
       query {
         author {
@@ -1007,16 +952,13 @@ describe('client', () => {
         __typename: 'Author',
       },
     };
-    const link = mockSingleLink(
-      {
-        request: { query },
-        result: { data: result },
-      },
-      {
-        request: { query: transformedQuery },
-        result: { data: transformedResult },
-      },
-    );
+    const link = mockSingleLink({
+      request: { query },
+      result: { data: result },
+    }, {
+      request: { query: transformedQuery },
+      result: { data: transformedResult },
+    }, false).setOnError(reject);
 
     const client = new ApolloClient({
       link,
@@ -1027,10 +969,11 @@ describe('client', () => {
       .query({ fetchPolicy: 'network-only', query })
       .then(actualResult => {
         expect(stripSymbols(actualResult.data)).toEqual(transformedResult);
-      });
+      })
+      .then(resolve, reject);
   });
 
-  it('should handle named fragments on mutations', () => {
+  itAsync('should handle named fragments on mutations', (resolve, reject) => {
     const mutation = gql`
       mutation {
         starAuthor(id: 12) {
@@ -1058,7 +1001,7 @@ describe('client', () => {
     const link = mockSingleLink({
       request: { query: mutation },
       result: { data: result },
-    });
+    }).setOnError(reject);
     const client = new ApolloClient({
       link,
       cache: new InMemoryCache({ addTypename: false }),
@@ -1066,10 +1009,10 @@ describe('client', () => {
 
     return client.mutate({ mutation }).then(actualResult => {
       expect(stripSymbols(actualResult.data)).toEqual(result);
-    });
+    }).then(resolve, reject);
   });
 
-  it('should be able to handle named fragments on network-only queries', () => {
+  itAsync('should be able to handle named fragments on network-only queries', (resolve, reject) => {
     const query = gql`
       fragment authorDetails on Author {
         firstName
@@ -1094,7 +1037,7 @@ describe('client', () => {
     const link = mockSingleLink({
       request: { query },
       result: { data: result },
-    });
+    }).setOnError(reject);
 
     const client = new ApolloClient({
       link,
@@ -1105,10 +1048,11 @@ describe('client', () => {
       .query({ fetchPolicy: 'network-only', query })
       .then(actualResult => {
         expect(stripSymbols(actualResult.data)).toEqual(result);
-      });
+      })
+      .then(resolve, reject);
   });
 
-  it('should be able to handle named fragments with multiple fragments', () => {
+  itAsync('should be able to handle named fragments with multiple fragments', (resolve, reject) => {
     const query = gql`
       query {
         author {
@@ -1139,7 +1083,7 @@ describe('client', () => {
     const link = mockSingleLink({
       request: { query },
       result: { data: result },
-    });
+    }).setOnError(reject);
     const client = new ApolloClient({
       link,
       cache: new InMemoryCache({ addTypename: false }),
@@ -1147,10 +1091,10 @@ describe('client', () => {
 
     return client.query({ query }).then(actualResult => {
       expect(stripSymbols(actualResult.data)).toEqual(result);
-    });
+    }).then(resolve, reject);
   });
 
-  it('should be able to handle named fragments', () => {
+  itAsync('should be able to handle named fragments', (resolve, reject) => {
     const query = gql`
       query {
         author {
@@ -1175,7 +1119,7 @@ describe('client', () => {
     const link = mockSingleLink({
       request: { query },
       result: { data: result },
-    });
+    }).setOnError(reject);
     const client = new ApolloClient({
       link,
       cache: new InMemoryCache({ addTypename: false }),
@@ -1183,10 +1127,10 @@ describe('client', () => {
 
     return client.query({ query }).then(actualResult => {
       expect(stripSymbols(actualResult.data)).toEqual(result);
-    });
+    }).then(resolve, reject);
   });
 
-  it('should be able to handle inlined fragments on an Interface type', () => {
+  itAsync('should be able to handle inlined fragments on an Interface type', (resolve, reject) => {
     const query = gql`
       query items {
         items {
@@ -1218,49 +1162,24 @@ describe('client', () => {
       ],
     };
 
-    const fancyFragmentMatcher = (
-      idValue: any, // TODO types, please.
-      typeCondition: string,
-      context: any,
-    ): boolean => {
-      const obj = context.store.get(idValue.id);
-
-      if (!obj) {
-        return false;
-      }
-
-      const implementingTypesMap: { [key: string]: string[] } = {
-        Item: ['ColorItem', 'MonochromeItem'],
-      };
-
-      if (obj.__typename === typeCondition) {
-        return true;
-      }
-
-      const implementingTypes = implementingTypesMap[typeCondition];
-      if (implementingTypes && implementingTypes.indexOf(obj.__typename) > -1) {
-        return true;
-      }
-
-      return false;
-    };
-
     const link = mockSingleLink({
       request: { query },
       result: { data: result },
-    });
+    }).setOnError(reject);
     const client = new ApolloClient({
       link,
       cache: new InMemoryCache({
-        fragmentMatcher: { match: fancyFragmentMatcher },
+        possibleTypes: {
+          Item: ['ColorItem', 'MonochromeItem'],
+        },
       }),
     });
     return client.query({ query }).then((actualResult: any) => {
       expect(stripSymbols(actualResult.data)).toEqual(result);
-    });
+    }).then(resolve, reject);
   });
 
-  it('should be able to handle inlined fragments on an Interface type with introspection fragment matcher', () => {
+  itAsync('should be able to handle inlined fragments on an Interface type with introspection fragment matcher', (resolve, reject) => {
     const query = gql`
       query items {
         items {
@@ -1295,40 +1214,23 @@ describe('client', () => {
     const link = mockSingleLink({
       request: { query },
       result: { data: result },
-    });
-
-    const ifm = new IntrospectionFragmentMatcher({
-      introspectionQueryResultData: {
-        __schema: {
-          types: [
-            {
-              kind: 'UNION',
-              name: 'Item',
-              possibleTypes: [
-                {
-                  name: 'ColorItem',
-                },
-                {
-                  name: 'MonochromeItem',
-                },
-              ],
-            },
-          ],
-        },
-      },
-    });
+    }).setOnError(reject);
 
     const client = new ApolloClient({
       link,
-      cache: new InMemoryCache({ fragmentMatcher: ifm }),
+      cache: new InMemoryCache({
+        possibleTypes: {
+          Item: ['ColorItem', 'MonochromeItem'],
+        },
+      }),
     });
 
     return client.query({ query }).then(actualResult => {
       expect(stripSymbols(actualResult.data)).toEqual(result);
-    });
+    }).then(resolve, reject);
   });
 
-  it('should call updateQueries and update after mutation on query with inlined fragments on an Interface type', done => {
+  itAsync('should call updateQueries and update after mutation on query with inlined fragments on an Interface type', (resolve, reject) => {
     const query = gql`
       query items {
         items {
@@ -1369,41 +1271,21 @@ describe('client', () => {
       fortuneCookie: 'The waiter spit in your food',
     };
 
-    const link = mockSingleLink(
-      {
-        request: { query },
-        result: { data: result },
-      },
-      {
-        request: { query: mutation },
-        result: { data: mutationResult },
-      },
-    );
-
-    const ifm = new IntrospectionFragmentMatcher({
-      introspectionQueryResultData: {
-        __schema: {
-          types: [
-            {
-              kind: 'UNION',
-              name: 'Item',
-              possibleTypes: [
-                {
-                  name: 'ColorItem',
-                },
-                {
-                  name: 'MonochromeItem',
-                },
-              ],
-            },
-          ],
-        },
-      },
-    });
+    const link = mockSingleLink({
+      request: { query },
+      result: { data: result },
+    }, {
+      request: { query: mutation },
+      result: { data: mutationResult },
+    }).setOnError(reject);
 
     const client = new ApolloClient({
       link,
-      cache: new InMemoryCache({ fragmentMatcher: ifm }),
+      cache: new InMemoryCache({
+        possibleTypes: {
+          Item: ['ColorItem', 'MonochromeItem'],
+        },
+      }),
     });
 
     const queryUpdaterSpy = jest.fn();
@@ -1427,14 +1309,14 @@ describe('client', () => {
             expect(queryUpdaterSpy).toBeCalled();
             expect(updateSpy).toBeCalled();
             sub.unsubscribe();
-            done();
+            resolve();
           })
           .catch(err => {
-            done.fail(err);
+            reject(err);
           });
       },
       error(err) {
-        done.fail(err);
+        reject(err);
       },
     });
   });
@@ -1489,7 +1371,7 @@ describe('client', () => {
     });
   });
 
-  it('does not deduplicate queries if option is set to false', () => {
+  itAsync('does not deduplicate queries if option is set to false', (resolve, reject) => {
     const queryDoc = gql`
       query {
         author {
@@ -1510,17 +1392,14 @@ describe('client', () => {
 
     // we have two responses for identical queries, but only the first should be requested.
     // the second one should never make it through to the network interface.
-    const link = mockSingleLink(
-      {
-        request: { query: queryDoc },
-        result: { data },
-        delay: 10,
-      },
-      {
-        request: { query: queryDoc },
-        result: { data: data2 },
-      },
-    );
+    const link = mockSingleLink({
+      request: { query: queryDoc },
+      result: { data },
+      delay: 10,
+    }, {
+      request: { query: queryDoc },
+      result: { data: data2 },
+    }).setOnError(reject);
     const client = new ApolloClient({
       link,
       cache: new InMemoryCache({ addTypename: false }),
@@ -1535,10 +1414,10 @@ describe('client', () => {
     return Promise.all([q1, q2]).then(([result1, result2]) => {
       expect(stripSymbols(result1.data)).toEqual(data);
       expect(stripSymbols(result2.data)).toEqual(data2);
-    });
+    }).then(resolve, reject);
   });
 
-  it('deduplicates queries by default', () => {
+  itAsync('deduplicates queries by default', (resolve, reject) => {
     const queryDoc = gql`
       query {
         author {
@@ -1559,17 +1438,14 @@ describe('client', () => {
 
     // we have two responses for identical queries, but only the first should be requested.
     // the second one should never make it through to the network interface.
-    const link = mockSingleLink(
-      {
-        request: { query: queryDoc },
-        result: { data },
-        delay: 10,
-      },
-      {
-        request: { query: queryDoc },
-        result: { data: data2 },
-      },
-    );
+    const link = mockSingleLink({
+      request: { query: queryDoc },
+      result: { data },
+      delay: 10,
+    }, {
+      request: { query: queryDoc },
+      result: { data: data2 },
+    }).setOnError(reject);
     const client = new ApolloClient({
       link,
       cache: new InMemoryCache({ addTypename: false }),
@@ -1581,10 +1457,10 @@ describe('client', () => {
     // if deduplication didn't happen, result.data will equal data2.
     return Promise.all([q1, q2]).then(([result1, result2]) => {
       expect(result1.data).toEqual(result2.data);
-    });
+    }).then(resolve, reject);
   });
 
-  it('unsubscribes from deduplicated observables only once', done => {
+  itAsync('unsubscribes from deduplicated observables only once', (resolve, reject) => {
     const document: DocumentNode = gql`
       query test1($x: String) {
         test(x: $x)
@@ -1602,7 +1478,7 @@ describe('client', () => {
           observer.complete();
           return () => {
             unsubscribed = true;
-            setTimeout(done, 0);
+            setTimeout(resolve, 0);
           };
         });
       }),
@@ -1680,11 +1556,11 @@ describe('client', () => {
       },
     };
 
-    it('for internal store', () => {
+    itAsync('for internal store', (resolve, reject) => {
       const link = mockSingleLink({
         request: { query },
         result: { data },
-      });
+      }).setOnError(reject);
 
       const client = new ApolloClient({
         link,
@@ -1701,7 +1577,7 @@ describe('client', () => {
           id: '1',
           name: 'Luke Skywalker',
         });
-      });
+      }).then(resolve, reject);
     });
   });
 
@@ -1769,11 +1645,11 @@ describe('client', () => {
       checkCacheAndNetworkError(() => client.query({ query }));
     });
 
-    it('fetches from cache first, then network', done => {
+    itAsync('fetches from cache first, then network', (resolve, reject) => {
       const link = mockSingleLink({
         request: { query },
         result: { data: networkFetch },
-      });
+      }).setOnError(reject);
 
       const client = new ApolloClient({
         link,
@@ -1787,21 +1663,21 @@ describe('client', () => {
         fetchPolicy: 'cache-and-network',
       });
 
-      subscribeAndCount(done, obs, (handleCount, result) => {
+      subscribeAndCount(reject, obs, (handleCount, result) => {
         if (handleCount === 1) {
           expect(stripSymbols(result.data)).toEqual(initialData);
         } else if (handleCount === 2) {
           expect(stripSymbols(result.data)).toEqual(networkFetch);
-          done();
+          resolve();
         }
       });
     });
 
-    it('does not fail if cache entry is not present', done => {
+    itAsync('does not fail if cache entry is not present', (resolve, reject) => {
       const link = mockSingleLink({
         request: { query },
         result: { data: networkFetch },
-      });
+      }).setOnError(reject);
       const client = new ApolloClient({
         link,
         cache: new InMemoryCache({ addTypename: false }),
@@ -1812,20 +1688,20 @@ describe('client', () => {
         fetchPolicy: 'cache-and-network',
       });
 
-      subscribeAndCount(done, obs, (handleCount, result) => {
+      subscribeAndCount(reject, obs, (handleCount, result) => {
         if (handleCount === 1) {
           expect(result.data).toBe(undefined);
           expect(result.loading).toBe(true);
         } else if (handleCount === 2) {
           expect(stripSymbols(result.data)).toEqual(networkFetch);
           expect(result.loading).toBe(false);
-          done();
+          resolve();
         }
       });
     });
 
-    it('fails if network request fails', done => {
-      const link = mockSingleLink(); // no queries = no replies.
+    itAsync('fails if network request fails', (resolve, reject) => {
+      const link = mockSingleLink().setOnError(error => { throw error }); // no queries = no replies.
       const client = new ApolloClient({
         link,
         cache: new InMemoryCache({ addTypename: false }),
@@ -1846,16 +1722,16 @@ describe('client', () => {
         error: e => {
           expect(e.message).toMatch(/No more mocked responses/);
           expect(count).toBe(1); // make sure next was called.
-          setTimeout(done, 100);
+          setTimeout(resolve, 100);
         },
       });
     });
 
-    it('fetches from cache first, then network and does not have an unhandled error', done => {
+    itAsync('fetches from cache first, then network and does not have an unhandled error', (resolve, reject) => {
       const link = mockSingleLink({
         request: { query },
         result: { errors: [{ message: 'network failure' }] },
-      });
+      }).setOnError(reject);
 
       const client = new ApolloClient({
         link,
@@ -1870,7 +1746,7 @@ describe('client', () => {
       });
       let shouldFail = true;
       process.once('unhandledRejection', rejection => {
-        if (shouldFail) done.fail('promise had an unhandledRejection');
+        if (shouldFail) reject('promise had an unhandledRejection');
       });
       let count = 0;
       obs.subscribe({
@@ -1884,7 +1760,7 @@ describe('client', () => {
           expect(count).toBe(1); // make sure next was called.
           setTimeout(() => {
             shouldFail = false;
-            done();
+            resolve();
           }, 0);
         },
       });
@@ -1914,7 +1790,7 @@ describe('client', () => {
       );
     });
 
-    it('are not watching the store or notifying on updates', done => {
+    itAsync('are not watching the store or notifying on updates', (resolve, reject) => {
       const query = gql`
         {
           test
@@ -1926,14 +1802,14 @@ describe('client', () => {
       const link = mockSingleLink({
         request: { query },
         result: { data },
-      });
+      }).setOnError(reject);
 
       const client = new ApolloClient({ link, cache: new InMemoryCache() });
 
       const obs = client.watchQuery({ query, fetchPolicy: 'cache-first' });
 
       let handleCalled = false;
-      subscribeAndCount(done, obs, (handleCount, result) => {
+      subscribeAndCount(reject, obs, (handleCount, result) => {
         if (handleCount === 1) {
           expect(stripSymbols(result.data)).toEqual(data);
           obs.setOptions({ fetchPolicy: 'standby' }).then(() => {
@@ -1942,20 +1818,20 @@ describe('client', () => {
           });
           setTimeout(() => {
             if (!handleCalled) {
-              done();
+              resolve();
             }
           }, 20);
         }
         if (handleCount === 2) {
           handleCalled = true;
-          done.fail(
+          reject(
             new Error('Handle should never be called on standby query'),
           );
         }
       });
     });
 
-    it('return the current result when coming out of standby', done => {
+    itAsync('return the current result when coming out of standby', (resolve, reject) => {
       const query = gql`
         {
           test
@@ -1967,14 +1843,14 @@ describe('client', () => {
       const link = mockSingleLink({
         request: { query },
         result: { data },
-      });
+      }).setOnError(reject);
 
       const client = new ApolloClient({ link, cache: new InMemoryCache() });
 
       const obs = client.watchQuery({ query, fetchPolicy: 'cache-first' });
 
       let handleCalled = false;
-      subscribeAndCount(done, obs, (handleCount, result) => {
+      subscribeAndCount(reject, obs, (handleCount, result) => {
         if (handleCount === 1) {
           expect(stripSymbols(result.data)).toEqual(data);
           obs.setOptions({ fetchPolicy: 'standby' }).then(() => {
@@ -1988,7 +1864,7 @@ describe('client', () => {
         if (handleCount === 2) {
           handleCalled = true;
           expect(stripSymbols(result.data)).toEqual(data2);
-          done();
+          resolve();
         }
       });
     });
@@ -2014,44 +1890,36 @@ describe('client', () => {
       },
     };
 
-    let link: any;
-    beforeEach(() => {
-      link = mockSingleLink(
-        {
-          request: { query },
-          result: { data: firstFetch },
-        },
-        {
-          request: { query },
-          result: { data: secondFetch },
-        },
-      );
-      //
-    });
-
-    afterAll(() => jest.useRealTimers());
+    function makeLink(reject: (reason: any) => any) {
+      return mockSingleLink({
+        request: { query },
+        result: { data: firstFetch },
+      }, {
+        request: { query },
+        result: { data: secondFetch },
+      }).setOnError(reject);
+    }
 
-    it('forces the query to rerun', () => {
+    itAsync('forces the query to rerun', (resolve, reject) => {
       const client = new ApolloClient({
-        link,
+        link: makeLink(reject),
         cache: new InMemoryCache({ addTypename: false }),
       });
 
       // Run a query first to initialize the store
-      return (
-        client
-          .query({ query })
-          // then query for real
-          .then(() => client.query({ query, fetchPolicy: 'network-only' }))
-          .then(result => {
-            expect(stripSymbols(result.data)).toEqual({ myNumber: { n: 2 } });
-          })
-      );
+      return client
+        .query({ query })
+        // then query for real
+        .then(() => client.query({ query, fetchPolicy: 'network-only' }))
+        .then(result => {
+          expect(stripSymbols(result.data)).toEqual({ myNumber: { n: 2 } });
+        })
+        .then(resolve, reject);
     });
 
-    it('can be disabled with ssrMode', () => {
+    itAsync('can be disabled with ssrMode', (resolve, reject) => {
       const client = new ApolloClient({
-        link,
+        link: makeLink(reject),
         ssrMode: true,
         cache: new InMemoryCache({ addTypename: false }),
       });
@@ -2059,56 +1927,48 @@ describe('client', () => {
       const options: WatchQueryOptions = { query, fetchPolicy: 'network-only' };
 
       // Run a query first to initialize the store
-      return (
-        client
-          .query({ query })
-          // then query for real
-          .then(() => client.query(options))
-          .then(result => {
-            expect(stripSymbols(result.data)).toEqual({ myNumber: { n: 1 } });
-
-            // Test that options weren't mutated, issue #339
-            expect(options).toEqual({
-              query,
-              fetchPolicy: 'network-only',
-            });
-          })
-      );
+      return client
+        .query({ query })
+        // then query for real
+        .then(() => client.query(options))
+        .then(result => {
+          expect(stripSymbols(result.data)).toEqual({ myNumber: { n: 1 } });
+          // Test that options weren't mutated, issue #339
+          expect(options).toEqual({
+            query,
+            fetchPolicy: 'network-only',
+          });
+        })
+        .then(resolve, reject);
     });
 
-    it('can temporarily be disabled with ssrForceFetchDelay', () => {
-      jest.useFakeTimers();
+    itAsync('can temporarily be disabled with ssrForceFetchDelay', (resolve, reject) => {
       const client = new ApolloClient({
-        link,
+        link: makeLink(reject),
         ssrForceFetchDelay: 100,
         cache: new InMemoryCache({ addTypename: false }),
       });
 
       // Run a query first to initialize the store
-      const outerPromise = client
+      return client
         .query({ query })
         // then query for real
         .then(() => {
-          const promise = client.query({ query, fetchPolicy: 'network-only' });
-          jest.runTimersToTime(0);
-          return promise;
+          return client.query({ query, fetchPolicy: 'network-only' });
         })
-        .then(result => {
+        .then(async result => {
           expect(stripSymbols(result.data)).toEqual({ myNumber: { n: 1 } });
-          jest.runTimersToTime(100);
-          const promise = client.query({ query, fetchPolicy: 'network-only' });
-          jest.runTimersToTime(0);
-          return promise;
+          await new Promise(resolve => setTimeout(resolve, 100));
+          return client.query({ query, fetchPolicy: 'network-only' });
         })
         .then(result => {
           expect(stripSymbols(result.data)).toEqual({ myNumber: { n: 2 } });
-        });
-      jest.runTimersToTime(0);
-      return outerPromise;
+        })
+        .then(resolve, reject);
     });
   });
 
-  it('should pass a network error correctly on a mutation', done => {
+  itAsync('should pass a network error correctly on a mutation', (resolve, reject) => {
     const mutation = gql`
       mutation {
         person {
@@ -2129,23 +1989,23 @@ describe('client', () => {
         request: { query: mutation },
         result: { data },
         error: networkError,
-      }),
+      }).setOnError(reject),
       cache: new InMemoryCache({ addTypename: false }),
     });
 
     client
       .mutate({ mutation })
       .then(_ => {
-        done.fail(new Error('Returned a result when it should not have.'));
+        reject(new Error('Returned a result when it should not have.'));
       })
       .catch((error: ApolloError) => {
         expect(error.networkError).toBeDefined();
         expect(error.networkError!.message).toBe(networkError.message);
-        done();
+        resolve();
       });
   });
 
-  it('should pass a GraphQL error correctly on a mutation', done => {
+  itAsync('should pass a GraphQL error correctly on a mutation', (resolve, reject) => {
     const mutation = gql`
       mutation {
         newPerson {
@@ -2167,22 +2027,22 @@ describe('client', () => {
       link: mockSingleLink({
         request: { query: mutation },
         result: { data, errors },
-      }),
+      }).setOnError(reject),
       cache: new InMemoryCache({ addTypename: false }),
     });
     client
       .mutate({ mutation })
       .then(_ => {
-        done.fail(new Error('Returned a result when it should not have.'));
+        reject(new Error('Returned a result when it should not have.'));
       })
       .catch((error: ApolloError) => {
         expect(error.graphQLErrors).toBeDefined();
         expect(error.graphQLErrors.length).toBe(1);
         expect(error.graphQLErrors[0].message).toBe(errors[0].message);
-        done();
+        resolve();
       });
   });
-  it('should allow errors to be returned from a mutation', done => {
+  itAsync('should allow errors to be returned from a mutation', (resolve, reject) => {
     const mutation = gql`
       mutation {
         newPerson {
@@ -2204,7 +2064,7 @@ describe('client', () => {
       link: mockSingleLink({
         request: { query: mutation },
         result: { data, errors },
-      }),
+      }).setOnError(reject),
       cache: new InMemoryCache({ addTypename: false }),
     });
     client
@@ -2214,13 +2074,14 @@ describe('client', () => {
         expect(result.errors.length).toBe(1);
         expect(result.errors[0].message).toBe(errors[0].message);
         expect(result.data).toEqual(data);
-        done();
+        resolve();
       })
       .catch((error: ApolloError) => {
         throw error;
       });
   });
-  it('should strip errors on a mutation if ignored', done => {
+
+  itAsync('should strip errors on a mutation if ignored', (resolve, reject) => {
     const mutation = gql`
       mutation {
         newPerson {
@@ -2242,7 +2103,7 @@ describe('client', () => {
       link: mockSingleLink({
         request: { query: mutation },
         result: { data, errors },
-      }),
+      }).setOnError(reject),
       cache: new InMemoryCache({ addTypename: false }),
     });
     client
@@ -2250,14 +2111,14 @@ describe('client', () => {
       .then(result => {
         expect(result.errors).toBeUndefined();
         expect(stripSymbols(result.data)).toEqual(data);
-        done();
+        resolve();
       })
       .catch((error: ApolloError) => {
         throw error;
       });
   });
 
-  it('should rollback optimistic after mutation got a GraphQL error', done => {
+  itAsync('should rollback optimistic after mutation got a GraphQL error', (resolve, reject) => {
     const mutation = gql`
       mutation {
         newPerson {
@@ -2281,7 +2142,7 @@ describe('client', () => {
       link: mockSingleLink({
         request: { query: mutation },
         result: { data, errors },
-      }),
+      }).setOnError(reject),
       cache: new InMemoryCache({ addTypename: false }),
     });
     const mutatePromise = client.mutate({
@@ -2296,18 +2157,20 @@ describe('client', () => {
       },
     });
 
-    const { data, optimisticData } = client.cache as any;
-    expect(optimisticData).not.toBe(data);
-    expect(optimisticData.parent).toBe(data);
+    {
+      const { data, optimisticData } = client.cache as any;
+      expect(optimisticData).not.toBe(data);
+      expect(optimisticData.parent).toBe(data);
+    }
 
     mutatePromise
       .then(_ => {
-        done.fail(new Error('Returned a result when it should not have.'));
+        reject(new Error('Returned a result when it should not have.'));
       })
       .catch((_: ApolloError) => {
         const { data, optimisticData } = client.cache as any;
         expect(optimisticData).toBe(data);
-        done();
+        resolve();
       });
   });
 
@@ -2549,7 +2412,7 @@ describe('client', () => {
     };
 
     // it('with self-made store', () => {
-    //   const link = mockSingleLink({
+    //   const link = mockSingleLink(reject, {
     //     request: { query: cloneDeep(query) },
     //     result: { data },
     //   });
@@ -2571,7 +2434,7 @@ describe('client', () => {
     // });
   });
 
-  it('should propagate errors from network interface to observers', done => {
+  itAsync('should propagate errors from network interface to observers', (resolve, reject) => {
     const link = ApolloLink.from([
       () =>
         new Observable(x => {
@@ -2598,12 +2461,12 @@ describe('client', () => {
     handle.subscribe({
       error(error) {
         expect(error.message).toBe('Network error: Uh oh!');
-        done();
+        resolve();
       },
     });
   });
 
-  it('should be able to refetch after there was a network error', done => {
+  itAsync('should be able to refetch after there was a network error', (resolve, reject) => {
     const query: DocumentNode = gql`
       query somethingelse {
         allPeople(first: 1) {
@@ -2619,8 +2482,8 @@ describe('client', () => {
     const link = mockSingleLink(
       { request: { query }, result: { data } },
       { request: { query }, error: new Error('This is an error!') },
-      { request: { query }, result: { data: dataTwo } },
-    );
+      { request: { query }, result: { data: dataTwo } }
+    ).setOnError(reject);
     const client = new ApolloClient({
       link,
       cache: new InMemoryCache({ addTypename: false }),
@@ -2642,7 +2505,7 @@ describe('client', () => {
           switch (count++) {
             case 0:
               if (!result.data!.allPeople) {
-                done.fail('Should have data by this point');
+                reject('Should have data by this point');
                 break;
               }
               // First result is loaded, run a refetch to get the second result
@@ -2654,7 +2517,7 @@ describe('client', () => {
               );
               setTimeout(() => {
                 observable.refetch().then(() => {
-                  done.fail('Expected error value on first refetch.');
+                  reject('Expected error value on first refetch.');
                 }, noop);
               }, 0);
               break;
@@ -2675,19 +2538,19 @@ describe('client', () => {
               expect(result.networkStatus).toBe(7);
               expect(result.errors).toBeFalsy();
               if (!result.data) {
-                done.fail('Should have data by this point');
+                reject('Should have data by this point');
                 break;
               }
               expect(stripSymbols(result.data.allPeople)).toEqual(
                 dataTwo.allPeople,
               );
-              done();
+              resolve();
               break;
             default:
               throw new Error('Unexpected fall through');
           }
         } catch (e) {
-          done.fail(e);
+          reject(e);
         }
       },
       error(error) {
@@ -2710,7 +2573,7 @@ describe('client', () => {
         // which should now contain valid data.
         setTimeout(() => {
           observable.refetch().catch(() => {
-            done.fail('Expected good data on second refetch.');
+            reject('Expected good data on second refetch.');
           });
         }, 0);
       },
@@ -2719,7 +2582,7 @@ describe('client', () => {
     subscription = observable.subscribe(observerOptions);
   });
 
-  it('should throw a GraphQL error', () => {
+  itAsync('should throw a GraphQL error', (resolve, reject) => {
     const query = gql`
       query {
         posts {
@@ -2737,7 +2600,7 @@ describe('client', () => {
     const link = mockSingleLink({
       request: { query },
       result: { errors },
-    });
+    }).setOnError(reject);
     const client = new ApolloClient({
       link,
       cache: new InMemoryCache(),
@@ -2747,10 +2610,10 @@ describe('client', () => {
       expect(err.message).toBe(
         'GraphQL error: Cannot query field "foo" on type "Post".',
       );
-    });
+    }).then(resolve, reject);
   });
 
-  it('should warn if server returns wrong data', () => {
+  itAsync('should warn if server returns wrong data', (resolve, reject) => {
     const query = gql`
       query {
         todos {
@@ -2776,19 +2639,22 @@ describe('client', () => {
     const link = mockSingleLink({
       request: { query },
       result,
-    });
+    }).setOnError(reject);
     const client = new ApolloClient({
       link,
-      cache: new InMemoryCache(),
+      cache: new InMemoryCache({
+        // Passing an empty map enables the warning:
+        possibleTypes: {},
+      }),
     });
 
     return withWarning(
       () => client.query({ query }),
       /Missing field description/,
-    );
+    ).then(resolve, reject);
   });
 
-  it('runs a query with the connection directive and writes it to the store key defined in the directive', () => {
+  itAsync('runs a query with the connection directive and writes it to the store key defined in the directive', (resolve, reject) => {
     const query = gql`
       {
         books(skip: 0, limit: 2) @connection(key: "abc") {
@@ -2818,7 +2684,7 @@ describe('client', () => {
     const link = mockSingleLink({
       request: { query: transformedQuery },
       result: { data: result },
-    });
+    }).setOnError(reject);
 
     const client = new ApolloClient({
       link,
@@ -2827,13 +2693,13 @@ describe('client', () => {
 
     return client.query({ query }).then(actualResult => {
       expect(stripSymbols(actualResult.data)).toEqual(result);
-    });
+    }).then(resolve, reject);
   });
 
-  it('should remove the connection directive before the link is sent', () => {
+  itAsync('runs query with cache field policy analogous to @connection', (resolve, reject) => {
     const query = gql`
       {
-        books(skip: 0, limit: 2) @connection {
+        books(skip: 0, limit: 2) {
           name
         }
       }
@@ -2860,8 +2726,60 @@ describe('client', () => {
     const link = mockSingleLink({
       request: { query: transformedQuery },
       result: { data: result },
+    }).setOnError(reject);
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache({
+        typePolicies: {
+          Query: {
+            fields: {
+              books: {
+                keyArgs: () => "abc",
+              },
+            },
+          },
+        },
+      }),
     });
 
+    return client.query({ query }).then(actualResult => {
+      expect(stripSymbols(actualResult.data)).toEqual(result);
+    }).then(resolve, reject);
+  });
+
+  itAsync('should remove the connection directive before the link is sent', (resolve, reject) => {
+    const query = gql`
+      {
+        books(skip: 0, limit: 2) @connection(key: "books") {
+          name
+        }
+      }
+    `;
+
+    const transformedQuery = gql`
+      {
+        books(skip: 0, limit: 2) {
+          name
+          __typename
+        }
+      }
+    `;
+
+    const result = {
+      books: [
+        {
+          name: 'abcd',
+          __typename: 'Book',
+        },
+      ],
+    };
+
+    const link = mockSingleLink({
+      request: { query: transformedQuery },
+      result: { data: result },
+    }).setOnError(reject);
+
     const client = new ApolloClient({
       link,
       cache: new InMemoryCache(),
@@ -2869,12 +2787,12 @@ describe('client', () => {
 
     return client.query({ query }).then(actualResult => {
       expect(stripSymbols(actualResult.data)).toEqual(result);
-    });
+    }).then(resolve, reject);
   });
 });
 
-describe('@connect', () => {
-  it('should run a query with the connection directive and write the result to the store key defined in the directive', () => {
+describe('@connection', () => {
+  itAsync('should run a query with the @connection directive and write the result to the store key defined in the directive', (resolve, reject) => {
     const query = gql`
       {
         books(skip: 0, limit: 2) @connection(key: "abc") {
@@ -2904,7 +2822,7 @@ describe('@connect', () => {
     const link = mockSingleLink({
       request: { query: transformedQuery },
       result: { data: result },
-    });
+    }).setOnError(reject);
 
     const client = new ApolloClient({
       link,
@@ -2914,10 +2832,10 @@ describe('@connect', () => {
     return client.query({ query }).then(actualResult => {
       expect(stripSymbols(actualResult.data)).toEqual(result);
       expect((client.cache as InMemoryCache).extract()).toMatchSnapshot();
-    });
+    }).then(resolve, reject);
   });
 
-  it('should run a query with the connection directive and filter arguments and write the result to the correct store key', () => {
+  itAsync('should run a query with the connection directive and filter arguments and write the result to the correct store key', (resolve, reject) => {
     const query = gql`
       query books($order: string) {
         books(skip: 0, limit: 2, order: $order)
@@ -2949,7 +2867,7 @@ describe('@connect', () => {
     const link = mockSingleLink({
       request: { query: transformedQuery, variables },
       result: { data: result },
-    });
+    }).setOnError(reject);
 
     const client = new ApolloClient({
       link,
@@ -2959,7 +2877,61 @@ describe('@connect', () => {
     return client.query({ query, variables }).then(actualResult => {
       expect(stripSymbols(actualResult.data)).toEqual(result);
       expect((client.cache as InMemoryCache).extract()).toMatchSnapshot();
+    }).then(resolve, reject);
+  });
+
+  itAsync('should support cache field policies that filter key arguments', (resolve, reject) => {
+    const query = gql`
+      query books($order: string) {
+        books(skip: 0, limit: 2, order: $order) {
+          name
+        }
+      }
+    `;
+    const transformedQuery = gql`
+      query books($order: string) {
+        books(skip: 0, limit: 2, order: $order) {
+          name
+          __typename
+        }
+      }
+    `;
+
+    const result = {
+      books: [
+        {
+          name: 'abcd',
+          __typename: 'Book',
+        },
+      ],
+    };
+
+    const variables = { order: 'popularity' };
+
+    const link = mockSingleLink({
+      request: { query: transformedQuery, variables },
+      result: { data: result },
+    }).setOnError(reject);
+
+    const client = new ApolloClient({
+      link,
+      cache: new InMemoryCache({
+        typePolicies: {
+          Query: {
+            fields: {
+              books: {
+                keyArgs: ["order"],
+              }
+            }
+          }
+        }
+      }),
     });
+
+    return client.query({ query, variables }).then(actualResult => {
+      expect(stripSymbols(actualResult.data)).toEqual(result);
+      expect((client.cache as InMemoryCache).extract()).toMatchSnapshot();
+    }).then(resolve, reject);
   });
 
   describe('default settings', () => {
@@ -2981,11 +2953,12 @@ describe('@connect', () => {
         n: 2,
       },
     };
-    it('allows setting default options for watchQuery', done => {
+
+    itAsync('allows setting default options for watchQuery', (resolve, reject) => {
       const link = mockSingleLink({
         request: { query },
         result: { data: networkFetch },
-      });
+      }).setOnError(reject);
       const client = new ApolloClient({
         link,
         cache: new InMemoryCache({ addTypename: false }),
@@ -3003,22 +2976,23 @@ describe('@connect', () => {
 
       const obs = client.watchQuery({ query });
 
-      subscribeAndCount(done, obs, (handleCount, result) => {
+      subscribeAndCount(reject, obs, (handleCount, result) => {
         const resultData = stripSymbols(result.data);
         if (handleCount === 1) {
           expect(resultData).toEqual(initialData);
         } else if (handleCount === 2) {
           expect(resultData).toEqual(networkFetch);
-          done();
+          resolve();
         }
       });
     });
-    it('allows setting default options for query', () => {
+
+    itAsync('allows setting default options for query', (resolve, reject) => {
       const errors = [{ message: 'failure', name: 'failure' }];
       const link = mockSingleLink({
         request: { query },
         result: { errors },
-      });
+      }).setOnError(reject);
       const client = new ApolloClient({
         link,
         cache: new InMemoryCache({ addTypename: false }),
@@ -3029,9 +3003,10 @@ describe('@connect', () => {
 
       return client.query({ query }).then(result => {
         expect(result.errors).toEqual(errors);
-      });
+      }).then(resolve, reject);
     });
-    it('allows setting default options for mutation', () => {
+
+    itAsync('allows setting default options for mutation', (resolve, reject) => {
       const mutation = gql`
         mutation upVote($id: ID!) {
           upvote(id: $id) {
@@ -3047,7 +3022,7 @@ describe('@connect', () => {
       const link = mockSingleLink({
         request: { query: mutation, variables: { id: 1 } },
         result: { data },
-      });
+      }).setOnError(reject);
 
       const client = new ApolloClient({
         link,
@@ -3059,31 +3034,32 @@ describe('@connect', () => {
 
       return client.mutate({ mutation }).then(result => {
         expect(result.data).toEqual(data);
-      });
+      }).then(resolve, reject);
     });
   });
 });
 
 function clientRoundtrip(
+  resolve: (result: any) => any,
+  reject: (reason: any) => any,
   query: DocumentNode,
   data: ExecutionResult,
   variables?: any,
-  fragmentMatcher?: FragmentMatcherInterface,
+  possibleTypes?: PossibleTypesMap,
 ) {
   const link = mockSingleLink({
     request: { query: cloneDeep(query) },
     result: data,
-  });
-
-  const config = {};
-  if (fragmentMatcher) config.fragmentMatcher = fragmentMatcher;
+  }).setOnError(reject);
 
   const client = new ApolloClient({
     link,
-    cache: new InMemoryCache(config),
+    cache: new InMemoryCache({
+      possibleTypes,
+    }),
   });
 
   return client.query({ query, variables }).then(result => {
     expect(stripSymbols(result.data)).toEqual(data.data);
-  });
+  }).then(resolve, reject);
 }
diff --git a/packages/apollo-client/src/__tests__/fetchMore.ts b/src/__tests__/fetchMore.ts
similarity index 82%
rename from packages/apollo-client/src/__tests__/fetchMore.ts
rename to src/__tests__/fetchMore.ts
--- a/packages/apollo-client/src/__tests__/fetchMore.ts
+++ b/src/__tests__/fetchMore.ts
@@ -1,9 +1,10 @@
-import { InMemoryCache } from 'apollo-cache-inmemory';
 import { assign, cloneDeep } from 'lodash';
 import gql from 'graphql-tag';
 
-import { mockSingleLink } from '../__mocks__/mockLinks';
-import ApolloClient, { NetworkStatus, ObservableQuery } from '../';
+import { mockSingleLink } from '../utilities/testing/mocking/mockLink';
+import { InMemoryCache } from '../cache/inmemory/inMemoryCache';
+import { ApolloClient, NetworkStatus, ObservableQuery } from '../';
+import { itAsync } from '../utilities/testing/itAsync';
 
 describe('updateQuery on a simple query', () => {
   const query = gql`
@@ -25,12 +26,12 @@ describe('updateQuery on a simple query', () => {
     },
   };
 
-  it('triggers new result from updateQuery', () => {
+  itAsync('triggers new result from updateQuery', (resolve, reject) => {
     let latestResult: any = null;
     const link = mockSingleLink({
       request: { query },
       result,
-    });
+    }).setOnError(reject);
 
     const client = new ApolloClient({
       link,
@@ -58,7 +59,8 @@ describe('updateQuery on a simple query', () => {
         });
       })
       .then(() => expect(latestResult.data.entry.value).toBe(2))
-      .then(() => sub.unsubscribe());
+      .then(() => sub.unsubscribe())
+      .then(resolve, reject);
   });
 });
 
@@ -87,7 +89,7 @@ describe('updateQuery on a query with required and optional variables', () => {
     },
   };
 
-  it('triggers new result from updateQuery', () => {
+  itAsync('triggers new result from updateQuery', (resolve, reject) => {
     let latestResult: any = null;
     const link = mockSingleLink({
       request: {
@@ -95,7 +97,7 @@ describe('updateQuery on a query with required and optional variables', () => {
         variables,
       },
       result,
-    });
+    }).setOnError(reject);
 
     const client = new ApolloClient({
       link,
@@ -124,7 +126,8 @@ describe('updateQuery on a query with required and optional variables', () => {
         });
       })
       .then(() => expect(latestResult.data.entry.value).toBe(2))
-      .then(() => sub.unsubscribe());
+      .then(() => sub.unsubscribe())
+      .then(resolve, reject);
   });
 });
 
@@ -199,17 +202,17 @@ describe('fetchMore on an observable query', () => {
   let link: any;
   let sub: any;
 
-  function setup(...mockedResponses: any[]) {
-    link = mockSingleLink(
-      {
-        request: {
-          query,
-          variables,
-        },
-        result,
+  function setup(
+    reject: (reason: any) => any,
+    ...mockedResponses: any[]
+  ) {
+    link = mockSingleLink({
+      request: {
+        query,
+        variables,
       },
-      ...mockedResponses,
-    );
+      result,
+    }, ...mockedResponses).setOnError(reject);
 
     client = new ApolloClient({
       link,
@@ -235,9 +238,9 @@ describe('fetchMore on an observable query', () => {
     sub = null;
   }
 
-  it('triggers new result with new variables', () => {
+  itAsync('triggers new result withAsync new variables', (resolve, reject) => {
     latestResult = null;
-    return setup({
+    return setup(reject, {
       request: {
         query,
         variables: variablesMore,
@@ -270,12 +273,13 @@ describe('fetchMore on an observable query', () => {
           expect(comments[i - 1].text).toEqual(`comment ${i}`);
         }
         unsetup();
-      });
+      })
+      .then(resolve, reject);
   });
 
-  it('basic fetchMore results merging', () => {
+  itAsync('basic fetchMore results merging', (resolve, reject) => {
     latestResult = null;
-    return setup({
+    return setup(reject, {
       request: {
         query,
         variables: variablesMore,
@@ -304,12 +308,13 @@ describe('fetchMore on an observable query', () => {
           expect(comments[i - 1].text).toEqual(`comment ${i}`);
         }
         unsetup();
-      });
+      })
+      .then(resolve, reject);
   });
 
-  it('fetching more with a different query', () => {
+  itAsync('fetching more with a different query', (resolve, reject) => {
     latestResult = null;
-    return setup({
+    return setup(reject, {
       request: {
         query: query2,
         variables: variables2,
@@ -340,18 +345,16 @@ describe('fetchMore on an observable query', () => {
           expect(comments[i - 1].text).toEqual(`new comment ${i}`);
         }
         unsetup();
-      });
+      })
+      .then(resolve, reject);
   });
 
-  it('will set the `network` status to `fetchMore`', done => {
-    link = mockSingleLink(
-      { request: { query, variables }, result, delay: 5 },
-      {
-        request: { query, variables: variablesMore },
-        result: resultMore,
-        delay: 5,
-      },
-    );
+  itAsync('will set the `network` status to `fetchMore`', (resolve, reject) => {
+    link = mockSingleLink({ request: { query, variables }, result, delay: 5 }, {
+      request: { query, variables: variablesMore },
+      result: resultMore,
+      delay: 5,
+    }).setOnError(reject);
 
     client = new ApolloClient({
       link,
@@ -394,27 +397,24 @@ describe('fetchMore on an observable query', () => {
           case 3:
             expect(networkStatus).toBe(NetworkStatus.ready);
             expect((data as any).entry.comments.length).toBe(20);
-            done();
+            resolve();
             break;
           default:
-            done.fail(new Error('`next` called too many times'));
+            reject(new Error('`next` called too many times'));
         }
       },
-      error: error => done.fail(error),
-      complete: () => done.fail(new Error('Should not have completed')),
+      error: error => reject(error),
+      complete: () => reject(new Error('Should not have completed')),
     });
   });
 
-  it('will not get an error from `fetchMore` if thrown', done => {
+  itAsync('will not get an error from `fetchMore` if thrown', (resolve, reject) => {
     const fetchMoreError = new Error('Uh, oh!');
-    link = mockSingleLink(
-      { request: { query, variables }, result, delay: 5 },
-      {
-        request: { query, variables: variablesMore },
-        error: fetchMoreError,
-        delay: 5,
-      },
-    );
+    link = mockSingleLink({ request: { query, variables }, result, delay: 5 }, {
+      request: { query, variables: variablesMore },
+      error: fetchMoreError,
+      delay: 5,
+    }).setOnError(reject);
 
     client = new ApolloClient({
       link,
@@ -457,25 +457,25 @@ describe('fetchMore on an observable query', () => {
           default:
             expect(networkStatus).toBe(NetworkStatus.ready);
             expect((data as any).entry.comments.length).toBe(10);
-            done();
+            resolve();
             break;
         }
       },
       error: () => {
-        done.fail(new Error('`error` called when it wasn’t supposed to be.'));
+        reject(new Error('`error` called when it wasn’t supposed to be.'));
       },
       complete: () => {
-        done.fail(
+        reject(
           new Error('`complete` called when it wasn’t supposed to be.'),
         );
       },
     });
   });
 
-  it('will not leak fetchMore query', () => {
+  itAsync('will not leak fetchMore query', (resolve, reject) => {
     latestResult = null;
     var beforeQueryCount;
-    return setup({
+    return setup(reject, {
       request: {
         query,
         variables: variablesMore,
@@ -504,7 +504,8 @@ describe('fetchMore on an observable query', () => {
         ).length;
         expect(afterQueryCount).toBe(beforeQueryCount);
         unsetup();
-      });
+      })
+      .then(resolve, reject);
   });
 });
 
@@ -568,17 +569,17 @@ describe('fetchMore on an observable query with connection', () => {
   let link: any;
   let sub: any;
 
-  function setup(...mockedResponses: any[]) {
-    link = mockSingleLink(
-      {
-        request: {
-          query: transformedQuery,
-          variables,
-        },
-        result,
+  function setup(
+    reject: (reason: any) => any,
+    ...mockedResponses: any[]
+  ) {
+    link = mockSingleLink({
+      request: {
+        query: transformedQuery,
+        variables,
       },
-      ...mockedResponses,
-    );
+      result,
+    }, ...mockedResponses).setOnError(reject);
 
     client = new ApolloClient({
       link,
@@ -604,9 +605,9 @@ describe('fetchMore on an observable query with connection', () => {
     sub = null;
   }
 
-  it('fetchMore with connection results merging', () => {
+  itAsync('fetchMore with connection results merging', (resolve, reject) => {
     latestResult = null;
-    return setup({
+    return setup(reject, {
       request: {
         query: transformedQuery,
         variables: variablesMore,
@@ -635,18 +636,16 @@ describe('fetchMore on an observable query with connection', () => {
           expect(comments[i - 1].text).toBe(`comment ${i}`);
         }
         unsetup();
-      });
+      })
+      .then(resolve, reject);
   });
 
-  it('will set the network status to `fetchMore`', done => {
-    link = mockSingleLink(
-      { request: { query: transformedQuery, variables }, result, delay: 5 },
-      {
-        request: { query: transformedQuery, variables: variablesMore },
-        result: resultMore,
-        delay: 5,
-      },
-    );
+  itAsync('will set the network status to `fetchMore`', (resolve, reject) => {
+    link = mockSingleLink({ request: { query: transformedQuery, variables }, result, delay: 5 }, {
+      request: { query: transformedQuery, variables: variablesMore },
+      result: resultMore,
+      delay: 5,
+    }).setOnError(reject);
 
     client = new ApolloClient({
       link,
@@ -689,27 +688,24 @@ describe('fetchMore on an observable query with connection', () => {
           case 3:
             expect(networkStatus).toBe(NetworkStatus.ready);
             expect((data as any).entry.comments.length).toBe(20);
-            done();
+            resolve();
             break;
           default:
-            done.fail(new Error('`next` called too many times'));
+            reject(new Error('`next` called too many times'));
         }
       },
-      error: error => done.fail(error),
-      complete: () => done.fail(new Error('Should not have completed')),
+      error: error => reject(error),
+      complete: () => reject(new Error('Should not have completed')),
     });
   });
 
-  it('will not get an error from `fetchMore` if thrown', done => {
+  itAsync('will not get an error from `fetchMore` if thrown', (resolve, reject) => {
     const fetchMoreError = new Error('Uh, oh!');
-    link = mockSingleLink(
-      { request: { query: transformedQuery, variables }, result, delay: 5 },
-      {
-        request: { query: transformedQuery, variables: variablesMore },
-        error: fetchMoreError,
-        delay: 5,
-      },
-    );
+    link = mockSingleLink({ request: { query: transformedQuery, variables }, result, delay: 5 }, {
+      request: { query: transformedQuery, variables: variablesMore },
+      error: fetchMoreError,
+      delay: 5,
+    }).setOnError(reject);
 
     client = new ApolloClient({
       link,
@@ -752,14 +748,14 @@ describe('fetchMore on an observable query with connection', () => {
           default:
             expect(networkStatus).toBe(NetworkStatus.ready);
             expect((data as any).entry.comments.length).toBe(10);
-            done();
+            resolve();
         }
       },
       error: () => {
-        done.fail(new Error('`error` called when it wasn’t supposed to be.'));
+        reject(new Error('`error` called when it wasn’t supposed to be.'));
       },
       complete: () => {
-        done.fail(
+        reject(
           new Error('`complete` called when it wasn’t supposed to be.'),
         );
       },
diff --git a/packages/apollo-client/src/__tests__/graphqlSubscriptions.ts b/src/__tests__/graphqlSubscriptions.ts
similarity index 93%
rename from packages/apollo-client/src/__tests__/graphqlSubscriptions.ts
rename to src/__tests__/graphqlSubscriptions.ts
--- a/packages/apollo-client/src/__tests__/graphqlSubscriptions.ts
+++ b/src/__tests__/graphqlSubscriptions.ts
@@ -1,12 +1,12 @@
 import gql from 'graphql-tag';
-import { InMemoryCache } from 'apollo-cache-inmemory';
-
-import { mockObservableLink, MockedSubscription } from '../__mocks__/mockLinks';
-
-import ApolloClient from '../';
 
+import {
+  mockObservableLink,
+  MockedSubscription
+} from '../utilities/testing/mocking/mockSubscriptionLink';
+import { InMemoryCache } from '../cache/inmemory/inMemoryCache';
+import { ApolloClient } from '../';
 import { QueryManager } from '../core/QueryManager';
-import { DataStore } from '../data/store';
 
 describe('GraphQL Subscriptions', () => {
   const results = [
@@ -131,7 +131,7 @@ describe('GraphQL Subscriptions', () => {
     const link = mockObservableLink(sub1);
     const queryManager = new QueryManager({
       link,
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
     });
 
     const obs = queryManager.startGraphQLSubscription(options);
@@ -169,7 +169,7 @@ describe('GraphQL Subscriptions', () => {
     let numResults = 0;
     const queryManager = new QueryManager({
       link,
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
     });
 
     // tslint:disable-next-line
@@ -214,7 +214,7 @@ describe('GraphQL Subscriptions', () => {
     const link = mockObservableLink(sub1);
     const queryManager = new QueryManager({
       link,
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
     });
 
     const obs = queryManager.startGraphQLSubscription(options);
diff --git a/packages/apollo-client/src/__tests__/local-state/__snapshots__/general.ts.snap b/src/__tests__/local-state/__snapshots__/general.ts.snap
similarity index 100%
rename from packages/apollo-client/src/__tests__/local-state/__snapshots__/general.ts.snap
rename to src/__tests__/local-state/__snapshots__/general.ts.snap
diff --git a/packages/apollo-client/src/__tests__/local-state/export.ts b/src/__tests__/local-state/export.ts
similarity index 97%
rename from packages/apollo-client/src/__tests__/local-state/export.ts
rename to src/__tests__/local-state/export.ts
--- a/packages/apollo-client/src/__tests__/local-state/export.ts
+++ b/src/__tests__/local-state/export.ts
@@ -1,10 +1,12 @@
 import gql from 'graphql-tag';
-
-import ApolloClient from '../..';
-import { InMemoryCache } from 'apollo-cache-inmemory';
-import { ApolloLink, Observable } from 'apollo-link';
 import { print } from 'graphql/language/printer';
 
+import { Observable } from '../../utilities/observables/Observable';
+import { itAsync } from '../../utilities/testing/itAsync';
+import { ApolloLink } from '../../link/core/ApolloLink';
+import { ApolloClient } from '../..';
+import { InMemoryCache } from '../../cache/inmemory/inMemoryCache';
+
 describe('@client @export tests', () => {
   it(
     'should not break @client only queries when the @export directive is ' +
@@ -324,10 +326,10 @@ describe('@client @export tests', () => {
     });
   });
 
-  it(
+  itAsync(
     'should support setting an @client @export variable, loaded from the ' +
       'cache, on a virtual field that is combined into a remote query.',
-    done => {
+    (resolve, reject) => {
       const query = gql`
         query postRequiringReview($reviewerId: Int!) {
           postRequiringReview {
@@ -360,7 +362,7 @@ describe('@client @export tests', () => {
             reviewerDetails,
           },
         });
-      });
+      }).setOnError(reject);
 
       const cache = new InMemoryCache();
       const client = new ApolloClient({
@@ -374,6 +376,7 @@ describe('@client @export tests', () => {
           postRequiringReview: {
             loggedInReviewerId,
             __typename: 'Post',
+            id: 10,
           },
         },
       });
@@ -387,8 +390,7 @@ describe('@client @export tests', () => {
           },
           reviewerDetails,
         });
-        done();
-      });
+      }).then(resolve, reject);
     },
   );
 
diff --git a/packages/apollo-client/src/__tests__/local-state/general.ts b/src/__tests__/local-state/general.ts
similarity index 90%
rename from packages/apollo-client/src/__tests__/local-state/general.ts
rename to src/__tests__/local-state/general.ts
--- a/packages/apollo-client/src/__tests__/local-state/general.ts
+++ b/src/__tests__/local-state/general.ts
@@ -1,15 +1,15 @@
 import gql from 'graphql-tag';
 import { DocumentNode, GraphQLError } from 'graphql';
-import { introspectionQuery } from 'graphql/utilities';
+import { getIntrospectionQuery } from 'graphql/utilities';
 
-import ApolloClient from '../..';
-import { ApolloCache } from 'apollo-cache';
-import {
-  InMemoryCache,
-  IntrospectionFragmentMatcher,
-} from 'apollo-cache-inmemory';
-import { ApolloLink, Observable, Operation } from 'apollo-link';
-import { hasDirectives } from 'apollo-utilities';
+import { Observable } from '../../utilities/observables/Observable';
+import { ApolloLink } from '../../link/core/ApolloLink';
+import { Operation } from '../../link/core/types';
+import { ApolloClient } from '../..';
+import { ApolloCache } from '../../cache/core/cache';
+import { InMemoryCache } from '../../cache/inmemory/inMemoryCache';
+import { hasDirectives } from '../../utilities/graphql/directives';
+import { itAsync } from '../../utilities/testing/itAsync';
 
 describe('General functionality', () => {
   it('should not impact normal non-@client use', () => {
@@ -71,7 +71,7 @@ describe('General functionality', () => {
 
   it('should not interfere with server introspection queries', () => {
     const query = gql`
-      ${introspectionQuery}
+      ${getIntrospectionQuery()}
     `;
 
     const error = new GraphQLError('no introspection result found');
@@ -227,19 +227,9 @@ describe('General functionality', () => {
 
     const client = new ApolloClient({
       cache: new InMemoryCache({
-        fragmentMatcher: new IntrospectionFragmentMatcher({
-          introspectionQueryResultData: {
-            __schema: {
-              types: [
-                {
-                  kind: 'UnionTypeDefinition',
-                  name: 'Foo',
-                  possibleTypes: [{ name: 'Bar' }, { name: 'Baz' }],
-                },
-              ],
-            },
-          },
-        }),
+        possibleTypes: {
+          Foo: ['Bar', 'Baz'],
+        },
       }),
       link,
       resolvers,
@@ -314,10 +304,10 @@ describe('Cache manipulation', () => {
       });
   });
 
-  it(
+  itAsync(
     'should be able to write to the cache with a local mutation and have ' +
       'things rerender automatically',
-    done => {
+    (resolve, reject) => {
       const query = gql`
         {
           field @client
@@ -359,7 +349,7 @@ describe('Cache manipulation', () => {
 
           if (count === 2) {
             expect({ ...data }).toMatchObject({ field: 1 });
-            done();
+            resolve();
           }
         },
       });
@@ -416,7 +406,7 @@ describe('Cache manipulation', () => {
       });
   });
 
-  it("should read @client fields from cache on refetch (#4741)", function (done) {
+  itAsync("should read @client fields from cache on refetch (#4741)", (resolve, reject) => {
     const query = gql`
       query FetchInitialData {
         serverData {
@@ -480,7 +470,7 @@ describe('Cache manipulation', () => {
             ],
           });
         } else {
-          done();
+          resolve();
         }
       },
     });
@@ -488,7 +478,7 @@ describe('Cache manipulation', () => {
 });
 
 describe('Sample apps', () => {
-  it('should support a simple counter app using local state', done => {
+  itAsync('should support a simple counter app using local state', (resolve, reject) => {
     const query = gql`
       query GetCount {
         count @client
@@ -565,7 +555,7 @@ describe('Sample apps', () => {
           try {
             expect({ ...data }).toMatchObject({ count: 0, lastCount: 1 });
           } catch (e) {
-            done.fail(e);
+            reject(e);
           }
           client.mutate({ mutation: increment, variables: { amount: 2 } });
         }
@@ -574,7 +564,7 @@ describe('Sample apps', () => {
           try {
             expect({ ...data }).toMatchObject({ count: 2, lastCount: 1 });
           } catch (e) {
-            done.fail(e);
+            reject(e);
           }
           client.mutate({ mutation: decrement, variables: { amount: 1 } });
         }
@@ -582,17 +572,17 @@ describe('Sample apps', () => {
           try {
             expect({ ...data }).toMatchObject({ count: 1, lastCount: 1 });
           } catch (e) {
-            done.fail(e);
+            reject(e);
           }
-          done();
+          resolve();
         }
       },
-      error: e => done.fail(e),
-      complete: done.fail,
+      error: e => reject(e),
+      complete: reject,
     });
   });
 
-  it('should support a simple todo app using local state', done => {
+  itAsync('should support a simple todo app using local state', (resolve, reject) => {
     const query = gql`
       query GetTasks {
         todos @client {
@@ -669,7 +659,7 @@ describe('Sample apps', () => {
               __typename: 'Todo',
             },
           ]);
-          done();
+          resolve();
         }
       },
     });
@@ -677,7 +667,7 @@ describe('Sample apps', () => {
 });
 
 describe('Combining client and server state/operations', () => {
-  it('should merge remote and local state', done => {
+  itAsync('should merge remote and local state', (resolve, reject) => {
     const query = gql`
       query list {
         list(name: "my list") {
@@ -756,11 +746,11 @@ describe('Combining client and server state/operations', () => {
         if (count === 1) {
           expect((response.data as any).list.items[0].isSelected).toBe(true);
           expect((response.data as any).list.items[1].isSelected).toBe(false);
-          done();
+          resolve();
         }
         count++;
       },
-      error: done.fail,
+      error: reject,
     });
     const variables = { id: 1 };
     const mutation = gql`
@@ -774,7 +764,7 @@ describe('Combining client and server state/operations', () => {
     }, 10);
   });
 
-  it('should correctly propagate an error from a client resolver', async done => {
+  itAsync('should correctly propagate an error from a client resolver', async (resolve, reject) => {
     const data = {
       list: {
         __typename: 'List',
@@ -819,7 +809,7 @@ describe('Combining client and server state/operations', () => {
 
     try {
       await client.query({ query, variables });
-      done.fail('Should have thrown!');
+      reject('Should have thrown!');
     } catch (e) {
       // Test Passed!
       expect(() => {
@@ -829,7 +819,7 @@ describe('Combining client and server state/operations', () => {
 
     try {
       await client.mutate({ mutation, variables });
-      done.fail('Should have thrown!');
+      reject('Should have thrown!');
     } catch (e) {
       // Test Passed!
       expect(() => {
@@ -837,10 +827,10 @@ describe('Combining client and server state/operations', () => {
       }).toThrowErrorMatchingSnapshot();
     }
 
-    done();
+    resolve();
   });
 
-  it('should handle a simple query with both server and client fields', done => {
+  itAsync('should handle a simple query with both server and client fields', (resolve, reject) => {
     const query = gql`
       query GetCount {
         count @client
@@ -869,12 +859,12 @@ describe('Combining client and server state/operations', () => {
     client.watchQuery({ query }).subscribe({
       next: ({ data }) => {
         expect({ ...data }).toMatchObject({ count: 0, lastCount: 1 });
-        done();
+        resolve();
       },
     });
   });
 
-  it('should support nested quering of both server and client fields', done => {
+  itAsync('should support nested querying of both server and client fields', (resolve, reject) => {
     const query = gql`
       query GetUser {
         user {
@@ -888,7 +878,17 @@ describe('Combining client and server state/operations', () => {
     const link = new ApolloLink(operation => {
       expect(operation.operationName).toBe('GetUser');
       return Observable.of({
-        data: { user: { lastName: 'Doe', __typename: 'User' } },
+        data: {
+          user: {
+            __typename: 'User',
+            // We need an id (or a keyFields policy) because, if the User
+            // object is not identifiable, the call to cache.writeData
+            // below will simply replace the existing data rather than
+            // merging the new data with the existing data.
+            id: 123,
+            lastName: 'Doe',
+          },
+        },
       });
     });
 
@@ -902,13 +902,14 @@ describe('Combining client and server state/operations', () => {
       data: {
         user: {
           __typename: 'User',
+          id: 123,
           firstName: 'John',
         },
       },
     });
 
     client.watchQuery({ query }).subscribe({
-      next: ({ data }: any) => {
+      next({ data }: any) {
         const { user } = data;
         try {
           expect(user).toMatchObject({
@@ -917,14 +918,14 @@ describe('Combining client and server state/operations', () => {
             __typename: 'User',
           });
         } catch (e) {
-          done.fail(e);
+          reject(e);
         }
-        done();
+        resolve();
       },
     });
   });
 
-  it('should combine both server and client mutations', done => {
+  itAsync('should combine both server and client mutations', (resolve, reject) => {
     const query = gql`
       query SampleQuery {
         count @client
@@ -1022,7 +1023,7 @@ describe('Combining client and server state/operations', () => {
             __typename: 'User',
             firstName: 'Harry',
           });
-          done();
+          resolve();
         }
       },
     });
diff --git a/packages/apollo-client/src/__tests__/local-state/resolvers.ts b/src/__tests__/local-state/resolvers.ts
similarity index 92%
rename from packages/apollo-client/src/__tests__/local-state/resolvers.ts
rename to src/__tests__/local-state/resolvers.ts
--- a/packages/apollo-client/src/__tests__/local-state/resolvers.ts
+++ b/src/__tests__/local-state/resolvers.ts
@@ -1,21 +1,22 @@
 import gql from 'graphql-tag';
 import { DocumentNode, ExecutionResult } from 'graphql';
 import { assign } from 'lodash';
-import { InMemoryCache } from 'apollo-cache-inmemory';
-import { ApolloLink, Observable } from 'apollo-link';
 
-import ApolloClient from '../..';
-import mockQueryManager from '../../__mocks__/mockQueryManager';
-import { Observer } from '../../util/Observable';
-import wrap from '../../util/wrap';
+import { Observable, Observer } from '../../utilities/observables/Observable';
+import { ApolloLink } from '../../link/core/ApolloLink';
+import { ApolloClient } from '../..';
+import mockQueryManager from '../../utilities/testing/mocking/mockQueryManager';
+import wrap from '../../utilities/testing/wrap';
+import { itAsync } from '../../utilities/testing/itAsync';
 import { ApolloQueryResult, Resolvers } from '../../core/types';
 import { WatchQueryOptions } from '../../core/watchQueryOptions';
 import { LocalState } from '../../core/LocalState';
+import { InMemoryCache } from '../../cache/inmemory/inMemoryCache';
 
 // Helper method that sets up a mockQueryManager and then passes on the
 // results to an observer.
 const assertWithObserver = ({
-  done,
+  reject,
   resolvers,
   query,
   serverQuery,
@@ -26,7 +27,7 @@ const assertWithObserver = ({
   delay,
   observer,
 }: {
-  done: jest.DoneCallback;
+  reject: (reason: any) => any;
   resolvers?: Resolvers;
   query: DocumentNode;
   serverQuery?: DocumentNode;
@@ -37,7 +38,7 @@ const assertWithObserver = ({
   delay?: number;
   observer: Observer<ApolloQueryResult<any>>;
 }) => {
-  const queryManager = mockQueryManager({
+  const queryManager = mockQueryManager(reject, {
     request: { query: serverQuery || query, variables },
     result: serverResult,
     error,
@@ -53,13 +54,13 @@ const assertWithObserver = ({
     queryOptions,
   ) as WatchQueryOptions;
   return queryManager.watchQuery<any>(finalOptions).subscribe({
-    next: wrap(done, observer.next!),
+    next: wrap(reject, observer.next!),
     error: observer.error,
   });
 };
 
 describe('Basic resolver capabilities', () => {
-  it('should run resolvers for @client queries', done => {
+  itAsync('should run resolvers for @client queries', (resolve, reject) => {
     const query = gql`
       query Test {
         foo @client {
@@ -75,7 +76,7 @@ describe('Basic resolver capabilities', () => {
     };
 
     assertWithObserver({
-      done,
+      reject,
       resolvers,
       query,
       observer: {
@@ -83,15 +84,15 @@ describe('Basic resolver capabilities', () => {
           try {
             expect(data).toEqual({ foo: { bar: true } });
           } catch (error) {
-            done.fail(error);
+            reject(error);
           }
-          done();
+          resolve();
         },
       },
     });
   });
 
-  it('should handle queries with a mix of @client and server fields', done => {
+  itAsync('should handle queries with a mix of @client and server fields', (resolve, reject) => {
     const query = gql`
       query Mixed {
         foo @client {
@@ -118,7 +119,7 @@ describe('Basic resolver capabilities', () => {
     };
 
     assertWithObserver({
-      done,
+      reject,
       resolvers,
       query,
       serverQuery,
@@ -128,15 +129,15 @@ describe('Basic resolver capabilities', () => {
           try {
             expect(data).toEqual({ foo: { bar: true }, bar: { baz: true } });
           } catch (error) {
-            done.fail(error);
+            reject(error);
           }
-          done();
+          resolve();
         },
       },
     });
   });
 
-  it('should handle a mix of @client fields with fragments and server fields', done => {
+  itAsync('should handle a mix of @client fields with fragments and server fields', (resolve, reject) => {
     const query = gql`
       fragment client on ClientData {
         bar
@@ -168,7 +169,7 @@ describe('Basic resolver capabilities', () => {
     };
 
     assertWithObserver({
-      done,
+      reject,
       resolvers,
       query,
       serverQuery,
@@ -181,15 +182,15 @@ describe('Basic resolver capabilities', () => {
               bar: { baz: true },
             });
           } catch (error) {
-            done.fail(error);
+            reject(error);
           }
-          done();
+          resolve();
         },
       },
     });
   });
 
-  it('should have access to query variables when running @client resolvers', done => {
+  itAsync('should have access to query variables when running @client resolvers', (resolve, reject) => {
     const query = gql`
       query WithVariables($id: ID!) {
         foo @client {
@@ -208,7 +209,7 @@ describe('Basic resolver capabilities', () => {
     };
 
     assertWithObserver({
-      done,
+      reject,
       resolvers,
       query,
       variables: { id: 1 },
@@ -217,15 +218,15 @@ describe('Basic resolver capabilities', () => {
           try {
             expect(data).toEqual({ foo: { bar: 1 } });
           } catch (error) {
-            done.fail(error);
+            reject(error);
           }
-          done();
+          resolve();
         },
       },
     });
   });
 
-  it('should pass context to @client resolvers', done => {
+  itAsync('should pass context to @client resolvers', (resolve, reject) => {
     const query = gql`
       query WithContext {
         foo @client {
@@ -244,7 +245,7 @@ describe('Basic resolver capabilities', () => {
     };
 
     assertWithObserver({
-      done,
+      reject,
       resolvers,
       query,
       queryOptions: { context: { id: 1 } },
@@ -253,18 +254,18 @@ describe('Basic resolver capabilities', () => {
           try {
             expect(data).toEqual({ foo: { bar: 1 } });
           } catch (error) {
-            done.fail(error);
+            reject(error);
           }
-          done();
+          resolve();
         },
       },
     });
   });
 
-  it(
+  itAsync(
     'should combine local @client resolver results with server results, for ' +
       'the same field',
-    done => {
+    (resolve, reject) => {
       const query = gql`
         query author {
           author {
@@ -295,7 +296,7 @@ describe('Basic resolver capabilities', () => {
       };
 
       assertWithObserver({
-        done,
+        reject,
         resolvers,
         query,
         serverQuery,
@@ -324,16 +325,16 @@ describe('Basic resolver capabilities', () => {
                 },
               });
             } catch (error) {
-              done.fail(error);
+              reject(error);
             }
-            done();
+            resolve();
           },
         },
       });
     },
   );
 
-  it('should handle resolvers that work with booleans properly', done => {
+  itAsync('should handle resolvers that work with booleans properly', (resolve, reject) => {
     const query = gql`
       query CartDetails {
         isInCart @client
@@ -358,7 +359,7 @@ describe('Basic resolver capabilities', () => {
         expect({ ...data }).toMatchObject({
           isInCart: false,
         });
-        done();
+        resolve();
       });
   });
 
@@ -702,7 +703,7 @@ describe('Writing cache data from resolvers', () => {
 });
 
 describe('Resolving field aliases', () => {
-  it('should run resolvers for missing client queries with aliased field', done => {
+  itAsync('should run resolvers for missing client queries with aliased field', (resolve, reject) => {
     // expect.assertions(1);
     const query = gql`
       query Aliased {
@@ -738,17 +739,17 @@ describe('Resolving field aliases', () => {
           baz: { foo: true, __typename: 'Baz' },
         });
       } catch (e) {
-        done.fail(e);
+        reject(e);
         return;
       }
-      done();
-    }, done.fail);
+      resolve();
+    }, reject);
   });
 
-  it(
+  itAsync(
     'should run resolvers for client queries when aliases are in use on ' +
       'the @client-tagged node',
-    done => {
+    (resolve, reject) => {
       const aliasedQuery = gql`
         query Test {
           fie: foo @client {
@@ -764,7 +765,7 @@ describe('Resolving field aliases', () => {
           Query: {
             foo: () => ({ bar: true, __typename: 'Foo' }),
             fie: () => {
-              done.fail(
+              reject(
                 "Called the resolver using the alias' name, instead of " +
                   'the correct resolver name.',
               );
@@ -775,12 +776,12 @@ describe('Resolving field aliases', () => {
 
       client.query({ query: aliasedQuery }).then(({ data }) => {
         expect(data).toEqual({ fie: { bar: true, __typename: 'Foo' } });
-        done();
-      }, done.fail);
+        resolve();
+      }, reject);
     },
   );
 
-  it('should respect aliases for *nested fields* on the @client-tagged node', done => {
+  itAsync('should respect aliases for *nested fields* on the @client-tagged node', (resolve, reject) => {
     const aliasedQuery = gql`
       query Test {
         fie: foo @client {
@@ -803,7 +804,7 @@ describe('Resolving field aliases', () => {
         Query: {
           foo: () => ({ bar: true, __typename: 'Foo' }),
           fie: () => {
-            done.fail(
+            reject(
               "Called the resolver using the alias' name, instead of " +
                 'the correct resolver name.',
             );
@@ -817,8 +818,8 @@ describe('Resolving field aliases', () => {
         fie: { fum: true, __typename: 'Foo' },
         baz: { foo: true, __typename: 'Baz' },
       });
-      done();
-    }, done.fail);
+      resolve();
+    }, reject);
   });
 
   it(
@@ -1059,10 +1060,10 @@ describe('Force local resolvers', () => {
     },
   );
 
-  it(
+  itAsync(
     'should force the running of local resolvers marked with ' +
     '`@client(always: true)` when using `ApolloClient.watchQuery`',
-    (done) => {
+    (resolve, reject) => {
       const query = gql`
         query IsUserLoggedIn {
           isUserLoggedIn @client(always: true)
@@ -1101,7 +1102,7 @@ describe('Force local resolvers', () => {
                   // Result is loaded from the cache since the resolver
                   // isn't being forced.
                   expect(callCount).toBe(2);
-                  done();
+                  resolve();
                 }
               });
             }
@@ -1156,7 +1157,7 @@ describe('Force local resolvers', () => {
 });
 
 describe('Async resolvers', () => {
-  it('should support async @client resolvers', async (done) => {
+  itAsync('should support async @client resolvers', async (resolve, reject) => {
     const query = gql`
       query Member {
         isLoggedIn @client
@@ -1176,12 +1177,12 @@ describe('Async resolvers', () => {
 
     const { data: { isLoggedIn } } = await client.query({ query });
     expect(isLoggedIn).toBe(true);
-    return done();
+    return resolve();
   });
 
-  it(
+  itAsync(
     'should support async @client resolvers mixed with remotely resolved data',
-    async (done) => {
+    async (resolve, reject) => {
       const query = gql`
         query Member {
           member {
@@ -1228,8 +1229,8 @@ describe('Async resolvers', () => {
       expect(member.name).toBe(testMember.name);
       expect(member.isLoggedIn).toBe(testMember.isLoggedIn);
       expect(member.sessionCount).toBe(testMember.sessionCount);
-      return done();
-    }
+      return resolve();
+    },
   );
 });
 
diff --git a/packages/apollo-client/src/__tests__/local-state/subscriptions.ts b/src/__tests__/local-state/subscriptions.ts
similarity index 88%
rename from packages/apollo-client/src/__tests__/local-state/subscriptions.ts
rename to src/__tests__/local-state/subscriptions.ts
--- a/packages/apollo-client/src/__tests__/local-state/subscriptions.ts
+++ b/src/__tests__/local-state/subscriptions.ts
@@ -1,8 +1,9 @@
 import gql from 'graphql-tag';
 
-import ApolloClient from '../..';
-import { InMemoryCache } from 'apollo-cache-inmemory';
-import { ApolloLink, Observable } from 'apollo-link';
+import { Observable } from '../../utilities/observables/Observable';
+import { ApolloLink } from '../../link/core/ApolloLink';
+import { ApolloClient } from '../..';
+import { InMemoryCache } from '../../cache/inmemory/inMemoryCache';
 
 describe('Basic functionality', () => {
   it('should not break subscriptions', done => {
diff --git a/packages/apollo-client/src/__tests__/mutationResults.ts b/src/__tests__/mutationResults.ts
similarity index 85%
rename from packages/apollo-client/src/__tests__/mutationResults.ts
rename to src/__tests__/mutationResults.ts
--- a/packages/apollo-client/src/__tests__/mutationResults.ts
+++ b/src/__tests__/mutationResults.ts
@@ -1,13 +1,13 @@
-import { ApolloLink, Observable } from 'apollo-link';
 import { cloneDeep } from 'lodash';
 import gql from 'graphql-tag';
-import { InMemoryCache } from 'apollo-cache-inmemory';
 
-import { mockSingleLink } from '../__mocks__/mockLinks';
-import ApolloClient from '..';
-
-import { Subscription } from '../util/Observable';
-import { withWarning } from '../util/wrap';
+import { Observable, Subscription } from '../utilities/observables/Observable';
+import { ApolloLink } from '../link/core/ApolloLink';
+import { mockSingleLink } from '../utilities/testing/mocking/mockLink';
+import { ApolloClient } from '..';
+import { InMemoryCache } from '../cache/inmemory/inMemoryCache';
+import { withWarning } from '../utilities/testing/wrap';
+import { itAsync } from '../utilities/testing/itAsync';
 
 describe('mutation results', () => {
   const query = gql`
@@ -120,14 +120,14 @@ describe('mutation results', () => {
   let client: ApolloClient<any>;
   let link: any;
 
-  function setupObsHandle(...mockedResponses: any[]) {
-    link = mockSingleLink(
-      {
-        request: { query: queryWithTypename } as any,
-        result,
-      },
-      ...mockedResponses,
-    );
+  function setupObsHandle(
+    reject: (reason: any) => any,
+    ...mockedResponses: any[]
+  ) {
+    link = mockSingleLink({
+      request: { query: queryWithTypename } as any,
+      result,
+    }, ...mockedResponses).setOnError(reject);
 
     client = new ApolloClient({
       link,
@@ -138,6 +138,8 @@ describe('mutation results', () => {
           }
           return null;
         },
+        // Passing an empty map enables warnings about missing fields:
+        possibleTypes: {},
       }),
     });
 
@@ -147,15 +149,16 @@ describe('mutation results', () => {
     });
   }
 
-  function setupDelayObsHandle(delay: number, ...mockedResponses: any[]) {
-    link = mockSingleLink(
-      {
-        request: { query: queryWithTypename } as any,
-        result,
-        delay,
-      },
-      ...mockedResponses,
-    );
+  function setupDelayObsHandle(
+    reject: (reason: any) => any,
+    delay: number,
+    ...mockedResponses: any[]
+  ) {
+    link = mockSingleLink({
+      request: { query: queryWithTypename } as any,
+      result,
+      delay,
+    }, ...mockedResponses).setOnError(reject);
 
     client = new ApolloClient({
       link,
@@ -166,6 +169,8 @@ describe('mutation results', () => {
           }
           return null;
         },
+        // Passing an empty map enables warnings about missing fields:
+        possibleTypes: {},
       }),
     });
 
@@ -175,20 +180,21 @@ describe('mutation results', () => {
     });
   }
 
-  function setup(...mockedResponses: any[]) {
-    const obsHandle = setupObsHandle(...mockedResponses);
+  function setup(
+    reject: (reason: any) => any,
+    ...mockedResponses: any[]
+  ) {
+    const obsHandle = setupObsHandle(reject, ...mockedResponses);
     return obsHandle.result();
   }
 
-  it('correctly primes cache for tests', () => {
-    return setup().then(() =>
-      client.query({
-        query,
-      }),
-    );
+  itAsync('correctly primes cache for tests', (resolve, reject) => {
+    return setup(reject).then(
+      () => client.query({ query })
+    ).then(resolve, reject);
   });
 
-  it('correctly integrates field changes by default', () => {
+  itAsync('correctly integrates field changes by default', (resolve, reject) => {
     const mutation = gql`
       mutation setCompleted {
         setCompleted(todoId: "3") {
@@ -211,7 +217,7 @@ describe('mutation results', () => {
       },
     };
 
-    return setup({
+    return setup(reject, {
       request: { query: mutation },
       result: mutationResult,
     })
@@ -223,9 +229,11 @@ describe('mutation results', () => {
       })
       .then((newResult: any) => {
         expect(newResult.data.todoList.todos[0].completed).toBe(true);
-      });
+      })
+      .then(resolve, reject);
   });
-  it('correctly integrates field changes by default with variables', done => {
+
+  itAsync('correctly integrates field changes by default with variables', (resolve, reject) => {
     const query = gql`
       query getMini($id: ID!) {
         mini(id: $id) {
@@ -245,28 +253,25 @@ describe('mutation results', () => {
       }
     `;
 
-    const link = mockSingleLink(
-      {
-        request: {
-          query,
-          variables: { id: 1 },
-        } as any,
-        delay: 100,
-        result: {
-          data: { mini: { id: 1, cover: 'image', __typename: 'Mini' } },
-        },
+    const link = mockSingleLink({
+      request: {
+        query,
+        variables: { id: 1 },
+      } as any,
+      delay: 100,
+      result: {
+        data: { mini: { id: 1, cover: 'image', __typename: 'Mini' } },
       },
-      {
-        request: {
-          query: mutation,
-          variables: { signature: '1234' },
-        } as any,
-        delay: 150,
-        result: {
-          data: { mini: { id: 1, cover: 'image2', __typename: 'Mini' } },
-        },
+    }, {
+      request: {
+        query: mutation,
+        variables: { signature: '1234' },
+      } as any,
+      delay: 150,
+      result: {
+        data: { mini: { id: 1, cover: 'image2', __typename: 'Mini' } },
       },
-    );
+    }).setOnError(reject);
 
     interface Data {
       mini: { id: number; cover: string; __typename: string };
@@ -298,22 +303,22 @@ describe('mutation results', () => {
 
           setTimeout(() => {
             if (count === 0)
-              done.fail(
+              reject(
                 new Error('mutate did not re-call observable with next value'),
               );
           }, 250);
         }
         if (count === 1) {
           expect(result.data.mini.cover).toBe('image2');
-          done();
+          resolve();
         }
         count++;
       },
-      error: done.fail,
+      error: reject,
     });
   });
 
-  it("should warn when the result fields don't match the query fields", () => {
+  itAsync("should warn when the result fields don't match the query fields", (resolve, reject) => {
     let handle: any;
     let subscriptionHandle: Subscription;
     let counter = 0;
@@ -365,6 +370,7 @@ describe('mutation results', () => {
 
     return withWarning(() => {
       return setup(
+        reject,
         {
           request: { query: queryTodos },
           result: queryTodosResult,
@@ -401,7 +407,8 @@ describe('mutation results', () => {
             },
           });
         })
-        .then(() => subscriptionHandle.unsubscribe());
+        .then(() => subscriptionHandle.unsubscribe())
+        .then(resolve, reject);
     }, /Missing field description/);
   });
 
@@ -431,9 +438,9 @@ describe('mutation results', () => {
       },
     };
 
-    it('analogous of ARRAY_INSERT', () => {
+    itAsync('analogous of ARRAY_INSERT', (resolve, reject) => {
       let subscriptionHandle: Subscription;
-      return setup({
+      return setup(reject, {
         request: { query: mutation },
         result: mutationResult,
       })
@@ -479,10 +486,11 @@ describe('mutation results', () => {
           expect(newResult.data.todoList.todos[0].text).toBe(
             'This one was created with a mutation.',
           );
-        });
+        })
+        .then(resolve, reject);
     });
 
-    it('does not fail if optional query variables are not supplied', () => {
+    itAsync('does not fail if optional query variables are not supplied', (resolve, reject) => {
       let subscriptionHandle: Subscription;
       const mutationWithVars = gql`
         mutation createTodo($requiredVar: String!, $optionalVar: String) {
@@ -501,7 +509,7 @@ describe('mutation results', () => {
         requiredVar: 'x',
         // optionalVar: 'y',
       };
-      return setup({
+      return setup(reject, {
         request: {
           query: mutationWithVars,
           variables,
@@ -554,11 +562,12 @@ describe('mutation results', () => {
           expect(newResult.data.todoList.todos[0].text).toBe(
             'This one was created with a mutation.',
           );
-        });
+        })
+        .then(resolve, reject);
     });
 
-    it('does not fail if the query did not complete correctly', () => {
-      const obsHandle = setupObsHandle({
+    itAsync('does not fail if the query did not complete correctly', (resolve, reject) => {
+      const obsHandle = setupObsHandle(reject, {
         request: { query: mutation },
         result: mutationResult,
       });
@@ -582,11 +591,11 @@ describe('mutation results', () => {
             return state;
           },
         },
-      });
+      }).then(resolve, reject);
     });
 
-    it('does not fail if the query did not finish loading', () => {
-      const obsHandle = setupDelayObsHandle(15, {
+    itAsync('does not fail if the query did not finish loading', (resolve, reject) => {
+      const obsHandle = setupDelayObsHandle(reject, 15, {
         request: { query: mutation },
         result: mutationResult,
       });
@@ -608,11 +617,12 @@ describe('mutation results', () => {
             return state;
           },
         },
-      });
+      }).then(resolve, reject);
     });
 
-    it('does not make next queries fail if a mutation fails', done => {
+    itAsync('does not make next queries fail if a mutation fails', (resolve, reject) => {
       const obsHandle = setupObsHandle(
+        error => { throw error },
         {
           request: { query: mutation },
           result: { errors: [new Error('mock error')] },
@@ -642,7 +652,7 @@ describe('mutation results', () => {
               },
             })
             .then(
-              () => done.fail(new Error('Mutation should have failed')),
+              () => reject(new Error('Mutation should have failed')),
               () =>
                 client.mutate({
                   mutation,
@@ -657,15 +667,15 @@ describe('mutation results', () => {
                 }),
             )
             .then(
-              () => done.fail(new Error('Mutation should have failed')),
+              () => reject(new Error('Mutation should have failed')),
               () => obsHandle.refetch(),
             )
-            .then(() => done(), done.fail);
+            .then(resolve, reject);
         },
       });
     });
 
-    it('error handling in reducer functions', () => {
+    itAsync('error handling in reducer functions', (resolve, reject) => {
       const oldError = console.error;
       const errors: any[] = [];
       console.error = (msg: string) => {
@@ -673,7 +683,7 @@ describe('mutation results', () => {
       };
 
       let subscriptionHandle: Subscription;
-      return setup({
+      return setup(reject, {
         request: { query: mutation },
         result: mutationResult,
       })
@@ -703,11 +713,12 @@ describe('mutation results', () => {
           expect(errors).toHaveLength(1);
           expect(errors[0].message).toBe(`Hello... It's me.`);
           console.error = oldError;
-        });
+        })
+        .then(resolve, reject);
     });
   });
 
-  it('does not fail if one of the previous queries did not complete correctly', done => {
+  itAsync('does not fail if one of the previous queries did not complete correctly', (resolve, reject) => {
     const variableQuery = gql`
       query Echo($message: String) {
         echo(message: $message)
@@ -750,20 +761,16 @@ describe('mutation results', () => {
       },
     };
 
-    link = mockSingleLink(
-      {
-        request: { query: variableQuery, variables: variables1 } as any,
-        result: result1,
-      },
-      {
-        request: { query: variableQuery, variables: variables2 } as any,
-        result: result2,
-      },
-      {
-        request: { query: resetMutation } as any,
-        result: resetMutationResult,
-      },
-    );
+    link = mockSingleLink({
+      request: { query: variableQuery, variables: variables1 } as any,
+      result: result1,
+    }, {
+      request: { query: variableQuery, variables: variables2 } as any,
+      result: result2,
+    }, {
+      request: { query: resetMutation } as any,
+      result: resetMutationResult,
+    }).setOnError(reject);
 
     client = new ApolloClient({
       link,
@@ -777,7 +784,7 @@ describe('mutation results', () => {
 
     const firstSubs = watchedQuery.subscribe({
       next: () => null,
-      error: done.fail,
+      error: reject,
     });
 
     // Cancel the query right away!
@@ -799,7 +806,7 @@ describe('mutation results', () => {
           });
         } else if (yieldCount === 2) {
           expect(data.echo).toBe('0');
-          done();
+          resolve();
         }
       },
       error: () => {
@@ -810,7 +817,7 @@ describe('mutation results', () => {
     watchedQuery.refetch(variables2);
   });
 
-  it('allows mutations with optional arguments', done => {
+  itAsync('allows mutations with optional arguments', (resolve, reject) => {
     let count = 0;
 
     client = new ApolloClient({
@@ -877,18 +884,19 @@ describe('mutation results', () => {
       .then(() => {
         expect((client.cache as InMemoryCache).extract()).toEqual({
           ROOT_MUTATION: {
+            __typename: 'Mutation',
             'result({"a":1,"b":2})': 'hello',
             'result({"a":1,"c":3})': 'world',
             'result({"b":2,"c":3})': 'goodbye',
             'result({})': 'moon',
           },
         });
-        done();
+        resolve();
       })
-      .catch(done.fail);
+      .catch(reject);
   });
 
-  it('allows mutations with default values', done => {
+  itAsync('allows mutations with default values', (resolve, reject) => {
     let count = 0;
 
     client = new ApolloClient({
@@ -954,17 +962,18 @@ describe('mutation results', () => {
       .then(() => {
         expect((client.cache as InMemoryCache).extract()).toEqual({
           ROOT_MUTATION: {
+            __typename: 'Mutation',
             'result({"a":1,"b":"water"})': 'hello',
             'result({"a":2,"b":"cheese","c":3})': 'world',
             'result({"a":1,"b":"cheese","c":3})': 'goodbye',
           },
         });
-        done();
+        resolve();
       })
-      .catch(done.fail);
+      .catch(reject);
   });
 
-  it('will pass null to the network interface when provided', done => {
+  itAsync('will pass null to the network interface when provided', (resolve, reject) => {
     let count = 0;
 
     client = new ApolloClient({
@@ -1031,14 +1040,15 @@ describe('mutation results', () => {
       .then(() => {
         expect((client.cache as InMemoryCache).extract()).toEqual({
           ROOT_MUTATION: {
+            __typename: 'Mutation',
             'result({"a":1,"b":2,"c":null})': 'hello',
             'result({"a":1,"b":null,"c":3})': 'world',
             'result({"a":null,"b":null,"c":null})': 'moon',
           },
         });
-        done();
+        resolve();
       })
-      .catch(done.fail);
+      .catch(reject);
   });
 
   describe('store transaction updater', () => {
@@ -1067,9 +1077,9 @@ describe('mutation results', () => {
       },
     };
 
-    it('analogous of ARRAY_INSERT', () => {
+    itAsync('analogous of ARRAY_INSERT', (resolve, reject) => {
       let subscriptionHandle: Subscription;
-      return setup({
+      return setup(reject, {
         request: { query: mutation },
         result: mutationResult,
       })
@@ -1131,10 +1141,11 @@ describe('mutation results', () => {
           expect(newResult.data.todoList.todos[0].text).toBe(
             'This one was created with a mutation.',
           );
-        });
+        })
+        .then(resolve, reject);
     });
 
-    it('does not fail if optional query variables are not supplied', () => {
+    itAsync('does not fail if optional query variables are not supplied', (resolve, reject) => {
       let subscriptionHandle: Subscription;
       const mutationWithVars = gql`
         mutation createTodo($requiredVar: String!, $optionalVar: String) {
@@ -1153,7 +1164,7 @@ describe('mutation results', () => {
         requiredVar: 'x',
         // optionalVar: 'y',
       };
-      return setup({
+      return setup(reject, {
         request: {
           query: mutationWithVars,
           variables,
@@ -1222,11 +1233,13 @@ describe('mutation results', () => {
           expect(newResult.data.todoList.todos[0].text).toBe(
             'This one was created with a mutation.',
           );
-        });
+        })
+        .then(resolve, reject);
     });
 
-    it('does not make next queries fail if a mutation fails', done => {
+    itAsync('does not make next queries fail if a mutation fails', (resolve, reject) => {
       const obsHandle = setupObsHandle(
+        error => { throw error },
         {
           request: { query: mutation },
           result: { errors: [new Error('mock error')] },
@@ -1273,7 +1286,7 @@ describe('mutation results', () => {
               },
             })
             .then(
-              () => done.fail(new Error('Mutation should have failed')),
+              () => reject(new Error('Mutation should have failed')),
               () =>
                 client.mutate({
                   mutation,
@@ -1309,14 +1322,15 @@ describe('mutation results', () => {
                 }),
             )
             .then(
-              () => done.fail(new Error('Mutation should have failed')),
+              () => reject(new Error('Mutation should have failed')),
               () => obsHandle.refetch(),
             )
-            .then(() => done(), done.fail);
+            .then(resolve, reject);
         },
       });
     });
-    it('error handling in reducer functions', () => {
+
+    itAsync('error handling in reducer functions', (resolve, reject) => {
       const oldError = console.error;
       const errors: any[] = [];
       console.error = (msg: string) => {
@@ -1324,7 +1338,7 @@ describe('mutation results', () => {
       };
 
       let subscriptionHandle: Subscription;
-      return setup({
+      return setup(reject, {
         request: { query: mutation },
         result: mutationResult,
       })
@@ -1352,7 +1366,8 @@ describe('mutation results', () => {
           expect(errors).toHaveLength(1);
           expect(errors[0].message).toBe(`Hello... It's me.`);
           console.error = oldError;
-        });
+        })
+        .then(resolve, reject);
     });
   });
 });
diff --git a/packages/apollo-client/src/__tests__/optimistic.ts b/src/__tests__/optimistic.ts
similarity index 88%
rename from packages/apollo-client/src/__tests__/optimistic.ts
rename to src/__tests__/optimistic.ts
--- a/packages/apollo-client/src/__tests__/optimistic.ts
+++ b/src/__tests__/optimistic.ts
@@ -1,18 +1,17 @@
 import { from } from 'rxjs';
 import { take, toArray, map } from 'rxjs/operators';
 import { assign, cloneDeep } from 'lodash';
-import { addTypenameToDocument } from 'apollo-utilities';
-import { InMemoryCache } from 'apollo-cache-inmemory';
-
-import { mockSingleLink } from '../__mocks__/mockLinks';
+import gql from 'graphql-tag';
 
+import { mockSingleLink } from '../utilities/testing/mocking/mockLink';
 import { MutationQueryReducersMap } from '../core/types';
-import { Subscription } from '../util/Observable';
-
-import ApolloClient from '../';
-
-import gql from 'graphql-tag';
-import { stripSymbols } from 'apollo-utilities';
+import { Subscription } from '../utilities/observables/Observable';
+import { ApolloClient } from '../';
+import { addTypenameToDocument } from '../utilities/graphql/transform';
+import { makeReference } from '../utilities/graphql/storeUtils';
+import { stripSymbols } from '../utilities/testing/stripSymbols';
+import { itAsync } from '../utilities/testing/itAsync';
+import { InMemoryCache } from '../cache/inmemory/inMemoryCache';
 
 describe('optimistic mutation results', () => {
   const query = gql`
@@ -101,14 +100,14 @@ describe('optimistic mutation results', () => {
   let client: ApolloClient;
   let link: any;
 
-  function setup(...mockedResponses: any[]) {
-    link = mockSingleLink(
-      {
-        request: { query },
-        result,
-      },
-      ...mockedResponses,
-    );
+  function setup(
+    reject: (reason: any) => any,
+    ...mockedResponses: any[]
+  ) {
+    link = mockSingleLink({
+      request: { query },
+      result,
+    }, ...mockedResponses).setOnError(reject);
 
     client = new ApolloClient({
       link,
@@ -190,9 +189,9 @@ describe('optimistic mutation results', () => {
         },
       };
 
-      it('handles a single error for a single mutation', async () => {
+      itAsync('handles a single error for a single mutation', async (resolve, reject) => {
         expect.assertions(6);
-        await setup({
+        await setup(reject, {
           request: { query: mutation },
           error: new Error('forbidden (test error)'),
         });
@@ -217,12 +216,15 @@ describe('optimistic mutation results', () => {
           expect((dataInStore['TodoList5'] as any).todos.length).toBe(3);
           expect(stripSymbols(dataInStore)).not.toHaveProperty('Todo99');
         }
+
+        resolve();
       });
 
-      it('handles errors produced by one mutation in a series', async () => {
+      itAsync('handles errors produced by one mutation in a series', async (resolve, reject) => {
         expect.assertions(10);
         let subscriptionHandle: Subscription;
         await setup(
+          reject,
           {
             request: { query: mutation },
             error: new Error('forbidden (test error)'),
@@ -274,19 +276,22 @@ describe('optimistic mutation results', () => {
         await Promise.all([promise, promise2]);
 
         subscriptionHandle.unsubscribe();
-        const dataInStore = (client.cache as InMemoryCache).extract(true);
-        expect((dataInStore['TodoList5'] as any).todos.length).toBe(4);
-        expect(stripSymbols(dataInStore)).not.toHaveProperty('Todo99');
-        expect(dataInStore).toHaveProperty('Todo66');
-        expect((dataInStore['TodoList5'] as any).todos).toContainEqual(
-          realIdValue('Todo66', 'Todo'),
-        );
-        expect((dataInStore['TodoList5'] as any).todos).not.toContainEqual(
-          realIdValue('Todo99', 'Todo'),
-        );
+        {
+          const dataInStore = (client.cache as InMemoryCache).extract(true);
+          expect((dataInStore['TodoList5'] as any).todos.length).toBe(4);
+          expect(stripSymbols(dataInStore)).not.toHaveProperty('Todo99');
+          expect(dataInStore).toHaveProperty('Todo66');
+          expect((dataInStore['TodoList5'] as any).todos).toContainEqual(
+            makeReference('Todo66'),
+          );
+          expect((dataInStore['TodoList5'] as any).todos).not.toContainEqual(
+            makeReference('Todo99'),
+          );
+          resolve();
+        }
       });
 
-      it('can run 2 mutations concurrently and handles all intermediate states well', async () => {
+      itAsync('can run 2 mutations concurrently and handles all intermediate states well', async (resolve, reject) => {
         expect.assertions(34);
         function checkBothMutationsAreApplied(
           expectedText1: any,
@@ -298,16 +303,17 @@ describe('optimistic mutation results', () => {
           expect(dataInStore).toHaveProperty('Todo66');
           // <any> can be removed once @types/chai adds deepInclude
           expect((dataInStore['TodoList5'] as any).todos).toContainEqual(
-            realIdValue('Todo66', 'Todo'),
+            makeReference('Todo66'),
           );
           expect((dataInStore['TodoList5'] as any).todos).toContainEqual(
-            realIdValue('Todo99', 'Todo'),
+            makeReference('Todo99'),
           );
           expect((dataInStore['Todo99'] as any).text).toBe(expectedText1);
           expect((dataInStore['Todo66'] as any).text).toBe(expectedText2);
         }
         let subscriptionHandle: Subscription;
         await setup(
+          reject,
           {
             request: { query: mutation },
             result: mutationResult,
@@ -381,6 +387,8 @@ describe('optimistic mutation results', () => {
           'This one was created with a mutation.',
           'Second mutation.',
         );
+
+        resolve();
       });
     });
 
@@ -416,10 +424,10 @@ describe('optimistic mutation results', () => {
         });
       };
 
-      it('handles a single error for a single mutation', async () => {
+      itAsync('handles a single error for a single mutation', async (resolve, reject) => {
         expect.assertions(6);
         try {
-          await setup({
+          await setup(reject, {
             request: { query: mutation },
             error: new Error('forbidden (test error)'),
           });
@@ -445,12 +453,15 @@ describe('optimistic mutation results', () => {
           expect((dataInStore['TodoList5'] as any).todos.length).toBe(3);
           expect(stripSymbols(dataInStore)).not.toHaveProperty('Todo99');
         }
+
+        resolve();
       });
 
-      it('handles errors produced by one mutation in a series', async () => {
+      itAsync('handles errors produced by one mutation in a series', async (resolve, reject) => {
         expect.assertions(10);
         let subscriptionHandle: Subscription;
         await setup(
+          reject,
           {
             request: { query: mutation },
             error: new Error('forbidden (test error)'),
@@ -502,19 +513,22 @@ describe('optimistic mutation results', () => {
         await Promise.all([promise, promise2]);
 
         subscriptionHandle.unsubscribe();
-        const dataInStore = (client.cache as InMemoryCache).extract(true);
-        expect((dataInStore['TodoList5'] as any).todos.length).toBe(4);
-        expect(stripSymbols(dataInStore)).not.toHaveProperty('Todo99');
-        expect(dataInStore).toHaveProperty('Todo66');
-        expect((dataInStore['TodoList5'] as any).todos).toContainEqual(
-          realIdValue('Todo66', 'Todo'),
-        );
-        expect((dataInStore['TodoList5'] as any).todos).not.toContainEqual(
-          realIdValue('Todo99', 'Todo'),
-        );
+        {
+          const dataInStore = (client.cache as InMemoryCache).extract(true);
+          expect((dataInStore['TodoList5'] as any).todos.length).toBe(4);
+          expect(stripSymbols(dataInStore)).not.toHaveProperty('Todo99');
+          expect(dataInStore).toHaveProperty('Todo66');
+          expect((dataInStore['TodoList5'] as any).todos).toContainEqual(
+            makeReference('Todo66'),
+          );
+          expect((dataInStore['TodoList5'] as any).todos).not.toContainEqual(
+            makeReference('Todo99'),
+          );
+          resolve();
+        }
       });
 
-      it('can run 2 mutations concurrently and handles all intermediate states well', async () => {
+      itAsync('can run 2 mutations concurrently and handles all intermediate states well', async (resolve, reject) => {
         expect.assertions(34);
         function checkBothMutationsAreApplied(
           expectedText1: any,
@@ -525,16 +539,17 @@ describe('optimistic mutation results', () => {
           expect(dataInStore).toHaveProperty('Todo99');
           expect(dataInStore).toHaveProperty('Todo66');
           expect((dataInStore['TodoList5'] as any).todos).toContainEqual(
-            realIdValue('Todo66', 'Todo'),
+            makeReference('Todo66'),
           );
           expect((dataInStore['TodoList5'] as any).todos).toContainEqual(
-            realIdValue('Todo99', 'Todo'),
+            makeReference('Todo99'),
           );
           expect((dataInStore['Todo99'] as any).text).toBe(expectedText1);
           expect((dataInStore['Todo66'] as any).text).toBe(expectedText2);
         }
         let subscriptionHandle: Subscription;
         await setup(
+          reject,
           {
             request: { query: mutation },
             result: mutationResult,
@@ -609,6 +624,8 @@ describe('optimistic mutation results', () => {
           'This one was created with a mutation.',
           'Second mutation.',
         );
+
+        resolve();
       });
     });
   });
@@ -679,12 +696,12 @@ describe('optimistic mutation results', () => {
       }
     `;
 
-    it(
+    itAsync(
       'should read the optimistic response of a mutation when making an ' +
         'ApolloClient.readQuery() call, if the `optimistic` param is set to ' +
         'true',
-      () => {
-        return setup({
+      (resolve, reject) => {
+        return setup(reject, {
           request: { query: todoListMutation },
           result: todoListMutationResult,
         }).then(() => {
@@ -692,22 +709,22 @@ describe('optimistic mutation results', () => {
             mutation: todoListMutation,
             optimisticResponse: todoListOptimisticResponse,
             update: (proxy, mResult: any) => {
-              const data = client.readQuery({ query: todoListQuery }, true);
+              const data = proxy.readQuery({ query: todoListQuery }, true);
               expect(data.todoList.todos[0].text).toEqual(
                 todoListOptimisticResponse.createTodo.todos[0].text,
               );
             },
           });
-        });
+        }).then(resolve, reject);
       },
     );
 
-    it(
+    itAsync(
       'should not read the optimistic response of a mutation when making ' +
         'an ApolloClient.readQuery() call, if the `optimistic` param is set ' +
         'to false',
-      () => {
-        return setup({
+      (resolve, reject) => {
+        return setup(reject, {
           request: { query: todoListMutation },
           result: todoListMutationResult,
         }).then(() => {
@@ -716,11 +733,11 @@ describe('optimistic mutation results', () => {
             optimisticResponse: todoListOptimisticResponse,
             update: (proxy, mResult: any) => {
               const incomingText = mResult.data.createTodo.todos[0].text;
-              const data = client.readQuery({ query: todoListQuery }, false);
+              const data = proxy.readQuery({ query: todoListQuery }, false);
               expect(data.todoList.todos[0].text).toEqual(incomingText);
             },
           });
-        });
+        }).then(resolve, reject);
       },
     );
 
@@ -735,12 +752,12 @@ describe('optimistic mutation results', () => {
       }
     `;
 
-    it(
+    itAsync(
       'should read the optimistic response of a mutation when making an ' +
         'ApolloClient.readFragment() call, if the `optimistic` param is set ' +
         'to true',
-      () => {
-        return setup({
+      (resolve, reject) => {
+        return setup(reject, {
           request: { query: todoListMutation },
           result: todoListMutationResult,
         }).then(() => {
@@ -748,7 +765,7 @@ describe('optimistic mutation results', () => {
             mutation: todoListMutation,
             optimisticResponse: todoListOptimisticResponse,
             update: (proxy, mResult: any) => {
-              const data: any = client.readFragment(
+              const data: any = proxy.readFragment(
                 {
                   id: 'TodoList5',
                   fragment: todoListFragment,
@@ -760,16 +777,16 @@ describe('optimistic mutation results', () => {
               );
             },
           });
-        });
+        }).then(resolve, reject);
       },
     );
 
-    it(
+    itAsync(
       'should not read the optimistic response of a mutation when making ' +
         'an ApolloClient.readFragment() call, if the `optimistic` param is ' +
         'set to false',
-      () => {
-        return setup({
+      (resolve, reject) => {
+        return setup(reject, {
           request: { query: todoListMutation },
           result: todoListMutationResult,
         }).then(() => {
@@ -778,7 +795,7 @@ describe('optimistic mutation results', () => {
             optimisticResponse: todoListOptimisticResponse,
             update: (proxy, mResult: any) => {
               const incomingText = mResult.data.createTodo.todos[0].text;
-              const data: any = client.readFragment(
+              const data: any = proxy.readFragment(
                 {
                   id: 'TodoList5',
                   fragment: todoListFragment,
@@ -788,7 +805,7 @@ describe('optimistic mutation results', () => {
               expect(data.todos[0].text).toEqual(incomingText);
             },
           });
-        });
+        }).then(resolve, reject);
       },
     );
   });
@@ -830,10 +847,10 @@ describe('optimistic mutation results', () => {
       },
     });
 
-    it('will use a passed variable in optimisticResponse', async () => {
+    itAsync('will use a passed variable in optimisticResponse', async (resolve, reject) => {
       expect.assertions(6);
       let subscriptionHandle: Subscription;
-      await setup({
+      await setup(reject, {
         request: { query: mutation, variables },
         result: mutationResult,
       });
@@ -898,6 +915,8 @@ describe('optimistic mutation results', () => {
       expect(newResult.data.todoList.todos[0].text).toEqual(
         'This one was created with a mutation.',
       );
+
+      resolve();
     });
   });
 
@@ -966,10 +985,10 @@ describe('optimistic mutation results', () => {
       },
     };
 
-    it('will insert a single item to the beginning', async () => {
+    itAsync('will insert a single itemAsync to the beginning', async (resolve, reject) => {
       expect.assertions(7);
       let subscriptionHandle: Subscription;
-      await setup({
+      await setup(reject, {
         request: { query: mutation },
         result: mutationResult,
       });
@@ -988,13 +1007,19 @@ describe('optimistic mutation results', () => {
         mutation,
         optimisticResponse,
         updateQueries: {
-          todoList: (prev, options) => {
+          todoList(prev, options) {
             const mResult = options.mutationResult as any;
             expect(mResult.data.createTodo.id).toEqual('99');
-
-            const state = cloneDeep(prev) as any;
-            state.todoList.todos.unshift(mResult.data.createTodo);
-            return state;
+            return {
+              ...prev,
+              todoList: {
+                ...prev.todoList,
+                todos: [
+                  mResult.data.createTodo,
+                  ...prev.todoList.todos,
+                ],
+              },
+            };
           },
         },
       });
@@ -1017,12 +1042,15 @@ describe('optimistic mutation results', () => {
       expect(newResult.data.todoList.todos[0].text).toEqual(
         'This one was created with a mutation.',
       );
+
+      resolve();
     });
 
-    it('two array insert like mutations', async () => {
+    itAsync('two array insert like mutations', async (resolve, reject) => {
       expect.assertions(9);
       let subscriptionHandle: Subscription;
       await setup(
+        reject,
         {
           request: { query: mutation },
           result: mutationResult,
@@ -1107,12 +1135,15 @@ describe('optimistic mutation results', () => {
       expect(newResult.data.todoList.todos[1].text).toEqual(
         'This one was created with a mutation.',
       );
+
+      resolve();
     });
 
-    it('two mutations, one fails', async () => {
+    itAsync('two mutations, one fails', async (resolve, reject) => {
       expect.assertions(10);
       let subscriptionHandle: Subscription;
       await setup(
+        reject,
         {
           request: { query: mutation },
           error: new Error('forbidden (test error)'),
@@ -1184,36 +1215,35 @@ describe('optimistic mutation results', () => {
       await Promise.all([promise, promise2]);
 
       subscriptionHandle.unsubscribe();
-      const dataInStore = (client.cache as InMemoryCache).extract(true);
-      expect((dataInStore['TodoList5'] as any).todos.length).toEqual(4);
-      expect(stripSymbols(dataInStore)).not.toHaveProperty('Todo99');
-      expect(dataInStore).toHaveProperty('Todo66');
-      expect((dataInStore['TodoList5'] as any).todos).toContainEqual(
-        realIdValue('Todo66', 'Todo'),
-      );
-      expect((dataInStore['TodoList5'] as any).todos).not.toContainEqual(
-        realIdValue('Todo99', 'Todo'),
-      );
+      {
+        const dataInStore = (client.cache as InMemoryCache).extract(true);
+        expect((dataInStore['TodoList5'] as any).todos.length).toEqual(4);
+        expect(stripSymbols(dataInStore)).not.toHaveProperty('Todo99');
+        expect(dataInStore).toHaveProperty('Todo66');
+        expect((dataInStore['TodoList5'] as any).todos).toContainEqual(
+          makeReference('Todo66'),
+        );
+        expect((dataInStore['TodoList5'] as any).todos).not.toContainEqual(
+          makeReference('Todo99'),
+        );
+        resolve();
+      }
     });
 
-    it('will handle dependent updates', async () => {
+    itAsync('will handle dependent updates', async (resolve, reject) => {
       expect.assertions(1);
-      link = mockSingleLink(
-        {
-          request: { query },
-          result,
-        },
-        {
-          request: { query: mutation },
-          result: mutationResult,
-          delay: 10,
-        },
-        {
-          request: { query: mutation },
-          result: mutationResult2,
-          delay: 20,
-        },
-      );
+      link = mockSingleLink({
+        request: { query },
+        result,
+      }, {
+        request: { query: mutation },
+        result: mutationResult,
+        delay: 10,
+      }, {
+        request: { query: mutation },
+        result: mutationResult2,
+        delay: 20,
+      }).setOnError(reject);
 
       const customOptimisticResponse1 = {
         __typename: 'Mutation',
@@ -1308,6 +1338,8 @@ describe('optimistic mutation results', () => {
           ...defaultTodos,
         ],
       ]);
+
+      resolve();
     });
   });
 
@@ -1366,10 +1398,10 @@ describe('optimistic mutation results', () => {
       },
     };
 
-    it('will insert a single item to the beginning', async () => {
+    itAsync('will insert a single itemAsync to the beginning', async (resolve, reject) => {
       expect.assertions(6);
       let subscriptionHandle: Subscription;
-      await setup({
+      await setup(reject, {
         request: { query: mutation },
         delay: 300,
         result: mutationResult,
@@ -1386,12 +1418,12 @@ describe('optimistic mutation results', () => {
       });
 
       let firstTime = true;
-      let before = new Date();
+      let before = Date.now();
       const promise = client.mutate({
         mutation,
         optimisticResponse,
         update: (proxy, mResult: any) => {
-          const after = new Date();
+          const after = Date.now();
           const duration = after - before;
           if (firstTime) {
             expect(duration < 300).toBe(true);
@@ -1401,13 +1433,18 @@ describe('optimistic mutation results', () => {
           }
           let data = proxy.readQuery({ query });
 
-          data.todoList.todos = [
-            mResult.data.createTodo,
-            ...data.todoList.todos,
-          ];
           proxy.writeQuery({
             query,
-            data,
+            data: {
+              ...data,
+              todoList: {
+                ...data.todoList,
+                todos: [
+                  mResult.data.createTodo,
+                  ...data.todoList.todos,
+                ],
+              },
+            },
           });
         },
       });
@@ -1428,12 +1465,15 @@ describe('optimistic mutation results', () => {
           'This one was created with a mutation.',
         );
       });
+
+      resolve();
     });
 
-    it('two array insert like mutations', async () => {
+    itAsync('two array insert like mutations', async (resolve, reject) => {
       expect.assertions(9);
       let subscriptionHandle: Subscription;
       await setup(
+        reject,
         {
           request: { query: mutation },
           result: mutationResult,
@@ -1536,12 +1576,15 @@ describe('optimistic mutation results', () => {
       expect(newResult.data.todoList.todos[1].text).toBe(
         'This one was created with a mutation.',
       );
+
+      resolve();
     });
 
-    it('two mutations, one fails', async () => {
+    itAsync('two mutations, one fails', async (resolve, reject) => {
       expect.assertions(10);
       let subscriptionHandle: Subscription;
       await setup(
+        reject,
         {
           request: { query: mutation },
           error: new Error('forbidden (test error)'),
@@ -1633,36 +1676,35 @@ describe('optimistic mutation results', () => {
       await Promise.all([promise, promise2]);
 
       subscriptionHandle.unsubscribe();
-      const dataInStore = (client.cache as InMemoryCache).extract(true);
-      expect((dataInStore['TodoList5'] as any).todos.length).toBe(4);
-      expect(stripSymbols(dataInStore)).not.toHaveProperty('Todo99');
-      expect(dataInStore).toHaveProperty('Todo66');
-      expect((dataInStore['TodoList5'] as any).todos).toContainEqual(
-        realIdValue('Todo66', 'Todo'),
-      );
-      expect((dataInStore['TodoList5'] as any).todos).not.toContainEqual(
-        realIdValue('Todo99', 'Todo'),
-      );
+      {
+        const dataInStore = (client.cache as InMemoryCache).extract(true);
+        expect((dataInStore['TodoList5'] as any).todos.length).toBe(4);
+        expect(stripSymbols(dataInStore)).not.toHaveProperty('Todo99');
+        expect(dataInStore).toHaveProperty('Todo66');
+        expect((dataInStore['TodoList5'] as any).todos).toContainEqual(
+          makeReference('Todo66'),
+        );
+        expect((dataInStore['TodoList5'] as any).todos).not.toContainEqual(
+          makeReference('Todo99'),
+        );
+        resolve();
+      }
     });
 
-    it('will handle dependent updates', async () => {
+    itAsync('will handle dependent updates', async (resolve, reject) => {
       expect.assertions(1);
-      link = mockSingleLink(
-        {
-          request: { query },
-          result,
-        },
-        {
-          request: { query: mutation },
-          result: mutationResult,
-          delay: 10,
-        },
-        {
-          request: { query: mutation },
-          result: mutationResult2,
-          delay: 20,
-        },
-      );
+      link = mockSingleLink({
+        request: { query },
+        result,
+      }, {
+        request: { query: mutation },
+        result: mutationResult,
+        delay: 10,
+      }, {
+        request: { query: mutation },
+        result: mutationResult2,
+        delay: 20,
+      }).setOnError(reject);
 
       const customOptimisticResponse1 = {
         __typename: 'Mutation',
@@ -1772,6 +1814,8 @@ describe('optimistic mutation results', () => {
           ...defaultTodos,
         ],
       ]);
+
+      resolve();
     });
   });
 });
@@ -1832,24 +1876,23 @@ describe('optimistic mutation - githunt comments', () => {
   let client: ApolloClient;
   let link: any;
 
-  function setup(...mockedResponses: any[]) {
-    link = mockSingleLink(
-      {
-        request: {
-          query: addTypenameToDocument(query),
-          variables,
-        },
-        result,
+  function setup(
+    reject: (reason: any) => any,
+    ...mockedResponses: any[]
+  ) {
+    link = mockSingleLink({
+      request: {
+        query: addTypenameToDocument(query),
+        variables,
       },
-      {
-        request: {
-          query: addTypenameToDocument(queryWithFragment),
-          variables,
-        },
-        result,
+      result,
+    }, {
+      request: {
+        query: addTypenameToDocument(queryWithFragment),
+        variables,
       },
-      ...mockedResponses,
-    );
+      result,
+    }, ...mockedResponses).setOnError(reject);
 
     client = new ApolloClient({
       link,
@@ -1925,7 +1968,7 @@ describe('optimistic mutation - githunt comments', () => {
     },
   };
 
-  it('can post a new comment', async () => {
+  itAsync('can post a new comment', async (resolve, reject) => {
     expect.assertions(1);
     const mutationVariables = {
       repoFullName: 'org/repo',
@@ -1933,7 +1976,7 @@ describe('optimistic mutation - githunt comments', () => {
     };
 
     let subscriptionHandle: Subscription;
-    await setup({
+    await setup(reject, {
       request: {
         query: addTypenameToDocument(mutation),
         variables: mutationVariables,
@@ -1962,14 +2005,7 @@ describe('optimistic mutation - githunt comments', () => {
 
     subscriptionHandle.unsubscribe();
     expect(newResult.data.entry.comments.length).toBe(2);
+
+    resolve();
   });
 });
-
-function realIdValue(id: string, typename: string) {
-  return {
-    type: 'id',
-    generated: false,
-    id,
-    typename,
-  };
-}
diff --git a/packages/apollo-client/src/__tests__/subscribeToMore.ts b/src/__tests__/subscribeToMore.ts
similarity index 86%
rename from packages/apollo-client/src/__tests__/subscribeToMore.ts
rename to src/__tests__/subscribeToMore.ts
--- a/packages/apollo-client/src/__tests__/subscribeToMore.ts
+++ b/src/__tests__/subscribeToMore.ts
@@ -1,13 +1,14 @@
 import gql from 'graphql-tag';
-import { ApolloLink, Operation } from 'apollo-link';
-import { InMemoryCache } from 'apollo-cache-inmemory';
-import { stripSymbols } from 'apollo-utilities';
-
 import { DocumentNode, OperationDefinitionNode } from 'graphql';
 
-import { mockSingleLink, mockObservableLink } from '../__mocks__/mockLinks';
-
-import ApolloClient from '../';
+import { ApolloLink } from '../link/core/ApolloLink';
+import { Operation } from '../link/core/types';
+import { mockSingleLink } from '../utilities/testing/mocking/mockLink';
+import { mockObservableLink } from '../utilities/testing/mocking/mockSubscriptionLink';
+import { ApolloClient } from '../';
+import { InMemoryCache } from '../cache/inmemory/inMemoryCache';
+import { stripSymbols } from '../utilities/testing/stripSymbols';
+import { itAsync } from '../utilities/testing/itAsync';
 
 const isSub = (operation: Operation) =>
   (operation.query as DocumentNode).definitions
@@ -58,10 +59,10 @@ describe('subscribeToMore', () => {
     name: string;
   }
 
-  it('triggers new result from subscription data', done => {
+  itAsync('triggers new result from subscription data', (resolve, reject) => {
     let latestResult: any = null;
     const wSLink = mockObservableLink();
-    const httpLink = mockSingleLink(req1);
+    const httpLink = mockSingleLink(req1).setOnError(reject);
 
     const link = ApolloLink.split(isSub, wSLink, httpLink);
     let counter = 0;
@@ -102,7 +103,7 @@ describe('subscribeToMore', () => {
         networkStatus: 7,
         stale: false,
       });
-      done();
+      resolve();
     }, 15);
 
     for (let i = 0; i < 2; i++) {
@@ -110,10 +111,10 @@ describe('subscribeToMore', () => {
     }
   });
 
-  it('calls error callback on error', done => {
+  itAsync('calls error callback on error', (resolve, reject) => {
     let latestResult: any = null;
     const wSLink = mockObservableLink();
-    const httpLink = mockSingleLink(req1);
+    const httpLink = mockSingleLink(req1).setOnError(reject);
 
     const link = ApolloLink.split(isSub, wSLink, httpLink);
 
@@ -160,7 +161,7 @@ describe('subscribeToMore', () => {
       });
       expect(counter).toBe(2);
       expect(errorCount).toBe(1);
-      done();
+      resolve();
     }, 15);
 
     for (let i = 0; i < 2; i++) {
@@ -168,11 +169,11 @@ describe('subscribeToMore', () => {
     }
   });
 
-  it('prints unhandled subscription errors to the console', done => {
+  itAsync('prints unhandled subscription errors to the console', (resolve, reject) => {
     let latestResult: any = null;
 
     const wSLink = mockObservableLink();
-    const httpLink = mockSingleLink(req1);
+    const httpLink = mockSingleLink(req1).setOnError(reject);
 
     const link = ApolloLink.split(isSub, wSLink, httpLink);
 
@@ -221,7 +222,7 @@ describe('subscribeToMore', () => {
       expect(counter).toBe(1);
       expect(errorCount).toBe(1);
       console.error = consoleErr;
-      done();
+      resolve();
     }, 15);
 
     for (let i = 0; i < 2; i++) {
@@ -229,33 +230,23 @@ describe('subscribeToMore', () => {
     }
   });
 
-  it('should not corrupt the cache (#3062)', async done => {
+  itAsync('should not corrupt the cache (#3062)', async (resolve, reject) => {
     let latestResult: any = null;
     const wSLink = mockObservableLink();
-    const httpLink = mockSingleLink(req4);
+    const httpLink = mockSingleLink(req4).setOnError(reject);
 
     const link = ApolloLink.split(isSub, wSLink, httpLink);
     let counter = 0;
 
     const client = new ApolloClient({
       cache: new InMemoryCache({ addTypename: false }).restore({
-        'ROOT_QUERY.entry.0': {
-          value: 1,
-        },
-        'ROOT_QUERY.entry.1': {
-          value: 2,
-        },
         ROOT_QUERY: {
           entry: [
             {
-              type: 'id',
-              id: 'ROOT_QUERY.entry.0',
-              generated: true,
+              value: 1,
             },
             {
-              type: 'id',
-              id: 'ROOT_QUERY.entry.1',
-              generated: true,
+              value: 2,
             },
           ],
         },
@@ -334,11 +325,11 @@ describe('subscribeToMore', () => {
       networkStatus: 7,
       stale: false,
     });
-    done();
+    resolve();
   });
   // TODO add a test that checks that subscriptions are cancelled when obs is unsubscribed from.
 
-  it('allows specification of custom types for variables and payload (#4246)', done => {
+  itAsync('allows specification of custom types for variables and payload (#4246)', (resolve, reject) => {
     interface TypedOperation extends Operation {
       variables: {
         someNumber: number;
@@ -354,7 +345,7 @@ describe('subscribeToMore', () => {
 
     let latestResult: any = null;
     const wSLink = mockObservableLink();
-    const httpLink = mockSingleLink(typedReq);
+    const httpLink = mockSingleLink(typedReq).setOnError(reject);
 
     const link = ApolloLink.split(isSub, wSLink, httpLink);
     let counter = 0;
@@ -402,7 +393,7 @@ describe('subscribeToMore', () => {
         networkStatus: 7,
         stale: false,
       });
-      done();
+      resolve();
     }, 15);
 
     for (let i = 0; i < 2; i++) {
diff --git a/packages/apollo-cache/src/__tests__/__snapshots__/utils.ts.snap b/src/cache/core/__tests__/__snapshots__/utils.ts.snap
similarity index 100%
rename from packages/apollo-cache/src/__tests__/__snapshots__/utils.ts.snap
rename to src/cache/core/__tests__/__snapshots__/utils.ts.snap
diff --git a/packages/apollo-cache/src/__tests__/cache.ts b/src/cache/core/__tests__/cache.ts
similarity index 100%
rename from packages/apollo-cache/src/__tests__/cache.ts
rename to src/cache/core/__tests__/cache.ts
diff --git a/packages/apollo-cache/src/__tests__/utils.ts b/src/cache/core/__tests__/utils.ts
similarity index 99%
rename from packages/apollo-cache/src/__tests__/utils.ts
rename to src/cache/core/__tests__/utils.ts
--- a/packages/apollo-cache/src/__tests__/utils.ts
+++ b/src/cache/core/__tests__/utils.ts
@@ -1,4 +1,5 @@
 import { print } from 'graphql/language/printer';
+
 import { queryFromPojo, fragmentFromPojo } from '../utils';
 
 describe('writing data with no query', () => {
diff --git a/src/cache/inmemory/__tests__/__snapshots__/cache.ts.snap b/src/cache/inmemory/__tests__/__snapshots__/cache.ts.snap
new file mode 100644
--- /dev/null
+++ b/src/cache/inmemory/__tests__/__snapshots__/cache.ts.snap
@@ -0,0 +1,229 @@
+// 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`] = `
+Object {
+  "bar": Object {
+    "i": 7,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "e": 4,
+    "h": Object {
+      "__ref": "bar",
+    },
+  },
+}
+`;
+
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (1/2) 2`] = `
+Object {
+  "bar": Object {
+    "i": 7,
+    "j": 8,
+    "k": 9,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "e": 4,
+    "f": 5,
+    "g": 6,
+    "h": Object {
+      "__ref": "bar",
+    },
+  },
+}
+`;
+
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (1/2) 3`] = `
+Object {
+  "bar": Object {
+    "__typename": "Bar",
+    "i": 10,
+    "j": 8,
+    "k": 9,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "e": 4,
+    "f": 5,
+    "g": 6,
+    "h": Object {
+      "__ref": "bar",
+    },
+  },
+}
+`;
+
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (1/2) 4`] = `
+Object {
+  "bar": Object {
+    "__typename": "Bar",
+    "i": 10,
+    "j": 11,
+    "k": 12,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "e": 4,
+    "f": 5,
+    "g": 6,
+    "h": Object {
+      "__ref": "bar",
+    },
+  },
+}
+`;
+
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (1/2) 5`] = `
+Object {
+  "bar": Object {
+    "__typename": "Bar",
+    "i": 7,
+    "j": 8,
+    "k": 9,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "e": 4,
+    "f": 5,
+    "g": 6,
+    "h": Object {
+      "__ref": "bar",
+    },
+  },
+}
+`;
+
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (1/2) 6`] = `
+Object {
+  "bar": Object {
+    "__typename": "Bar",
+    "i": 10,
+    "j": 11,
+    "k": 12,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "e": 4,
+    "f": 5,
+    "g": 6,
+    "h": Object {
+      "__ref": "bar",
+    },
+  },
+}
+`;
+
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (2/2) 1`] = `
+Object {
+  "bar": Object {
+    "i": 7,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "e": 4,
+    "h": Object {
+      "__ref": "bar",
+    },
+  },
+}
+`;
+
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (2/2) 2`] = `
+Object {
+  "bar": Object {
+    "i": 7,
+    "j": 8,
+    "k": 9,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "e": 4,
+    "f": 5,
+    "g": 6,
+    "h": Object {
+      "__ref": "bar",
+    },
+  },
+}
+`;
+
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (2/2) 3`] = `
+Object {
+  "bar": Object {
+    "__typename": "Bar",
+    "i": 10,
+    "j": 8,
+    "k": 9,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "e": 4,
+    "f": 5,
+    "g": 6,
+    "h": Object {
+      "__ref": "bar",
+    },
+  },
+}
+`;
+
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (2/2) 4`] = `
+Object {
+  "bar": Object {
+    "__typename": "Bar",
+    "i": 10,
+    "j": 11,
+    "k": 12,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "e": 4,
+    "f": 5,
+    "g": 6,
+    "h": Object {
+      "__ref": "bar",
+    },
+  },
+}
+`;
+
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (2/2) 5`] = `
+Object {
+  "bar": Object {
+    "__typename": "Bar",
+    "i": 7,
+    "j": 8,
+    "k": 9,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "e": 4,
+    "f": 5,
+    "g": 6,
+    "h": Object {
+      "__ref": "bar",
+    },
+  },
+}
+`;
+
+exports[`Cache writeFragment will write some deeply nested data into the store at any id (2/2) 6`] = `
+Object {
+  "bar": Object {
+    "__typename": "Bar",
+    "i": 10,
+    "j": 11,
+    "k": 12,
+  },
+  "foo": Object {
+    "__typename": "Foo",
+    "e": 4,
+    "f": 5,
+    "g": 6,
+    "h": Object {
+      "__ref": "bar",
+    },
+  },
+}
+`;
diff --git a/src/cache/inmemory/__tests__/__snapshots__/roundtrip.ts.snap b/src/cache/inmemory/__tests__/__snapshots__/roundtrip.ts.snap
new file mode 100644
--- /dev/null
+++ b/src/cache/inmemory/__tests__/__snapshots__/roundtrip.ts.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`writing to the store throws when trying to write an object without id that was previously queried with id 1`] = `"Store error: the application attempted to write an object with no provided id but the store already contains an id of abcd for this object."`;
diff --git a/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap b/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap
new file mode 100644
--- /dev/null
+++ b/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`writing to the store throws when trying to write an object without id that was previously queried with id 1`] = `"Store error: the application attempted to write an object with no provided id but the store already contains an id of abcd for this object."`;
diff --git a/packages/apollo-cache-inmemory/src/__tests__/cache.ts b/src/cache/inmemory/__tests__/cache.ts
similarity index 87%
rename from packages/apollo-cache-inmemory/src/__tests__/cache.ts
rename to src/cache/inmemory/__tests__/cache.ts
--- a/packages/apollo-cache-inmemory/src/__tests__/cache.ts
+++ b/src/cache/inmemory/__tests__/cache.ts
@@ -1,8 +1,9 @@
 import gql, { disableFragmentWarnings } from 'graphql-tag';
-import { stripSymbols } from 'apollo-utilities';
-import { cloneDeep } from 'lodash';
 
-import { InMemoryCache, InMemoryCacheConfig } from '..';
+import { stripSymbols } from '../../../utilities/testing/stripSymbols';
+import { cloneDeep } from '../../../utilities/common/cloneDeep';
+import { makeReference } from '../../../utilities/graphql/storeUtils';
+import { InMemoryCache, InMemoryCacheConfig } from '../inMemoryCache';
 
 disableFragmentWarnings();
 
@@ -24,12 +25,6 @@ describe('Cache', () => {
           resultCaching: false,
         }).restore(cloneDeep(data)),
       ),
-      initialDataForCaches.map(data =>
-        new InMemoryCache({
-          addTypename: false,
-          freezeResults: true,
-        }).restore(cloneDeep(data)),
-      ),
     ];
 
     cachesList.forEach((caches, i) => {
@@ -55,11 +50,6 @@ describe('Cache', () => {
         ...config,
         resultCaching: false,
       }),
-      new InMemoryCache({
-        addTypename: false,
-        ...config,
-        freezeResults: true,
-      }),
     ];
 
     caches.forEach((cache, i) => {
@@ -127,21 +117,13 @@ describe('Cache', () => {
             a: 1,
             b: 2,
             c: 3,
-            d: {
-              type: 'id',
-              id: 'foo',
-              generated: false,
-            },
+            d: makeReference('foo'),
           },
           foo: {
             e: 4,
             f: 5,
             g: 6,
-            h: {
-              type: 'id',
-              id: 'bar',
-              generated: false,
-            },
+            h: makeReference('bar'),
           },
           bar: {
             i: 7,
@@ -391,22 +373,14 @@ describe('Cache', () => {
             a: 1,
             b: 2,
             c: 3,
-            d: {
-              type: 'id',
-              id: 'foo',
-              generated: false,
-            },
+            d: makeReference('foo'),
           },
           foo: {
             __typename: 'Foo',
             e: 4,
             f: 5,
             g: 6,
-            h: {
-              type: 'id',
-              id: 'bar',
-              generated: false,
-            },
+            h: makeReference('bar'),
           },
           bar: {
             __typename: 'Bar',
@@ -638,6 +612,7 @@ describe('Cache', () => {
 
       expect((proxy as InMemoryCache).extract()).toEqual({
         ROOT_QUERY: {
+          __typename: "Query",
           a: 1,
         },
       });
@@ -654,6 +629,7 @@ describe('Cache', () => {
 
       expect((proxy as InMemoryCache).extract()).toEqual({
         ROOT_QUERY: {
+          __typename: "Query",
           a: 1,
           b: 2,
           c: 3,
@@ -673,6 +649,7 @@ describe('Cache', () => {
 
       expect((proxy as InMemoryCache).extract()).toEqual({
         ROOT_QUERY: {
+          __typename: "Query",
           a: 4,
           b: 5,
           c: 6,
@@ -698,16 +675,12 @@ describe('Cache', () => {
 
         expect((proxy as InMemoryCache).extract()).toEqual({
           ROOT_QUERY: {
+            __typename: "Query",
             a: 1,
             d: {
-              type: 'id',
-              id: '$ROOT_QUERY.d',
-              generated: true,
+              e: 4,
             },
           },
-          '$ROOT_QUERY.d': {
-            e: 4,
-          },
         });
 
         proxy.writeQuery({
@@ -726,24 +699,16 @@ describe('Cache', () => {
 
         expect((proxy as InMemoryCache).extract()).toEqual({
           ROOT_QUERY: {
+            __typename: "Query",
             a: 1,
+            // The new value for d overwrites the old value, since there
+            // is no custom merge function defined for Query.d.
             d: {
-              type: 'id',
-              id: '$ROOT_QUERY.d',
-              generated: true,
-            },
-          },
-          '$ROOT_QUERY.d': {
-            e: 4,
-            h: {
-              type: 'id',
-              id: '$ROOT_QUERY.d.h',
-              generated: true,
+              h: {
+                i: 7,
+              },
             },
           },
-          '$ROOT_QUERY.d.h': {
-            i: 7,
-          },
         });
 
         proxy.writeQuery({
@@ -774,30 +739,21 @@ describe('Cache', () => {
 
         expect((proxy as InMemoryCache).extract()).toEqual({
           ROOT_QUERY: {
+            __typename: "Query",
             a: 1,
             b: 2,
             c: 3,
             d: {
-              type: 'id',
-              id: '$ROOT_QUERY.d',
-              generated: true,
-            },
-          },
-          '$ROOT_QUERY.d': {
-            e: 4,
-            f: 5,
-            g: 6,
-            h: {
-              type: 'id',
-              id: '$ROOT_QUERY.d.h',
-              generated: true,
+              e: 4,
+              f: 5,
+              g: 6,
+              h: {
+                i: 7,
+                j: 8,
+                k: 9,
+              },
             },
           },
-          '$ROOT_QUERY.d.h': {
-            i: 7,
-            j: 8,
-            k: 9,
-          },
         });
       },
     );
@@ -825,6 +781,7 @@ describe('Cache', () => {
 
         expect((proxy as InMemoryCache).extract()).toEqual({
           ROOT_QUERY: {
+            __typename: "Query",
             'field({"literal":true,"value":42})': 1,
             'field({"literal":false,"value":42})': 2,
           },
@@ -855,6 +812,7 @@ describe('Cache', () => {
 
         expect((proxy as InMemoryCache).extract()).toEqual({
           ROOT_QUERY: {
+            __typename: "Query",
             'field({"literal":true,"value":42})': 1,
             'field({"literal":false,"value":null})': 2,
           },
@@ -1229,3 +1187,140 @@ describe('Cache', () => {
     });
   });
 });
+
+describe("InMemoryCache#broadcastWatches", function () {
+  it("should keep distinct consumers distinct (issue #5733)", function () {
+    const cache = new InMemoryCache();
+    const query = gql`
+      query {
+        value(arg: $arg) {
+          name
+        }
+      }
+    `;
+
+    const receivedCallbackResults: [string, number, any][] = [];
+
+    let nextWatchId = 1;
+    function watch(arg) {
+      const watchId = `id${nextWatchId++}`;
+      cache.watch({
+        query,
+        variables: { arg },
+        optimistic: false,
+        callback(result) {
+          receivedCallbackResults.push([watchId, arg, result]);
+        },
+      });
+      return watchId;
+    }
+
+    const id1 = watch(1);
+    expect(receivedCallbackResults).toEqual([]);
+
+    function write(arg: number, name: string) {
+      cache.writeQuery({
+        query,
+        variables: { arg },
+        data: {
+          value: { name },
+        },
+      });
+    }
+
+    write(1, "one");
+
+    const received1 = [id1, 1, {
+      result: {
+        value: {
+          name: "one",
+        },
+      },
+      complete: true,
+    }];
+
+    expect(receivedCallbackResults).toEqual([
+      received1,
+    ]);
+
+    const id2 = watch(2);
+
+    expect(receivedCallbackResults).toEqual([
+      received1,
+    ]);
+
+    write(2, "two");
+
+    const received2 = [id2, 2, {
+      result: {
+        value: {
+          name: "two",
+        },
+      },
+      complete: true,
+    }];
+
+    expect(receivedCallbackResults).toEqual([
+      received1,
+      // New results:
+      received1,
+      received2,
+    ]);
+
+    const id3 = watch(1);
+    const id4 = watch(1);
+
+    write(1, "one");
+
+    const received3 = [id3, 1, {
+      result: {
+        value: {
+          name: "one",
+        },
+      },
+      complete: true,
+    }];
+
+    const received4 = [id4, 1, {
+      result: {
+        value: {
+          name: "one",
+        },
+      },
+      complete: true,
+    }];
+
+    expect(receivedCallbackResults).toEqual([
+      received1,
+      received1,
+      received2,
+      // New results:
+      received3,
+      received4,
+    ]);
+
+    write(2, "TWO");
+
+    const received2AllCaps = [id2, 2, {
+      result: {
+        value: {
+          name: "TWO",
+        },
+      },
+      complete: true,
+    }];
+
+    expect(receivedCallbackResults).toEqual([
+      received1,
+      received1,
+      received2,
+      received3,
+      received4,
+      // New results:
+      received1,
+      received2AllCaps,
+      received3,
+      received4,
+    ]);
+  });
+});
diff --git a/packages/apollo-cache-inmemory/src/__tests__/diffAgainstStore.ts b/src/cache/inmemory/__tests__/diffAgainstStore.ts
similarity index 86%
rename from packages/apollo-cache-inmemory/src/__tests__/diffAgainstStore.ts
rename to src/cache/inmemory/__tests__/diffAgainstStore.ts
--- a/packages/apollo-cache-inmemory/src/__tests__/diffAgainstStore.ts
+++ b/src/cache/inmemory/__tests__/diffAgainstStore.ts
@@ -1,17 +1,16 @@
 import gql, { disableFragmentWarnings } from 'graphql-tag';
-import { toIdValue } from 'apollo-utilities';
 
-import { defaultNormalizedCacheFactory } from '../objectCache';
+import { Reference, makeReference } from '../../../utilities/graphql/storeUtils';
+import { defaultNormalizedCacheFactory } from '../entityStore';
 import { StoreReader } from '../readFromStore';
 import { StoreWriter } from '../writeToStore';
-import { HeuristicFragmentMatcher } from '../fragmentMatcher';
-import { defaultDataIdFromObject } from '../inMemoryCache';
+import { defaultDataIdFromObject } from '../policies';
 import { NormalizedCache } from '../types';
-
-const fragmentMatcherFunction = new HeuristicFragmentMatcher().match;
+import { Policies } from '../policies';
 
 disableFragmentWarnings();
-export function withError(func: Function, regex: RegExp) {
+
+export function withError(func: Function, regex?: RegExp) {
   let message: string = null as never;
   const { error } = console;
   console.error = (m: any) => {
@@ -20,7 +19,9 @@ export function withError(func: Function, regex: RegExp) {
 
   try {
     const result = func();
-    expect(message).toMatch(regex);
+    if (regex) {
+      expect(message).toMatch(regex);
+    }
     return result;
   } finally {
     console.error = error;
@@ -28,8 +29,11 @@ export function withError(func: Function, regex: RegExp) {
 }
 
 describe('diffing queries against the store', () => {
-  const reader = new StoreReader();
-  const writer = new StoreWriter();
+  const policies = new Policies({
+    dataIdFromObject: defaultDataIdFromObject,
+  })
+  const reader = new StoreReader({ policies });
+  const writer = new StoreWriter({ policies });
 
   it(
     'expects named fragments to return complete as true when diffd against ' +
@@ -53,10 +57,6 @@ describe('diffing queries against the store', () => {
             }
           }
         `,
-        fragmentMatcherFunction,
-        config: {
-          dataIdFromObject: defaultDataIdFromObject,
-        },
       });
 
       expect(queryResult.complete).toEqual(false);
@@ -99,10 +99,6 @@ describe('diffing queries against the store', () => {
             }
           }
         `,
-        fragmentMatcherFunction,
-        config: {
-          dataIdFromObject: defaultDataIdFromObject,
-        },
       });
 
       expect(queryResult.complete).toEqual(false);
@@ -138,30 +134,33 @@ describe('diffing queries against the store', () => {
   });
 
   it('caches root queries both under the ID of the node and the query name', () => {
-    const firstQuery = gql`
-      {
-        people_one(id: "1") {
-          __typename
-          id
-          name
-        }
-      }
-    `;
-
-    const result = {
-      people_one: {
-        __typename: 'Person',
-        id: '1',
-        name: 'Luke Skywalker',
-      },
-    };
-
-    const getIdField = ({ id }: { id: string }) => id;
+    const writer = new StoreWriter({
+      policies: new Policies({
+        typePolicies: {
+          Person: {
+            keyFields: ["id"],
+          },
+        },
+      }),
+    });
 
     const store = writer.writeQueryToStore({
-      result,
-      query: firstQuery,
-      dataIdFromObject: getIdField,
+      query: gql`
+        {
+          people_one(id: "1") {
+            __typename
+            idAlias: id
+            name
+          }
+        }
+      `,
+      result: {
+        people_one: {
+          __typename: 'Person',
+          idAlias: '1',
+          name: 'Luke Skywalker',
+        },
+      },
     });
 
     const secondQuery = gql`
@@ -180,7 +179,11 @@ describe('diffing queries against the store', () => {
     });
 
     expect(complete).toBeTruthy();
-    expect(store.get('1')).toEqual(result.people_one);
+    expect(store.get('Person:{"id":"1"}')).toEqual({
+      __typename: 'Person',
+      id: '1',
+      name: 'Luke Skywalker',
+    });
   });
 
   it('does not swallow errors other than field errors', () => {
@@ -253,11 +256,10 @@ describe('diffing queries against the store', () => {
         store,
         query: unionQuery,
         returnPartialData: false,
-        fragmentMatcherFunction,
       });
 
-      expect(complete).toBe(false);
-    }, /IntrospectionFragmentMatcher/);
+      expect(complete).toBe(true);
+    });
   });
 
   it('does not error on a query with fields missing from all but one named fragment', () => {
@@ -304,7 +306,7 @@ describe('diffing queries against the store', () => {
       query: unionQuery,
     });
 
-    expect(complete).toBe(false);
+    expect(complete).toBe(true);
   });
 
   it('throws an error on a query with fields missing from matching named fragments', () => {
@@ -421,6 +423,7 @@ describe('diffing queries against the store', () => {
 
     expect(simpleDiff.result).toEqual({
       people_one: {
+        __typename: 'Person',
         name: 'Luke Skywalker',
       },
     });
@@ -432,6 +435,7 @@ describe('diffing queries against the store', () => {
 
     expect(inlineDiff.result).toEqual({
       people_one: {
+        __typename: 'Person',
         name: 'Luke Skywalker',
       },
     });
@@ -443,6 +447,7 @@ describe('diffing queries against the store', () => {
 
     expect(namedDiff.result).toEqual({
       people_one: {
+        __typename: 'Person',
         name: 'Luke Skywalker',
       },
     });
@@ -491,14 +496,17 @@ describe('diffing queries against the store', () => {
       },
     };
 
-    function dataIdFromObject({ id }: { id: string }) {
-      return id;
-    }
+    const policies = new Policies({
+      dataIdFromObject({ id }: { id: string }) {
+        return id;
+      },
+    });
+
+    const writer = new StoreWriter({ policies });
 
     const store = writer.writeQueryToStore({
       query,
       result: queryResult,
-      dataIdFromObject,
     });
 
     const { result } = reader.diffQueryAgainstStore({
@@ -507,14 +515,14 @@ describe('diffing queries against the store', () => {
     });
 
     expect(result).toEqual(queryResult);
-    expect(dataIdFromObject(result.a[0])).toBe('a:1');
-    expect(dataIdFromObject(result.a[1])).toBe('a:2');
-    expect(dataIdFromObject(result.a[2])).toBe('a:3');
-    expect(dataIdFromObject(result.c.e[0])).toBe('e:1');
-    expect(dataIdFromObject(result.c.e[1])).toBe('e:2');
-    expect(dataIdFromObject(result.c.e[2])).toBe('e:3');
-    expect(dataIdFromObject(result.c.e[3])).toBe('e:4');
-    expect(dataIdFromObject(result.c.e[4])).toBe('e:5');
+    expect(policies.identify(result.a[0])).toBe('a:1');
+    expect(policies.identify(result.a[1])).toBe('a:2');
+    expect(policies.identify(result.a[2])).toBe('a:3');
+    expect(policies.identify(result.c.e[0])).toBe('e:1');
+    expect(policies.identify(result.c.e[1])).toBe('e:2');
+    expect(policies.identify(result.c.e[2])).toBe('e:3');
+    expect(policies.identify(result.c.e[3])).toBe('e:4');
+    expect(policies.identify(result.c.e[4])).toBe('e:5');
   });
 
   describe('referential equality preservation', () => {
@@ -822,10 +830,15 @@ describe('diffing queries against the store', () => {
         },
       };
 
+      const writer = new StoreWriter({
+        policies: new Policies({
+          dataIdFromObject: ({ id }: { id: string }) => id,
+        }),
+      });
+
       const store = writer.writeQueryToStore({
         query,
         result: queryResult,
-        dataIdFromObject: ({ id }: { id: string }) => id,
       });
 
       const previousResult = {
@@ -910,6 +923,7 @@ describe('diffing queries against the store', () => {
       expect(result.d).not.toEqual(previousResult.d);
       expect(result.d.f).toEqual(previousResult.d.f);
     });
+
     it('will preserve equality with custom resolvers', () => {
       const listQuery = gql`
         {
@@ -924,7 +938,7 @@ describe('diffing queries against the store', () => {
       const listResult = {
         people: [
           {
-            id: '4',
+            id: 4,
             name: 'Luke Skywalker',
             __typename: 'Person',
           },
@@ -941,32 +955,46 @@ describe('diffing queries against the store', () => {
         }
       `;
 
-      const dataIdFromObject = (obj: any) => obj.id;
+      const policies = new Policies({
+        typePolicies: {
+          Query: {
+            fields: {
+              person(_, { args, isReference, toReference, readField }) {
+                expect(typeof args.id).toBe('number');
+                const ref = toReference({ __typename: 'Person', id: args.id });
+                expect(isReference(ref)).toBe(true);
+                expect(ref).toEqual({
+                  __ref: `Person:${JSON.stringify({ id: args.id })}`,
+                });
+                const found = readField<Reference[]>("people").find(
+                  person => person.__ref === ref.__ref);
+                expect(found).toBeTruthy();
+                return found;
+              },
+            },
+          },
+          Person: {
+            keyFields: ["id"],
+          },
+        },
+      });
+
+      const writer = new StoreWriter({ policies });
+      const reader = new StoreReader({ policies });
 
       const store = writer.writeQueryToStore({
         query: listQuery,
         result: listResult,
-        dataIdFromObject,
       });
 
       const previousResult = {
         person: listResult.people[0],
       };
 
-      const cacheRedirects = {
-        Query: {
-          person: (_: any, args: any) =>
-            toIdValue({ id: args['id'], typename: 'Person' }),
-        },
-      };
-
-      const config = { dataIdFromObject, cacheRedirects };
-
       const { result } = reader.diffQueryAgainstStore({
         store,
         query: itemQuery,
         previousResult,
-        config,
       });
 
       expect(result).toEqual(previousResult);
@@ -1091,24 +1119,30 @@ describe('diffing queries against the store', () => {
 
         expect(result).toEqual({
           user: {
+            __typename: 'User',
             id: 1,
             name: 'Ben',
             company: {
+              __typename: 'Company',
               id: 1,
               name: 'Apollo',
               users: [
                 {
+                  __typename: 'User',
                   id: 1,
                   name: 'Ben',
                   company: {
+                    __typename: 'Company',
                     id: 1,
                     name: 'Apollo',
                   },
                 },
                 {
+                  __typename: 'User',
                   id: 2,
                   name: 'James',
                   company: {
+                    __typename: 'Company',
                     id: 1,
                     name: 'Apollo',
                   },
@@ -1121,7 +1155,11 @@ describe('diffing queries against the store', () => {
 
       // Check first using generated IDs.
       check(
-        writer.writeQueryToStore({
+        new StoreWriter({
+          policies: new Policies({
+            dataIdFromObject: void 0,
+          })
+        }).writeQueryToStore({
           query,
           result: {
             user: company.users[0],
@@ -1131,8 +1169,11 @@ describe('diffing queries against the store', () => {
 
       // Now check with __typename-specific IDs.
       check(
-        writer.writeQueryToStore({
-          dataIdFromObject: defaultDataIdFromObject,
+        new StoreWriter({
+          policies: new Policies({
+            dataIdFromObject: defaultDataIdFromObject,
+          }),
+        }).writeQueryToStore({
           query,
           result: {
             user: company.users[0],
diff --git a/src/cache/inmemory/__tests__/entityStore.ts b/src/cache/inmemory/__tests__/entityStore.ts
new file mode 100644
--- /dev/null
+++ b/src/cache/inmemory/__tests__/entityStore.ts
@@ -0,0 +1,1208 @@
+import gql from 'graphql-tag';
+import { EntityStore, supportsResultCaching } from '../entityStore';
+import { InMemoryCache } from '../inMemoryCache';
+import { DocumentNode, FieldNode } from 'graphql';
+import { getOperationDefinition, getFragmentDefinitions } from '../../../utilities/graphql/getFromAST';
+import { createFragmentMap } from '../../../utilities/graphql/fragments';
+
+describe('EntityStore', () => {
+  it('should support result caching if so configured', () => {
+    const storeWithResultCaching = new EntityStore.Root({
+      resultCaching: true,
+    });
+
+    const storeWithoutResultCaching = new EntityStore.Root({
+      resultCaching: false,
+    });
+
+    expect(supportsResultCaching({ some: "arbitrary object " })).toBe(false);
+    expect(supportsResultCaching(storeWithResultCaching)).toBe(true);
+    expect(supportsResultCaching(storeWithoutResultCaching)).toBe(false);
+
+    const layerWithCaching = storeWithResultCaching.addLayer("with caching", () => {});
+    expect(supportsResultCaching(layerWithCaching)).toBe(true);
+    const anotherLayer = layerWithCaching.addLayer("another layer", () => {});
+    expect(supportsResultCaching(anotherLayer)).toBe(true);
+    expect(
+      anotherLayer
+        .removeLayer("with caching")
+        .removeLayer("another layer")
+    ).toBe(storeWithResultCaching);
+    expect(supportsResultCaching(storeWithResultCaching)).toBe(true);
+
+    const layerWithoutCaching = storeWithoutResultCaching.addLayer("with caching", () => {});
+    expect(supportsResultCaching(layerWithoutCaching)).toBe(false);
+    expect(layerWithoutCaching.removeLayer("with caching")).toBe(storeWithoutResultCaching);
+    expect(supportsResultCaching(storeWithoutResultCaching)).toBe(false);
+  });
+
+  function newBookAuthorCache() {
+    const cache = new InMemoryCache({
+      resultCaching: true,
+      dataIdFromObject(value: any) {
+        switch (value && value.__typename) {
+          case 'Book':
+            return 'Book:' + value.isbn;
+          case 'Author':
+            return 'Author:' + value.name;
+        }
+      },
+    });
+
+    const query = gql`
+      query {
+        book {
+          title
+          author {
+            name
+          }
+        }
+      }
+    `;
+
+    return {
+      cache,
+      query,
+    };
+  }
+
+  it('should reclaim no-longer-reachable, unretained entities', () => {
+    const { cache, query } = newBookAuthorCache();
+
+    cache.writeQuery({
+      query,
+      data: {
+        book: {
+          __typename: 'Book',
+          isbn: '9781451673319',
+          title: 'Fahrenheit 451',
+          author: {
+            __typename: 'Author',
+            name: 'Ray Bradbury',
+          }
+        },
+      },
+    });
+
+    expect(cache.extract()).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        book: {
+          __ref: "Book:9781451673319",
+        },
+      },
+      "Book:9781451673319": {
+        __typename: "Book",
+        title: "Fahrenheit 451",
+        author: {
+          __ref: 'Author:Ray Bradbury',
+        }
+      },
+      "Author:Ray Bradbury": {
+        __typename: "Author",
+        name: "Ray Bradbury",
+      },
+    });
+
+    cache.writeQuery({
+      query,
+      data: {
+        book: {
+          __typename: 'Book',
+          isbn: '0312429215',
+          title: '2666',
+          author: {
+            __typename: 'Author',
+            name: 'Roberto Bolaño',
+          },
+        },
+      },
+    });
+
+    const snapshot = cache.extract();
+
+    expect(snapshot).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        book: {
+          __ref: "Book:0312429215",
+        },
+      },
+      "Book:9781451673319": {
+        __typename: "Book",
+        title: "Fahrenheit 451",
+        author: {
+          __ref: 'Author:Ray Bradbury',
+        }
+      },
+      "Author:Ray Bradbury": {
+        __typename: "Author",
+        name: "Ray Bradbury",
+      },
+      "Book:0312429215": {
+        __typename: "Book",
+        author: {
+          __ref: "Author:Roberto Bolaño",
+        },
+        title: "2666",
+      },
+      "Author:Roberto Bolaño": {
+        __typename: "Author",
+        name: "Roberto Bolaño",
+      },
+    });
+
+    expect(cache.gc().sort()).toEqual([
+      'Author:Ray Bradbury',
+      'Book:9781451673319',
+    ]);
+
+    expect(cache.extract()).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        book: {
+          __ref: "Book:0312429215",
+        },
+      },
+      "Book:0312429215": {
+        __typename: "Book",
+        author: {
+          __ref: "Author:Roberto Bolaño",
+        },
+        title: "2666",
+      },
+      "Author:Roberto Bolaño": {
+        __typename: "Author",
+        name: "Roberto Bolaño",
+      },
+    });
+
+    // Nothing left to garbage collect.
+    expect(cache.gc()).toEqual([]);
+
+    // Go back to the pre-GC snapshot.
+    cache.restore(snapshot);
+    expect(cache.extract()).toEqual(snapshot);
+
+    // Reading a specific fragment causes it to be retained during garbage collection.
+    const authorNameFragment = gql`
+      fragment AuthorName on Author {
+        name
+      }
+    `;
+    const ray = cache.readFragment({
+      id: 'Author:Ray Bradbury',
+      fragment: authorNameFragment,
+    });
+
+    expect(cache.retain('Author:Ray Bradbury')).toBe(1);
+
+    expect(ray).toEqual({
+      __typename: 'Author',
+      name: 'Ray Bradbury',
+    });
+
+    expect(cache.gc()).toEqual([
+      // Only Fahrenheit 451 (the book) is reclaimed this time.
+      'Book:9781451673319',
+    ]);
+
+    expect(cache.extract()).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        book: {
+          __ref: "Book:0312429215",
+        },
+      },
+      "Author:Ray Bradbury": {
+        __typename: "Author",
+        name: "Ray Bradbury",
+      },
+      "Book:0312429215": {
+        __typename: "Book",
+        author: {
+          __ref: "Author:Roberto Bolaño",
+        },
+        title: "2666",
+      },
+      "Author:Roberto Bolaño": {
+        __typename: "Author",
+        name: "Roberto Bolaño",
+      },
+    });
+
+    expect(cache.gc()).toEqual([]);
+
+    expect(cache.release('Author:Ray Bradbury')).toBe(0);
+
+    expect(cache.gc()).toEqual([
+      'Author:Ray Bradbury',
+    ]);
+
+    expect(cache.gc()).toEqual([]);
+  });
+
+  it('should respect optimistic updates, when active', () => {
+    const { cache, query } = newBookAuthorCache();
+
+    cache.writeQuery({
+      query,
+      data: {
+        book: {
+          __typename: 'Book',
+          isbn: '9781451673319',
+          title: 'Fahrenheit 451',
+          author: {
+            __typename: 'Author',
+            name: 'Ray Bradbury',
+          }
+        },
+      },
+    });
+
+    expect(cache.gc()).toEqual([]);
+
+    // Orphan the F451 / Ray Bradbury data, but avoid collecting garbage yet.
+    cache.writeQuery({
+      query,
+      data: {
+        book: {
+          __typename: 'Book',
+          isbn: '1980719802',
+          title: '1984',
+          author: {
+            __typename: 'Author',
+            name: 'George Orwell',
+          },
+        }
+      }
+    });
+
+    cache.recordOptimisticTransaction(proxy => {
+      proxy.writeFragment({
+        id: 'Author:Ray Bradbury',
+        fragment: gql`
+          fragment AuthorBooks on Author {
+            books {
+              title
+            }
+          }
+        `,
+        data: {
+          books: [
+            {
+              __typename: 'Book',
+              isbn: '9781451673319',
+            },
+          ],
+        },
+      });
+    }, "ray books");
+
+    expect(cache.extract(true)).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        book: {
+          __ref: "Book:1980719802",
+        },
+      },
+      "Author:Ray Bradbury": {
+        __typename: "Author",
+        name: "Ray Bradbury",
+        books: [
+          {
+            __ref: "Book:9781451673319",
+          },
+        ],
+      },
+      "Book:9781451673319": {
+        __typename: "Book",
+        title: "Fahrenheit 451",
+        author: {
+          __ref: "Author:Ray Bradbury",
+        },
+      },
+      "Author:George Orwell": {
+        __typename: "Author",
+        name: "George Orwell",
+      },
+      "Book:1980719802": {
+        __typename: "Book",
+        title: "1984",
+        author: {
+          __ref: "Author:George Orwell",
+        },
+      },
+    });
+
+    // Nothing can be reclaimed while the optimistic update is retaining
+    // Fahrenheit 451.
+    expect(cache.gc()).toEqual([]);
+
+    cache.removeOptimistic("ray books");
+
+    expect(cache.extract(true)).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        book: {
+          __ref: "Book:1980719802",
+        },
+      },
+      "Author:Ray Bradbury": {
+        __typename: "Author",
+        name: "Ray Bradbury",
+        // Note that the optimistic books field has disappeared, as expected.
+      },
+      "Book:9781451673319": {
+        __typename: "Book",
+        title: "Fahrenheit 451",
+        author: {
+          __ref: "Author:Ray Bradbury",
+        },
+      },
+      "Author:George Orwell": {
+        __typename: "Author",
+        name: "George Orwell",
+      },
+      "Book:1980719802": {
+        __typename: "Book",
+        title: "1984",
+        author: {
+          __ref: "Author:George Orwell",
+        },
+      },
+    });
+
+    expect(cache.gc().sort()).toEqual([
+      "Author:Ray Bradbury",
+      "Book:9781451673319",
+    ]);
+
+    expect(cache.extract(true)).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        book: {
+          __ref: "Book:1980719802",
+        },
+      },
+      "Author:George Orwell": {
+        __typename: "Author",
+        name: "George Orwell",
+      },
+      "Book:1980719802": {
+        __typename: "Book",
+        title: "1984",
+        author: {
+          __ref: "Author:George Orwell",
+        },
+      },
+    });
+
+    expect(cache.gc()).toEqual([]);
+  });
+
+  it('should respect retain/release methods', () => {
+    const { query, cache } = newBookAuthorCache();
+
+    const eagerBookData = {
+      __typename: 'Book',
+      isbn: '1603589082',
+      title: 'Eager',
+      subtitle: 'The Surprising, Secret Life of Beavers and Why They Matter',
+      author: {
+        __typename: 'Author',
+        name: 'Ben Goldfarb',
+      },
+    };
+
+    const spinelessBookData = {
+      __typename: 'Book',
+      isbn: '0735211280',
+      title: 'Spineless',
+      subtitle: 'The Science of Jellyfish and the Art of Growing a Backbone',
+      author: {
+        __typename: 'Author',
+        name: 'Juli Berwald',
+      },
+    };
+
+    cache.writeQuery({
+      query,
+      data: {
+        book: spinelessBookData,
+      },
+    });
+
+    expect(cache.extract(true)).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        book: {
+          __ref: "Book:0735211280",
+        },
+      },
+      "Book:0735211280": {
+        __typename: "Book",
+        author: {
+          __ref: "Author:Juli Berwald",
+        },
+        title: "Spineless",
+      },
+      "Author:Juli Berwald": {
+        __typename: "Author",
+        name: "Juli Berwald",
+      },
+    });
+
+    cache.writeQuery({
+      query,
+      data: {
+        book: eagerBookData,
+      },
+    });
+
+    const snapshotWithBothBooksAndAuthors = {
+      ROOT_QUERY: {
+        __typename: "Query",
+        book: {
+          __ref: "Book:1603589082",
+        },
+      },
+      "Book:0735211280": {
+        __typename: "Book",
+        author: {
+          __ref: "Author:Juli Berwald",
+        },
+        title: "Spineless",
+      },
+      "Author:Juli Berwald": {
+        __typename: "Author",
+        name: "Juli Berwald",
+      },
+      "Book:1603589082": {
+        __typename: "Book",
+        author: {
+          __ref: "Author:Ben Goldfarb",
+        },
+        title: "Eager",
+      },
+      "Author:Ben Goldfarb": {
+        __typename: "Author",
+        name: "Ben Goldfarb",
+      },
+    };
+
+    expect(cache.extract(true)).toEqual(snapshotWithBothBooksAndAuthors);
+
+    expect(cache.retain("Book:0735211280")).toBe(1);
+
+    expect(cache.gc()).toEqual([]);
+
+    expect(cache.retain("Author:Juli Berwald")).toBe(1);
+
+    cache.recordOptimisticTransaction(proxy => {
+      proxy.writeFragment({
+        id: "Author:Juli Berwald",
+        fragment: gql`
+          fragment AuthorBooks on Author {
+            books {
+              title
+            }
+          }
+        `,
+        data: {
+          books: [
+            {
+              __typename: 'Book',
+              isbn: '0735211280',
+            },
+          ],
+        },
+      });
+    }, "juli books");
+
+    // Retain the Spineless book on the optimistic layer (for the first time)
+    // but release it on the root layer.
+    expect(cache.retain("Book:0735211280", true)).toBe(1);
+    expect(cache.release("Book:0735211280")).toBe(0);
+
+    // The Spineless book is still protected by the reference from author Juli
+    // Berwald's optimistically-added author.books field.
+    expect(cache.gc()).toEqual([]);
+
+    expect(cache.extract(true)).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        book: {
+          __ref: "Book:1603589082",
+        },
+      },
+      "Book:0735211280": {
+        __typename: "Book",
+        author: {
+          __ref: "Author:Juli Berwald",
+        },
+        title: "Spineless",
+      },
+      "Author:Juli Berwald": {
+        __typename: "Author",
+        name: "Juli Berwald",
+        // Note this extra optimistic field.
+        books: [
+          {
+            __ref: "Book:0735211280",
+          },
+        ],
+      },
+      "Book:1603589082": {
+        __typename: "Book",
+        author: {
+          __ref: "Author:Ben Goldfarb",
+        },
+        title: "Eager",
+      },
+      "Author:Ben Goldfarb": {
+        __typename: "Author",
+        name: "Ben Goldfarb",
+      },
+    });
+
+    // A non-optimistic snapshot will not have the extra books field.
+    expect(cache.extract(false)).toEqual(snapshotWithBothBooksAndAuthors);
+
+    cache.removeOptimistic("juli books");
+
+    // The optimistic books field is gone now that we've removed the optimistic
+    // layer that added it.
+    expect(cache.extract(true)).toEqual(snapshotWithBothBooksAndAuthors);
+
+    // The Spineless book is no longer retained or kept alive by any other root
+    // IDs, so it can finally be collected.
+    expect(cache.gc()).toEqual([
+      "Book:0735211280",
+    ]);
+
+    expect(cache.release("Author:Juli Berwald")).toBe(0);
+
+    // Now that Juli Berwald's author entity is no longer retained, garbage
+    // collection cometh for her. Look out, Juli!
+    expect(cache.gc()).toEqual([
+      "Author:Juli Berwald",
+    ]);
+
+    expect(cache.gc()).toEqual([]);
+  });
+
+  it('allows cache eviction', () => {
+    const { cache, query } = newBookAuthorCache();
+
+    const cuckoosCallingBook = {
+      __typename: "Book",
+      isbn: "031648637X",
+      title: "The Cuckoo's Calling",
+      author: {
+        __typename: "Author",
+        name: "Robert Galbraith",
+      },
+    };
+
+    expect(cache.identify(cuckoosCallingBook)).toBe("Book:031648637X");
+
+    cache.writeQuery({
+      query,
+      data: {
+        book: cuckoosCallingBook,
+      },
+    });
+
+    expect(cache.evict("Author:J.K. Rowling")).toBe(false);
+
+    const bookAuthorFragment = gql`
+      fragment BookAuthor on Book {
+        author {
+          name
+        }
+      }
+    `;
+
+    const fragmentResult = cache.readFragment({
+      id: cache.identify(cuckoosCallingBook),
+      fragment: bookAuthorFragment,
+    });
+
+    expect(fragmentResult).toEqual({
+      __typename: "Book",
+      author: {
+        __typename: "Author",
+        name: "Robert Galbraith",
+      },
+    });
+
+    cache.recordOptimisticTransaction(proxy => {
+      proxy.writeFragment({
+        id: cache.identify(cuckoosCallingBook),
+        fragment: bookAuthorFragment,
+        data: {
+          ...fragmentResult,
+          author: {
+            __typename: "Author",
+            name: "J.K. Rowling",
+          },
+        },
+      });
+    }, "real name");
+
+    const snapshotWithBothNames = {
+      ROOT_QUERY: {
+        __typename: "Query",
+        book: {
+          __ref: "Book:031648637X",
+        },
+      },
+      "Book:031648637X": {
+        __typename: "Book",
+        author: {
+          __ref: "Author:J.K. Rowling",
+        },
+        title: "The Cuckoo's Calling",
+      },
+      "Author:Robert Galbraith": {
+        __typename: "Author",
+        name: "Robert Galbraith",
+      },
+      "Author:J.K. Rowling": {
+        __typename: "Author",
+        name: "J.K. Rowling",
+      },
+    };
+
+    expect(cache.extract(true)).toEqual(snapshotWithBothNames);
+
+    expect(cache.gc()).toEqual([]);
+
+    expect(cache.retain('Author:Robert Galbraith')).toBe(1);
+
+    expect(cache.gc()).toEqual([]);
+
+    expect(cache.evict("Author:Robert Galbraith")).toBe(true);
+
+    expect(cache.gc()).toEqual([]);
+
+    cache.removeOptimistic("real name");
+
+    expect(cache.extract(true)).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        book: {
+          __ref: "Book:031648637X",
+        },
+      },
+      "Book:031648637X": {
+        __typename: "Book",
+        author: {
+          __ref: "Author:Robert Galbraith",
+        },
+        title: "The Cuckoo's Calling",
+      },
+      "Author:Robert Galbraith": {
+        __typename: "Author",
+        name: "Robert Galbraith",
+      },
+    });
+
+    cache.writeFragment({
+      id: cache.identify(cuckoosCallingBook),
+      fragment: bookAuthorFragment,
+      data: {
+        ...fragmentResult,
+        author: {
+          __typename: "Author",
+          name: "J.K. Rowling",
+        },
+      },
+    });
+
+    expect(cache.extract(true)).toEqual(snapshotWithBothNames);
+
+    expect(cache.retain("Author:Robert Galbraith")).toBe(2);
+
+    expect(cache.gc()).toEqual([]);
+
+    expect(cache.release("Author:Robert Galbraith")).toBe(1);
+    expect(cache.release("Author:Robert Galbraith")).toBe(0);
+
+    expect(cache.gc()).toEqual([
+      "Author:Robert Galbraith",
+    ]);
+
+    // If you're ever tempted to do this, you probably want to use cache.clear()
+    // instead, but evicting the ROOT_QUERY should work at least.
+    expect(cache.evict("ROOT_QUERY")).toBe(true);
+
+    expect(cache.extract(true)).toEqual({
+      "Book:031648637X": {
+        __typename: "Book",
+        author: {
+          __ref: "Author:J.K. Rowling",
+        },
+        title: "The Cuckoo's Calling",
+      },
+      "Author:J.K. Rowling": {
+        __typename: "Author",
+        name: "J.K. Rowling",
+      },
+    });
+
+    const ccId = cache.identify(cuckoosCallingBook);
+    expect(cache.retain(ccId)).toBe(2);
+    expect(cache.release(ccId)).toBe(1);
+    expect(cache.release(ccId)).toBe(0);
+
+    expect(cache.gc().sort()).toEqual([
+      "Author:J.K. Rowling",
+      ccId,
+    ]);
+  });
+
+  it("allows evicting specific fields", () => {
+    const query: DocumentNode = gql`
+      query {
+        authorOfBook(isbn: $isbn) {
+          name
+          hobby
+        }
+        publisherOfBook(isbn: $isbn) {
+          name
+          yearOfFounding
+        }
+      }
+    `;
+
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Query: {
+          fields: {
+            authorOfBook: {
+              keyArgs: ["isbn"],
+            },
+          },
+        },
+        Author: {
+          keyFields: ["name"],
+        },
+        Publisher: {
+          keyFields: ["name"],
+        },
+      },
+    });
+
+    const TedChiangData = {
+      __typename: "Author",
+      name: "Ted Chiang",
+      hobby: "video games",
+    };
+
+    const KnopfData = {
+      __typename: "Publisher",
+      name: "Alfred A. Knopf",
+      yearOfFounding: 1915,
+    };
+
+    cache.writeQuery({
+      query,
+      data: {
+        authorOfBook: TedChiangData,
+        publisherOfBook: KnopfData,
+      },
+      variables: {
+        isbn: "1529014514",
+      },
+    });
+
+    const justTedRootQueryData = {
+      __typename: "Query",
+      'authorOfBook:{"isbn":"1529014514"}': {
+        __ref: 'Author:{"name":"Ted Chiang"}',
+      },
+      // This storeFieldName format differs slightly from that of
+      // authorOfBook because we did not define keyArgs for the
+      // publisherOfBook field, so the legacy storeKeyNameFromField
+      // function was used instead.
+      'publisherOfBook({"isbn":"1529014514"})': {
+        __ref: 'Publisher:{"name":"Alfred A. Knopf"}',
+      },
+    };
+
+    expect(cache.extract()).toEqual({
+      ROOT_QUERY: justTedRootQueryData,
+      'Author:{"name":"Ted Chiang"}': TedChiangData,
+      'Publisher:{"name":"Alfred A. Knopf"}': KnopfData,
+    });
+
+    const JennyOdellData = {
+      __typename: "Author",
+      name: "Jenny Odell",
+      hobby: "birding",
+    };
+
+    const MelvilleData = {
+      __typename: "Publisher",
+      name: "Melville House",
+      yearOfFounding: 2001,
+    };
+
+    cache.writeQuery({
+      query,
+      data: {
+        authorOfBook: JennyOdellData,
+        publisherOfBook: MelvilleData,
+      },
+      variables: {
+        isbn: "1760641790",
+      },
+    });
+
+    const justJennyRootQueryData = {
+      __typename: "Query",
+      'authorOfBook:{"isbn":"1760641790"}': {
+        __ref: 'Author:{"name":"Jenny Odell"}',
+      },
+      'publisherOfBook({"isbn":"1760641790"})': {
+        __ref: 'Publisher:{"name":"Melville House"}',
+      },
+    };
+
+    expect(cache.extract()).toEqual({
+      ROOT_QUERY: {
+        ...justTedRootQueryData,
+        ...justJennyRootQueryData,
+      },
+      'Author:{"name":"Ted Chiang"}': TedChiangData,
+      'Publisher:{"name":"Alfred A. Knopf"}': KnopfData,
+      'Author:{"name":"Jenny Odell"}': JennyOdellData,
+      'Publisher:{"name":"Melville House"}': MelvilleData,
+    });
+
+    const fullTedResult = cache.readQuery<any>({
+      query,
+      variables: {
+        isbn: "1529014514",
+      },
+    });
+
+    expect(fullTedResult).toEqual({
+      authorOfBook: TedChiangData,
+      publisherOfBook: KnopfData,
+    });
+
+    const fullJennyResult = cache.readQuery<any>({
+      query,
+      variables: {
+        isbn: "1760641790",
+      },
+    });
+
+    expect(fullJennyResult).toEqual({
+      authorOfBook: JennyOdellData,
+      publisherOfBook: MelvilleData,
+    });
+
+    cache.evict(
+      cache.identify({
+        __typename: "Publisher",
+        name: "Alfred A. Knopf",
+      }),
+      "yearOfFounding",
+    );
+
+    expect(cache.extract()).toEqual({
+      ROOT_QUERY: {
+        ...justTedRootQueryData,
+        ...justJennyRootQueryData,
+      },
+      'Author:{"name":"Ted Chiang"}': TedChiangData,
+      'Publisher:{"name":"Alfred A. Knopf"}': {
+        __typename: "Publisher",
+        name: "Alfred A. Knopf",
+        // yearOfFounding has been removed
+      },
+      'Author:{"name":"Jenny Odell"}': JennyOdellData,
+      'Publisher:{"name":"Melville House"}': MelvilleData,
+    });
+
+    // Nothing to garbage collect yet.
+    expect(cache.gc()).toEqual([]);
+
+    cache.evict(cache.identify({
+      __typename: "Publisher",
+      name: "Melville House",
+    }));
+
+    expect(cache.extract()).toEqual({
+      ROOT_QUERY: {
+        ...justTedRootQueryData,
+        ...justJennyRootQueryData,
+      },
+      'Author:{"name":"Ted Chiang"}': TedChiangData,
+      'Publisher:{"name":"Alfred A. Knopf"}': {
+        __typename: "Publisher",
+        name: "Alfred A. Knopf",
+      },
+      'Author:{"name":"Jenny Odell"}': JennyOdellData,
+      // Melville House has been removed
+    });
+
+    cache.evict("ROOT_QUERY", "publisherOfBook");
+
+    function withoutPublisherOfBook(obj: object) {
+      const clean = { ...obj };
+      Object.keys(obj).forEach(key => {
+        if (key.startsWith("publisherOfBook")) {
+          delete clean[key];
+        }
+      });
+      return clean;
+    }
+
+    expect(cache.extract()).toEqual({
+      ROOT_QUERY: {
+        ...withoutPublisherOfBook(justTedRootQueryData),
+        ...withoutPublisherOfBook(justJennyRootQueryData),
+      },
+      'Author:{"name":"Ted Chiang"}': TedChiangData,
+      'Publisher:{"name":"Alfred A. Knopf"}': {
+        __typename: "Publisher",
+        name: "Alfred A. Knopf",
+      },
+      'Author:{"name":"Jenny Odell"}': JennyOdellData,
+    });
+
+    expect(cache.gc()).toEqual([
+      'Publisher:{"name":"Alfred A. Knopf"}',
+    ]);
+
+    expect(cache.extract()).toEqual({
+      ROOT_QUERY: {
+        ...withoutPublisherOfBook(justTedRootQueryData),
+        ...withoutPublisherOfBook(justJennyRootQueryData),
+      },
+      'Author:{"name":"Ted Chiang"}': TedChiangData,
+      'Author:{"name":"Jenny Odell"}': JennyOdellData,
+    });
+
+    const partialTedResult = cache.diff<any>({
+      query,
+      returnPartialData: true,
+      optimistic: false, // required but not important
+      variables: {
+        isbn: "1529014514",
+      },
+    });
+    expect(partialTedResult.complete).toBe(false);
+    expect(partialTedResult.result).toEqual({
+      authorOfBook: TedChiangData,
+    });
+    // The result caching system preserves the referential identity of
+    // unchanged nested result objects.
+    expect(
+      partialTedResult.result.authorOfBook,
+    ).toBe(fullTedResult.authorOfBook);
+
+    const partialJennyResult = cache.diff<any>({
+      query,
+      returnPartialData: true,
+      optimistic: true, // required but not important
+      variables: {
+        isbn: "1760641790",
+      },
+    });
+    expect(partialJennyResult.complete).toBe(false);
+    expect(partialJennyResult.result).toEqual({
+      authorOfBook: JennyOdellData,
+    });
+    // The result caching system preserves the referential identity of
+    // unchanged nested result objects.
+    expect(
+      partialJennyResult.result.authorOfBook,
+    ).toBe(fullJennyResult.authorOfBook);
+
+    const tedWithoutHobby = {
+      __typename: "Author",
+      name: "Ted Chiang",
+    };
+
+    cache.evict(
+      cache.identify(tedWithoutHobby),
+      "hobby",
+    );
+
+    expect(cache.diff<any>({
+      query,
+      returnPartialData: true,
+      optimistic: false, // required but not important
+      variables: {
+        isbn: "1529014514",
+      },
+    })).toEqual({
+      complete: false,
+      result: {
+        authorOfBook: tedWithoutHobby,
+      },
+    });
+
+    cache.evict("ROOT_QUERY", "authorOfBook");
+    expect(cache.gc().sort()).toEqual([
+      'Author:{"name":"Jenny Odell"}',
+      'Author:{"name":"Ted Chiang"}',
+    ]);
+    expect(cache.extract()).toEqual({
+      ROOT_QUERY: {
+        // Everything else has been removed.
+        __typename: "Query",
+      },
+    });
+  });
+
+  it("supports cache.identify(object)", () => {
+    const queryWithAliases: DocumentNode = gql`
+      query {
+        abcs {
+          first: a
+          second: b
+          ...Rest
+        }
+      }
+      fragment Rest on ABCs {
+        third: c
+      }
+    `;
+
+    const queryWithoutAliases: DocumentNode = gql`
+      query {
+        abcs {
+          a
+          b
+          ...Rest
+        }
+      }
+      fragment Rest on ABCs {
+        c
+      }
+    `;
+
+    const cache = new InMemoryCache({
+      typePolicies: {
+        ABCs: {
+          keyFields: ["b", "a", "c"],
+        },
+      },
+    });
+
+    const ABCs = {
+      __typename: "ABCs",
+      first: "ay",
+      second: "bee",
+      third: "see",
+    };
+
+    cache.writeQuery({
+      query: queryWithAliases,
+      data: {
+        abcs: ABCs,
+      },
+    });
+
+    expect(cache.extract()).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        abcs: {
+          __ref: 'ABCs:{"b":"bee","a":"ay","c":"see"}',
+        },
+      },
+      'ABCs:{"b":"bee","a":"ay","c":"see"}': {
+        __typename: "ABCs",
+        a: "ay",
+        b: "bee",
+        c: "see",
+      },
+    });
+
+    const resultWithAliases = cache.readQuery({
+      query: queryWithAliases,
+    });
+
+    expect(resultWithAliases).toEqual({ abcs: ABCs });
+
+    const resultWithoutAliases = cache.readQuery({
+      query: queryWithoutAliases,
+    });
+
+    expect(resultWithoutAliases).toEqual({
+      abcs: {
+        __typename: "ABCs",
+        a: "ay",
+        b: "bee",
+        c: "see",
+      },
+    });
+
+    expect(cache.identify({
+      __typename: "ABCs",
+      a: 1,
+      b: 2,
+      c: 3,
+    })).toBe('ABCs:{"b":2,"a":1,"c":3}');
+
+    expect(() => cache.identify(ABCs)).toThrow(
+      "Missing field b while computing key fields",
+    );
+
+    expect(cache.readFragment({
+      id: cache.identify({
+        __typename: "ABCs",
+        a: "ay",
+        b: "bee",
+        c: "see",
+      }),
+      fragment: gql`
+        fragment JustB on ABCs {
+          b
+        }
+      `,
+    })).toEqual({
+      __typename: "ABCs",
+      b: "bee",
+    });
+
+    expect(cache.readQuery({
+      query: queryWithAliases,
+    })).toBe(resultWithAliases);
+
+    expect(cache.readQuery({
+      query: queryWithoutAliases,
+    })).toBe(resultWithoutAliases);
+
+    cache.evict(cache.identify({
+      __typename: "ABCs",
+      a: "ay",
+      b: "bee",
+      c: "see",
+    }));
+
+    expect(cache.extract()).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        abcs: {
+          __ref: 'ABCs:{"b":"bee","a":"ay","c":"see"}',
+        },
+      },
+    });
+
+    expect(() => cache.readQuery({
+      query: queryWithAliases,
+    })).toThrow(/Can't find field a on object/);
+
+    expect(() => cache.readQuery({
+      query: queryWithoutAliases,
+    })).toThrow(/Can't find field a on object/);
+  });
+});
diff --git a/src/cache/inmemory/__tests__/fragmentMatcher.ts b/src/cache/inmemory/__tests__/fragmentMatcher.ts
new file mode 100644
--- /dev/null
+++ b/src/cache/inmemory/__tests__/fragmentMatcher.ts
@@ -0,0 +1,225 @@
+import gql from 'graphql-tag';
+
+import { InMemoryCache } from '../inMemoryCache';
+
+describe('fragment matching', () => {
+  it('can match exact types with or without possibleTypes', () => {
+    const cacheWithoutPossibleTypes = new InMemoryCache({
+      addTypename: true,
+    });
+
+    const cacheWithPossibleTypes = new InMemoryCache({
+      addTypename: true,
+      possibleTypes: {
+        Animal: ['Cat', 'Dog'],
+      },
+    });
+
+    const query = gql`
+      query AnimalNames {
+        animals {
+          id
+          name
+          ...CatDetails
+        }
+      }
+      fragment CatDetails on Cat {
+        livesLeft
+        killsToday
+      }
+    `;
+
+    const data = {
+      animals: [
+        {
+          __typename: 'Cat',
+          id: 1,
+          name: 'Felix',
+          livesLeft: 8,
+          killsToday: 2,
+        },
+        {
+          __typename: 'Dog',
+          id: 2,
+          name: 'Baxter',
+        },
+      ],
+    };
+
+    cacheWithoutPossibleTypes.writeQuery({ query, data });
+    expect(cacheWithoutPossibleTypes.readQuery({ query })).toEqual(data);
+
+    cacheWithPossibleTypes.writeQuery({ query, data });
+    expect(cacheWithPossibleTypes.readQuery({ query })).toEqual(data);
+  });
+
+  it('can match interface subtypes', () => {
+    const cache = new InMemoryCache({
+      addTypename: true,
+      possibleTypes: {
+        Animal: ['Cat', 'Dog'],
+      },
+    });
+
+    const query = gql`
+      query BestFriend {
+        bestFriend {
+          id
+          ...AnimalName
+        }
+      }
+      fragment AnimalName on Animal {
+        name
+      }
+    `;
+
+    const data = {
+      bestFriend: {
+        __typename: 'Dog',
+        id: 2,
+        name: 'Beckett',
+      },
+    };
+
+    cache.writeQuery({ query, data });
+    expect(cache.readQuery({ query })).toEqual(data);
+  });
+
+  it('can match union member types', () => {
+    const cache = new InMemoryCache({
+      addTypename: true,
+      possibleTypes: {
+        Status: ['PASSING', 'FAILING', 'SKIPPED'],
+      },
+    });
+
+    const query = gql`
+      query {
+        testResults {
+          id
+          output {
+            ... on Status {
+              stdout
+            }
+            ... on FAILING {
+              stderr
+            }
+          }
+        }
+      }
+    `;
+
+    const data = {
+      testResults: [
+        {
+          __typename: 'TestResult',
+          id: 123,
+          output: {
+            __typename: 'PASSING',
+            stdout: 'ok!',
+          },
+        },
+        {
+          __typename: 'TestResult',
+          id: 456,
+          output: {
+            __typename: 'FAILING',
+            stdout: '',
+            stderr: 'oh no',
+          },
+        },
+      ],
+    };
+
+    cache.writeQuery({ query, data });
+    expect(cache.readQuery({ query })).toEqual(data);
+  });
+
+  it('can match indirect subtypes while avoiding cycles', () => {
+    const cache = new InMemoryCache({
+      addTypename: true,
+      possibleTypes: {
+        Animal: ['Animal', 'Bug', 'Mammal'],
+        Bug: ['Ant', 'Spider', 'RolyPoly'],
+        Mammal: ['Dog', 'Cat', 'Human'],
+        Cat: ['Calico', 'Siamese', 'Sphynx', 'Tabby'],
+      },
+    });
+
+    const query = gql`
+      query {
+        animals {
+          ... on Mammal {
+            hasFur
+            bodyTemperature
+          }
+          ... on Bug {
+            isVenomous
+          }
+        }
+      }
+    `;
+
+    const data = {
+      animals: [
+        {
+          __typename: 'Sphynx',
+          hasFur: false,
+          bodyTemperature: 99,
+        },
+        {
+          __typename: 'Dog',
+          hasFur: true,
+          bodyTemperature: 102,
+        },
+        {
+          __typename: 'Spider',
+          isVenomous: 'maybe',
+        },
+      ],
+    };
+
+    cache.writeQuery({ query, data });
+    expect(cache.readQuery({ query })).toEqual(data);
+  });
+
+  it('can match against the root Query', () => {
+    const cache = new InMemoryCache({
+      addTypename: true,
+    });
+
+    const query = gql`
+      query AllPeople {
+        people {
+          id
+          name
+        }
+        ...PeopleTypes
+      }
+      fragment PeopleTypes on Query {
+        __type(name: "Person") {
+          name
+          kind
+        }
+      }
+    `;
+
+    const data = {
+      people: [
+        {
+          __typename: 'Person',
+          id: 123,
+          name: 'Ben',
+        },
+      ],
+      __type: {
+        __typename: '__Type',
+        name: 'Person',
+        kind: 'OBJECT',
+      },
+    };
+
+    cache.writeQuery({ query, data });
+    expect(cache.readQuery({ query })).toEqual(data);
+  });
+});
diff --git a/src/cache/inmemory/__tests__/optimistic.ts b/src/cache/inmemory/__tests__/optimistic.ts
new file mode 100644
--- /dev/null
+++ b/src/cache/inmemory/__tests__/optimistic.ts
@@ -0,0 +1,448 @@
+import gql from 'graphql-tag';
+
+import { InMemoryCache } from '../inMemoryCache';
+
+describe('optimistic cache layers', () => {
+  it('return === results for repeated reads', () => {
+    const cache = new InMemoryCache({
+      resultCaching: true,
+      dataIdFromObject(value: any) {
+        switch (value && value.__typename) {
+          case 'Book':
+            return 'Book:' + value.isbn;
+          case 'Author':
+            return 'Author:' + value.name;
+        }
+      },
+    });
+
+    const query = gql`
+      {
+        book {
+          title
+          author {
+            name
+          }
+        }
+      }
+    `;
+
+    function readOptimistic(cache: InMemoryCache) {
+      return cache.readQuery<{ book: any }>({ query }, true);
+    }
+
+    function readRealistic(cache: InMemoryCache) {
+      return cache.readQuery<{ book: any }>({ query }, false);
+    }
+
+    cache.writeQuery({
+      query,
+      data: {
+        book: {
+          __typename: 'Book',
+          isbn: '1980719802',
+          title: '1984',
+          author: {
+            __typename: 'Author',
+            name: 'George Orwell',
+          },
+        },
+      },
+    });
+
+    const result1984 = readOptimistic(cache);
+    expect(result1984).toEqual({
+      book: {
+        __typename: 'Book',
+        title: '1984',
+        author: {
+          __typename: 'Author',
+          name: 'George Orwell',
+        },
+      },
+    });
+
+    expect(result1984).toBe(readOptimistic(cache));
+    expect(result1984).toBe(readRealistic(cache));
+
+    let result2666InTransaction: ReturnType<typeof readOptimistic>;
+    cache.performTransaction(proxy => {
+      expect(readOptimistic(cache)).toEqual(result1984);
+
+      proxy.writeQuery({
+        query,
+        data: {
+          book: {
+            __typename: 'Book',
+            isbn: '0312429215',
+            title: '2666',
+            author: {
+              __typename: 'Author',
+              name: 'Roberto Bolaño',
+            },
+          },
+        },
+      });
+
+      result2666InTransaction = readOptimistic(proxy);
+      expect(result2666InTransaction).toEqual({
+        book: {
+          __typename: 'Book',
+          title: '2666',
+          author: {
+            __typename: 'Author',
+            name: 'Roberto Bolaño',
+          },
+        },
+      });
+    }, 'first');
+
+    expect(readOptimistic(cache)).toBe(result2666InTransaction);
+
+    expect(result1984).toBe(readRealistic(cache));
+
+    let resultCatch22: ReturnType<typeof readOptimistic>;
+    cache.performTransaction(proxy => {
+      proxy.writeQuery({
+        query,
+        data: {
+          book: {
+            __typename: 'Book',
+            isbn: '1451626657',
+            title: 'Catch-22',
+            author: {
+              __typename: 'Author',
+              name: 'Joseph Heller',
+            },
+          },
+        },
+      });
+
+      expect((resultCatch22 = readOptimistic(proxy))).toEqual({
+        book: {
+          __typename: 'Book',
+          title: 'Catch-22',
+          author: {
+            __typename: 'Author',
+            name: 'Joseph Heller',
+          },
+        },
+      });
+    }, 'second');
+
+    expect(readOptimistic(cache)).toBe(resultCatch22);
+
+    expect(result1984).toBe(readRealistic(cache));
+
+    cache.removeOptimistic('first');
+
+    expect(readOptimistic(cache)).toBe(resultCatch22);
+
+    // Write a new book to the root Query.book field, which should not affect
+    // the 'second' optimistic layer that is still applied.
+    cache.writeQuery({
+      query,
+      data: {
+        book: {
+          __typename: 'Book',
+          isbn: '9781451673319',
+          title: 'Fahrenheit 451',
+          author: {
+            __typename: 'Author',
+            name: 'Ray Bradbury',
+          },
+        },
+      },
+    });
+
+    expect(readOptimistic(cache)).toBe(resultCatch22);
+
+    const resultF451 = readRealistic(cache);
+    expect(resultF451).toEqual({
+      book: {
+        __typename: 'Book',
+        title: 'Fahrenheit 451',
+        author: {
+          __typename: 'Author',
+          name: 'Ray Bradbury',
+        },
+      },
+    });
+
+    cache.removeOptimistic('second');
+
+    expect(resultF451).toBe(readRealistic(cache));
+    expect(resultF451).toBe(readOptimistic(cache));
+
+    expect(cache.extract(true)).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        book: { __ref: 'Book:9781451673319' },
+      },
+      'Book:1980719802': {
+        title: '1984',
+        author: { __ref: 'Author:George Orwell' },
+        __typename: 'Book',
+      },
+      'Book:9781451673319': {
+        title: 'Fahrenheit 451',
+        author: { __ref: 'Author:Ray Bradbury' },
+        __typename: 'Book',
+      },
+      'Author:George Orwell': {
+        __typename: 'Author',
+        name: 'George Orwell',
+      },
+      'Author:Ray Bradbury': {
+        __typename: 'Author',
+        name: 'Ray Bradbury',
+      },
+    });
+  });
+
+  it('dirties appropriate IDs when optimistic layers are removed', () => {
+    const cache = new InMemoryCache({
+      resultCaching: true,
+      dataIdFromObject(value: any) {
+        switch (value && value.__typename) {
+          case 'Book':
+            return 'Book:' + value.isbn;
+          case 'Author':
+            return 'Author:' + value.name;
+        }
+      },
+    });
+
+    type Q = {
+      books: any[];
+    };
+
+    const query = gql`
+      {
+        books {
+          title
+          subtitle
+        }
+      }
+    `;
+
+    const eagerBookData = {
+      __typename: 'Book',
+      isbn: '1603589082',
+      title: 'Eager',
+      subtitle: 'The Surprising, Secret Life of Beavers and Why They Matter',
+      author: {
+        __typename: 'Author',
+        name: 'Ben Goldfarb',
+      },
+    };
+
+    const spinelessBookData = {
+      __typename: 'Book',
+      isbn: '0735211280',
+      title: 'Spineless',
+      subtitle: 'The Science of Jellyfish and the Art of Growing a Backbone',
+      author: {
+        __typename: 'Author',
+        name: 'Juli Berwald',
+      },
+    };
+
+    cache.writeQuery({
+      query,
+      data: {
+        books: [eagerBookData, spinelessBookData],
+      },
+    });
+
+    expect(cache.extract(true)).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        books: [{ __ref: 'Book:1603589082' }, { __ref: 'Book:0735211280' }],
+      },
+      'Book:1603589082': {
+        title: 'Eager',
+        subtitle: eagerBookData.subtitle,
+        __typename: 'Book',
+      },
+      'Book:0735211280': {
+        title: 'Spineless',
+        subtitle: spinelessBookData.subtitle,
+        __typename: 'Book',
+      },
+    });
+
+    function read() {
+      return cache.readQuery<Q>({ query }, true);
+    }
+
+    const result = read();
+    expect(result).toEqual({
+      books: [
+        {
+          __typename: 'Book',
+          title: 'Eager',
+          subtitle: 'The Surprising, Secret Life of Beavers and Why They Matter',
+        },
+        {
+          __typename: 'Book',
+          title: 'Spineless',
+          subtitle: 'The Science of Jellyfish and the Art of Growing a Backbone',
+        },
+      ],
+    });
+    expect(read()).toBe(result);
+
+    const bookAuthorNameFragment = gql`
+      fragment BookAuthorName on Book {
+        author {
+          name
+        }
+      }
+    `;
+
+    cache.writeFragment({
+      id: 'Book:0735211280',
+      fragment: bookAuthorNameFragment,
+      data: {
+        author: spinelessBookData.author,
+      },
+    });
+
+    // Adding an author doesn't change the structure of the original result,
+    // because the original query did not ask for author information.
+    const resultWithSpinlessAuthor = read();
+    expect(resultWithSpinlessAuthor).toEqual(result);
+    expect(resultWithSpinlessAuthor).toBe(result);
+    expect(resultWithSpinlessAuthor.books[0]).toBe(result.books[0]);
+    expect(resultWithSpinlessAuthor.books[1]).toBe(result.books[1]);
+
+    cache.recordOptimisticTransaction(proxy => {
+      proxy.writeFragment({
+        id: 'Book:1603589082',
+        fragment: bookAuthorNameFragment,
+        data: {
+          author: eagerBookData.author,
+        },
+      });
+    }, 'eager author');
+
+    expect(read()).toEqual(result);
+
+    const queryWithAuthors = gql`
+      {
+        books {
+          title
+          subtitle
+          author {
+            name
+          }
+        }
+      }
+    `;
+
+    function readWithAuthors(optimistic = true) {
+      return cache.readQuery<Q>({
+        query: queryWithAuthors,
+      }, optimistic);
+    }
+
+    function withoutISBN(data: any) {
+      return JSON.parse(JSON.stringify(
+        data,
+        (key, value) => {
+          if (key === 'isbn') return;
+          return value;
+        },
+      ));
+    }
+
+    const resultWithTwoAuthors = readWithAuthors();
+    expect(resultWithTwoAuthors).toEqual({
+      books: [
+        withoutISBN(eagerBookData),
+        withoutISBN(spinelessBookData),
+      ],
+    });
+
+    const buzzBookData = {
+      __typename: 'Book',
+      isbn: '0465052614',
+      title: 'Buzz',
+      subtitle: 'The Nature and Necessity of Bees',
+      author: {
+        __typename: 'Author',
+        name: 'Thor Hanson',
+      },
+    };
+
+    cache.recordOptimisticTransaction(proxy => {
+      proxy.writeQuery({
+        query: queryWithAuthors,
+        data: {
+          books: [
+            eagerBookData,
+            spinelessBookData,
+            buzzBookData,
+          ],
+        },
+      });
+    }, 'buzz book');
+
+    const resultWithBuzz = readWithAuthors();
+
+    expect(resultWithBuzz).toEqual({
+      books: [
+        withoutISBN(eagerBookData),
+        withoutISBN(spinelessBookData),
+        withoutISBN(buzzBookData),
+      ],
+    });
+    expect(resultWithBuzz.books[0]).toEqual(resultWithTwoAuthors.books[0]);
+    expect(resultWithBuzz.books[1]).toEqual(resultWithTwoAuthors.books[1]);
+
+    // Before removing the Buzz optimistic layer from the cache, write the same
+    // data to the root layer of the cache.
+    cache.writeQuery({
+      query: queryWithAuthors,
+      data: {
+        books: [eagerBookData, spinelessBookData, buzzBookData],
+      },
+    });
+
+    expect(readWithAuthors()).toBe(resultWithBuzz);
+
+    function readSpinelessFragment() {
+      return cache.readFragment<{ author: any }>(
+        {
+          id: 'Book:' + spinelessBookData.isbn,
+          fragment: bookAuthorNameFragment,
+        },
+        true,
+      );
+    }
+
+    const spinelessBeforeRemovingBuzz = readSpinelessFragment();
+    cache.removeOptimistic('buzz book');
+    const spinelessAfterRemovingBuzz = readSpinelessFragment();
+    expect(spinelessBeforeRemovingBuzz).toEqual(spinelessAfterRemovingBuzz);
+    expect(spinelessBeforeRemovingBuzz).not.toBe(spinelessAfterRemovingBuzz);
+    expect(spinelessBeforeRemovingBuzz.author).toBe(
+      spinelessAfterRemovingBuzz.author,
+    );
+
+    const resultAfterRemovingBuzzLayer = readWithAuthors();
+    expect(resultAfterRemovingBuzzLayer).toEqual(resultWithBuzz);
+    expect(resultAfterRemovingBuzzLayer).not.toBe(resultWithBuzz);
+    resultWithTwoAuthors.books.forEach((book, i) => {
+      expect(book).toEqual(resultAfterRemovingBuzzLayer.books[i]);
+      expect(book).toBe(resultAfterRemovingBuzzLayer.books[i]);
+    });
+
+    const nonOptimisticResult = readWithAuthors(false);
+    expect(nonOptimisticResult).toEqual(resultWithBuzz);
+    cache.removeOptimistic('eager author');
+    const resultWithoutOptimisticLayers = readWithAuthors();
+    expect(resultWithoutOptimisticLayers).toBe(nonOptimisticResult);
+  });
+});
diff --git a/src/cache/inmemory/__tests__/policies.ts b/src/cache/inmemory/__tests__/policies.ts
new file mode 100644
--- /dev/null
+++ b/src/cache/inmemory/__tests__/policies.ts
@@ -0,0 +1,2525 @@
+import gql from "graphql-tag";
+import { InMemoryCache } from "../inMemoryCache";
+import { StoreValue } from "../../../utilities";
+import { FieldPolicy, Policies } from "../policies";
+import { Reference } from "../../../utilities/graphql/storeUtils";
+
+function reverse(s: string) {
+  return s.split("").reverse().join("");
+}
+
+describe("type policies", function () {
+  const bookQuery = gql`
+    query {
+      book {
+        title
+        author {
+          name
+        }
+      }
+    }
+  `;
+
+  const theInformationBookData = {
+    __typename: "Book",
+    isbn: "1400096235",
+    title: "The Information",
+    subtitle: "A History, a Theory, a Flood",
+    author: {
+      name: "James Gleick"
+    },
+  };
+
+  function checkAuthorName(cache: InMemoryCache) {
+    expect(cache.readQuery({
+      query: gql`
+        query {
+          book {
+            author {
+              name
+            }
+          }
+        }
+      `,
+    })).toEqual({
+      book: {
+        __typename: "Book",
+        author: {
+          name: theInformationBookData.author.name,
+        },
+      },
+    });
+  }
+
+  it("can specify basic keyFields", function () {
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Book: {
+          keyFields: ["isbn"],
+        },
+      },
+    });
+
+    cache.writeQuery({
+      query: bookQuery,
+      data: {
+        book: theInformationBookData,
+      },
+    });
+
+    expect(cache.extract(true)).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        book: {
+          __ref: 'Book:{"isbn":"1400096235"}',
+        },
+      },
+      'Book:{"isbn":"1400096235"}': {
+        __typename: "Book",
+        title: "The Information",
+        author: {
+          name: "James Gleick"
+        },
+      },
+    });
+
+    checkAuthorName(cache);
+  });
+
+  it("can specify composite keyFields", function () {
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Book: {
+          keyFields: ["title", "author", ["name"]],
+        },
+      },
+    });
+
+    cache.writeQuery({
+      query: bookQuery,
+      data: {
+        book: theInformationBookData,
+      },
+    });
+
+    expect(cache.extract(true)).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        book: {
+          __ref: 'Book:{"title":"The Information","author":{"name":"James Gleick"}}',
+        },
+      },
+      'Book:{"title":"The Information","author":{"name":"James Gleick"}}': {
+        __typename: "Book",
+        title: "The Information",
+        author: {
+          name: "James Gleick"
+        },
+      },
+    });
+
+    checkAuthorName(cache);
+  });
+
+  it("keeps keyFields in specified order", function () {
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Book: {
+          keyFields: ["author", ["name"], "title"],
+        },
+      },
+    });
+
+    cache.writeQuery({
+      query: bookQuery,
+      data: {
+        book: theInformationBookData,
+      },
+    });
+
+    expect(cache.extract(true)).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        book: {
+          __ref: 'Book:{"author":{"name":"James Gleick"},"title":"The Information"}',
+        },
+      },
+      'Book:{"author":{"name":"James Gleick"},"title":"The Information"}': {
+        __typename: "Book",
+        title: "The Information",
+        author: {
+          name: "James Gleick"
+        },
+      },
+    });
+
+    checkAuthorName(cache);
+  });
+
+  it("accepts keyFields functions", function () {
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Book: {
+          keyFields(book, context) {
+            expect(context.selectionSet.kind).toBe("SelectionSet");
+            expect(context.fragmentMap).toEqual({});
+            expect(context.policies).toBeInstanceOf(Policies);
+            return context.typename + ":" + book.isbn;
+          },
+        },
+      },
+    });
+
+    cache.writeQuery({
+      query: bookQuery,
+      data: {
+        book: theInformationBookData,
+      },
+    });
+
+    expect(cache.extract(true)).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        book: {
+          __ref: "Book:1400096235",
+        },
+      },
+      "Book:1400096235": {
+        __typename: "Book",
+        title: "The Information",
+        author: {
+          name: "James Gleick"
+        },
+      },
+    });
+
+    checkAuthorName(cache);
+  });
+
+  it("works with fragments that contain aliased key fields", function () {
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Book: {
+          keyFields: ["ISBN", "title"],
+        },
+      },
+    });
+
+    cache.writeQuery({
+      query: gql`
+        query {
+          book {
+            ...BookFragment
+            author {
+              name
+            }
+          }
+        }
+        fragment BookFragment on Book {
+          isbn: ISBN
+          title
+        }
+      `,
+      data: {
+        book: theInformationBookData,
+      },
+    });
+
+    expect(cache.extract(true)).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        book: {
+          __ref: 'Book:{"ISBN":"1400096235","title":"The Information"}',
+        },
+      },
+      'Book:{"ISBN":"1400096235","title":"The Information"}': {
+        __typename: "Book",
+        ISBN: "1400096235",
+        title: "The Information",
+        author: {
+          name: "James Gleick"
+        },
+      },
+    });
+
+    checkAuthorName(cache);
+  });
+
+  it("complains about missing key fields", function () {
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Book: {
+          keyFields: ["title", "year"],
+        },
+      },
+    });
+
+    const query = gql`
+      query {
+        book {
+          title
+          year
+        }
+      }
+    `;
+
+    cache.writeQuery({
+      query,
+      data: {
+        book: {
+          year: 2011,
+          theInformationBookData,
+        },
+      },
+    });
+
+    expect(() => {
+      cache.writeQuery({
+        query,
+        data: {
+          book: theInformationBookData,
+        },
+      });
+    }).toThrow("Missing field year while computing key fields");
+  });
+
+  describe("field policies", function () {
+    it("can filter key arguments", function () {
+      const cache = new InMemoryCache({
+        typePolicies: {
+          Query: {
+            fields: {
+              book: {
+                keyArgs: ["isbn"],
+              },
+            },
+          },
+        },
+      });
+
+      cache.writeQuery({
+        query: gql`
+          query {
+            book(junk: "ignored", isbn: "0465030793") {
+              title
+            }
+          }
+        `,
+        data: {
+          book: {
+            __typename: "Book",
+            isbn: "0465030793",
+            title: "I Am a Strange Loop",
+          },
+        },
+      });
+
+      expect(cache.extract(true)).toEqual({
+        ROOT_QUERY: {
+          __typename: "Query",
+          'book:{"isbn":"0465030793"}': {
+            __typename: "Book",
+            title: "I Am a Strange Loop",
+          },
+        },
+      });
+    });
+
+    it("can filter key arguments in non-Query fields", function () {
+      const cache = new InMemoryCache({
+        typePolicies: {
+          Book: {
+            keyFields: ["isbn"],
+            fields: {
+              author: {
+                keyArgs: ["firstName", "lastName"],
+              },
+            },
+          },
+          Author: {
+            keyFields: ["name"],
+          },
+        },
+      });
+
+      const query = gql`
+        query {
+          book {
+            isbn
+            title
+            author(
+              firstName: "Douglas",
+              middleName: "Richard",
+              lastName: "Hofstadter"
+            ) {
+              name
+            }
+          }
+        }
+      `;
+
+      const data = {
+        book: {
+          __typename: "Book",
+          isbn: "0465030793",
+          title: "I Am a Strange Loop",
+          author: {
+            __typename: "Author",
+            name: "Douglas Hofstadter",
+          },
+        },
+      };
+
+      cache.writeQuery({ query, data });
+
+      expect(cache.extract(true)).toEqual({
+        ROOT_QUERY: {
+          __typename: "Query",
+          book: {
+            __ref: 'Book:{"isbn":"0465030793"}',
+          },
+        },
+        'Book:{"isbn":"0465030793"}': {
+          __typename: "Book",
+          isbn: "0465030793",
+          title: "I Am a Strange Loop",
+          'author:{"firstName":"Douglas","lastName":"Hofstadter"}': {
+            __ref: 'Author:{"name":"Douglas Hofstadter"}',
+          },
+        },
+        'Author:{"name":"Douglas Hofstadter"}': {
+          __typename: "Author",
+          name: "Douglas Hofstadter",
+        },
+      });
+
+      const result = cache.readQuery({ query });
+      expect(result).toEqual(data);
+    });
+
+    it("assumes keyArgs:false when read or merge function present", function () {
+      const cache = new InMemoryCache({
+        typePolicies: {
+          TypeA: {
+            fields: {
+              a() {
+                return "a";
+              },
+            },
+          },
+
+          TypeB: {
+            fields: {
+              b: {
+                keyArgs: ["x"],
+                read() {
+                  return "b";
+                },
+              },
+            },
+          },
+
+          TypeC: {
+            fields: {
+              c: {
+                keyArgs: false,
+                merge(existing, incoming: string) {
+                  return reverse(incoming);
+                },
+              },
+            },
+          },
+
+          TypeD: {
+            fields: {
+              d: {
+                keyArgs(field) {
+                  return "d";
+                },
+                read(existing: string) {
+                  return existing.toLowerCase();
+                },
+                merge(existing: string, incoming: string) {
+                  return incoming.toUpperCase();
+                },
+              },
+            },
+          },
+
+          TypeE: {
+            fields: {
+              e: {
+                read(existing: string) {
+                  return existing.slice(1);
+                },
+                merge(existing: string, incoming: string) {
+                  return "*" + incoming;
+                },
+              },
+            },
+          },
+
+          TypeF: {
+            fields: {
+              f: {
+                // nothing
+              },
+            },
+          },
+
+          Query: {
+            fields: {
+              types(existing: any[], { args }) {
+                const fromCode = args.from.charCodeAt(0);
+                const toCode = args.to.charCodeAt(0);
+                let e = 0;
+                for (let code = fromCode; code <= toCode; ++code) {
+                  const upper = String.fromCharCode(code).toUpperCase();
+                  const obj = existing[e++];
+                  expect(obj.__typename).toBe("Type" + upper);
+                }
+                return existing;
+              },
+            },
+          },
+        },
+      });
+
+      const query = gql`
+        query {
+          types(from: "A", to: "F") {
+            ... on TypeA { a }
+            ... on TypeB { b(x: 1, y: 2, z: 3) }
+            ... on TypeC { c(see: "si") }
+            ... on TypeD { d }
+            ... on TypeE { e(eee: "ee") }
+            ... on TypeF { f(g: "h") }
+          }
+        }
+      `;
+
+      cache.writeQuery({
+        query,
+        data: {
+          types: [{
+            __typename: "TypeA",
+          }, {
+            __typename: "TypeB",
+            b: "x1",
+          }, {
+            __typename: "TypeC",
+            c: "naive",
+          }, {
+            __typename: "TypeD",
+            d: "quiet",
+          }, {
+            __typename: "TypeE",
+            e: "asterisk",
+          }, {
+            __typename: "TypeF",
+            f: "effigy",
+          }],
+        },
+      });
+
+      expect(cache.extract()).toEqual({
+        ROOT_QUERY: {
+          __typename: "Query",
+          types: [
+            {
+              __typename: "TypeA",
+            },
+            {
+              __typename: "TypeB",
+              'b:{"x":1}': "x1",
+            },
+            {
+              __typename: "TypeC",
+              c: "evian",
+            },
+            {
+              __typename: "TypeD",
+              d: "QUIET",
+            },
+            {
+              __typename: "TypeE",
+              e: "*asterisk",
+            },
+            {
+              __typename: "TypeF",
+              'f({"g":"h"})': "effigy",
+            },
+          ],
+        },
+      });
+
+      const result = cache.readQuery({ query });
+      expect(result).toEqual({
+        types: [
+          {
+            __typename: "TypeA",
+            a: "a",
+          }, {
+            __typename: "TypeB",
+            b: "b",
+          }, {
+            __typename: "TypeC",
+            c: "evian",
+          }, {
+            __typename: "TypeD",
+            d: "quiet",
+          }, {
+            __typename: "TypeE",
+            e: "asterisk",
+          }, {
+            __typename: "TypeF",
+            f: "effigy",
+          }
+        ],
+      });
+    });
+
+    it("can use stable storage in read functions", function () {
+      const storageSet = new Set<Record<string, any>>();
+
+      const cache = new InMemoryCache({
+        typePolicies: {
+          Task: {
+            fields: {
+              result(existing, { args, storage }) {
+                storageSet.add(storage);
+                if (storage.result) return storage.result;
+                return storage.result = compute(args);
+              },
+            },
+          },
+        },
+      });
+
+      let computeCount = 0;
+      function compute(args) {
+        return `expensive result ${++computeCount}`;
+      }
+
+      cache.writeQuery({
+        query: gql`
+          query {
+            tasks {
+              id
+            }
+          }
+        `,
+        data: {
+          tasks: [{
+            __typename: "Task",
+            id: 1,
+          }, {
+            __typename: "Task",
+            id: 2,
+          }],
+        },
+      });
+
+      expect(cache.extract()).toEqual({
+        ROOT_QUERY: {
+          __typename: "Query",
+          tasks: [
+            { __ref: "Task:1" },
+            { __ref: "Task:2" },
+          ],
+        },
+        "Task:1": {
+          __typename: "Task",
+          id: 1,
+        },
+        "Task:2": {
+          __typename: "Task",
+          id: 2,
+        },
+      });
+
+      const result1 = cache.readQuery({
+        query: gql`
+          query {
+            tasks {
+              result
+            }
+          }
+        `,
+      });
+
+      expect(result1).toEqual({
+        tasks: [{
+          __typename: "Task",
+          result: "expensive result 1",
+        }, {
+          __typename: "Task",
+          result: "expensive result 2",
+        }],
+      });
+
+      const result2 = cache.readQuery({
+        query: gql`
+          query {
+            tasks {
+              id
+              result
+            }
+          }
+        `,
+      });
+
+      expect(result2).toEqual({
+        tasks: [{
+          __typename: "Task",
+          id: 1,
+          result: "expensive result 1",
+        }, {
+          __typename: "Task",
+          id: 2,
+          result: "expensive result 2",
+        }],
+      });
+
+      // Clear the cached results.
+      storageSet.forEach(storage => {
+        delete storage.result;
+      });
+
+      const result3 = cache.readQuery({
+        query: gql`
+          query {
+            tasks {
+              __typename
+              result
+            }
+          }
+        `,
+      });
+
+      expect(result3).toEqual({
+        tasks: [{
+          __typename: "Task",
+          result: "expensive result 3",
+        }, {
+          __typename: "Task",
+          result: "expensive result 4",
+        }],
+      });
+    });
+
+    it("can use read function to implement synthetic/computed keys", function () {
+      const cache = new InMemoryCache({
+        typePolicies: {
+          Person: {
+            keyFields: ["firstName", "lastName"],
+            fields: {
+              fullName(_, { readField }) {
+                const firstName = readField("firstName");
+                const lastName = readField("lastName");
+                return `${firstName} ${lastName}`;
+              },
+            },
+          },
+        },
+      });
+
+      cache.writeQuery({
+        query: gql`
+          query {
+            me {
+              firstName
+              lastName
+            }
+          }
+        `,
+        data: {
+          me: {
+            __typename: "Person",
+            firstName: "Ben",
+            lastName: "Newman",
+          },
+        },
+      });
+
+      const expectedExtraction = {
+        ROOT_QUERY: {
+          __typename: "Query",
+          me: {
+            __ref: 'Person:{"firstName":"Ben","lastName":"Newman"}',
+          },
+        },
+        'Person:{"firstName":"Ben","lastName":"Newman"}': {
+          __typename: "Person",
+          firstName: "Ben",
+          lastName: "Newman",
+        },
+      };
+
+      expect(cache.extract(true)).toEqual(expectedExtraction);
+
+      const expectedResult = {
+        me: {
+          __typename: "Person",
+          fullName: "Ben Newman",
+        },
+      };
+
+      expect(cache.readQuery({
+        query: gql`
+          query {
+            me {
+              fullName
+            }
+          }
+        `,
+      })).toEqual(expectedResult);
+
+      expect(cache.readQuery({
+        query: gql`
+          query {
+            me {
+              fullName @client
+            }
+          }
+        `,
+      })).toEqual(expectedResult);
+
+      expect(cache.extract(true)).toEqual(expectedExtraction);
+    });
+
+    it("read and merge can cooperate through options.storage", function () {
+      const cache = new InMemoryCache({
+        typePolicies: {
+          Query: {
+            fields: {
+              jobs: {
+                merge(existing: any[] = [], incoming: any[]) {
+                  return [...existing, ...incoming];
+                },
+              },
+            },
+          },
+
+          Job: {
+            keyFields: ["name"],
+            fields: {
+              result: {
+                read(_, { storage }) {
+                  return storage.result;
+                },
+                merge(_, incoming, { storage, invalidate }) {
+                  storage.result = incoming;
+                  invalidate();
+                },
+              },
+            },
+          },
+        },
+      });
+
+      const query = gql`
+        query {
+          jobs {
+            name
+            result
+          }
+        }
+      `;
+
+      cache.writeQuery({
+        query,
+        data: {
+          jobs: [{
+            __typename: "Job",
+            name: "Job #1",
+            // intentionally omitting the result field
+          }, {
+            __typename: "Job",
+            name: "Job #2",
+            // intentionally omitting the result field
+          }, {
+            __typename: "Job",
+            name: "Job #3",
+            // intentionally omitting the result field
+          }],
+        },
+      });
+
+      const snapshot1 = {
+        ROOT_QUERY: {
+          __typename: "Query",
+          jobs: [
+            { __ref: 'Job:{"name":"Job #1"}' },
+            { __ref: 'Job:{"name":"Job #2"}' },
+            { __ref: 'Job:{"name":"Job #3"}' },
+          ],
+        },
+        'Job:{"name":"Job #1"}': {
+          __typename: "Job",
+          name: "Job #1",
+        },
+        'Job:{"name":"Job #2"}': {
+          __typename: "Job",
+          name: "Job #2",
+        },
+        'Job:{"name":"Job #3"}': {
+          __typename: "Job",
+          name: "Job #3",
+        },
+      };
+
+      expect(cache.extract()).toEqual(snapshot1);
+
+      expect(cache.diff({
+        query,
+        optimistic: false,
+        returnPartialData: true,
+      })).toEqual({
+        result: {
+          jobs: [{
+            __typename: "Job",
+            name: "Job #1",
+          }, {
+            __typename: "Job",
+            name: "Job #2",
+          }, {
+            __typename: "Job",
+            name: "Job #3",
+          }],
+        },
+        complete: false,
+      });
+
+      function setResult(jobNum: number) {
+        cache.writeFragment({
+          id: cache.identify({
+            __typename: "Job",
+            name: `Job #${jobNum}`,
+          }),
+          fragment: gql`
+            fragment JobResult on Job {
+              result
+            }
+          `,
+          data: {
+            __typename: "Job",
+            result: `result for job ${jobNum}`,
+          },
+        });
+      }
+
+      setResult(2);
+
+      // Nothing should have changed in the cache itself as a result of
+      // writing a result for job #2.
+      expect(cache.extract()).toEqual(snapshot1);
+
+      expect(cache.diff({
+        query,
+        optimistic: false,
+        returnPartialData: true,
+      })).toEqual({
+        result: {
+          jobs: [{
+            __typename: "Job",
+            name: "Job #1",
+          }, {
+            __typename: "Job",
+            name: "Job #2",
+            result: "result for job 2",
+          }, {
+            __typename: "Job",
+            name: "Job #3",
+          }],
+        },
+        complete: false,
+      });
+
+      cache.writeQuery({
+        query,
+        data: {
+          jobs: [{
+            __typename: "Job",
+            name: "Job #4",
+            result: "result for job 4",
+          }],
+        },
+      });
+
+      const snapshot2 = {
+        ...snapshot1,
+        ROOT_QUERY: {
+          ...snapshot1.ROOT_QUERY,
+          jobs: [
+            ...snapshot1.ROOT_QUERY.jobs,
+            { __ref: 'Job:{"name":"Job #4"}' },
+          ],
+        },
+        'Job:{"name":"Job #4"}': {
+          __typename: "Job",
+          name: "Job #4",
+        },
+      };
+
+      expect(cache.extract()).toEqual(snapshot2);
+
+      expect(cache.diff({
+        query,
+        optimistic: false,
+        returnPartialData: true,
+      })).toEqual({
+        result: {
+          jobs: [{
+            __typename: "Job",
+            name: "Job #1",
+          }, {
+            __typename: "Job",
+            name: "Job #2",
+            result: "result for job 2",
+          }, {
+            __typename: "Job",
+            name: "Job #3",
+          }, {
+            __typename: "Job",
+            name: "Job #4",
+            result: "result for job 4",
+          }],
+        },
+        complete: false,
+      });
+
+      setResult(1);
+      setResult(3);
+
+      expect(cache.diff({
+        query,
+        optimistic: false,
+        returnPartialData: true,
+      })).toEqual({
+        result: {
+          jobs: [{
+            __typename: "Job",
+            name: "Job #1",
+            result: "result for job 1",
+          }, {
+            __typename: "Job",
+            name: "Job #2",
+            result: "result for job 2",
+          }, {
+            __typename: "Job",
+            name: "Job #3",
+            result: "result for job 3",
+          }, {
+            __typename: "Job",
+            name: "Job #4",
+            result: "result for job 4",
+          }],
+        },
+        complete: true,
+      });
+
+      expect(cache.readQuery({ query })).toEqual({
+        jobs: [{
+          __typename: "Job",
+          name: "Job #1",
+          result: "result for job 1",
+        }, {
+          __typename: "Job",
+          name: "Job #2",
+          result: "result for job 2",
+        }, {
+          __typename: "Job",
+          name: "Job #3",
+          result: "result for job 3",
+        }, {
+          __typename: "Job",
+          name: "Job #4",
+          result: "result for job 4",
+        }],
+      });
+    });
+
+    it("merge functions can deduplicate items using readField", function () {
+      const cache = new InMemoryCache({
+        typePolicies: {
+          Query: {
+            fields: {
+              books: {
+                merge(existing: any[] = [], incoming: any[], {
+                  readField,
+                }) {
+                  if (existing) {
+                    const merged = existing.slice(0);
+                    const existingIsbnSet =
+                      new Set(merged.map(book => readField("isbn", book)));
+                    incoming.forEach(book => {
+                      const isbn = readField("isbn", book);
+                      if (!existingIsbnSet.has(isbn)) {
+                        existingIsbnSet.add(isbn);
+                        merged.push(book);
+                      }
+                    });
+                    return merged;
+                  }
+                  return incoming;
+                },
+
+                // Returns the books array, sorted by title.
+                read(existing: any[], { readField }) {
+                  if (existing) {
+                    return existing.slice(0).sort((a, b) => {
+                      const aTitle = readField<string>("title", a);
+                      const bTitle = readField<string>("title", b);
+                      if (aTitle === bTitle) return 0;
+                      if (aTitle < bTitle) return -1;
+                      return 1;
+                    });
+                  }
+                  return [];
+                },
+              },
+            },
+          },
+
+          Book: {
+            keyFields: ["isbn"],
+          },
+        },
+      });
+
+      const query = gql`
+        query {
+          books {
+            isbn
+            title
+          }
+        }
+      `;
+
+      const programmingRustBook = {
+        __typename: "Book",
+        isbn: "9781491927281",
+        title: "Programming Rust: Fast, Safe Systems Development",
+      };
+
+      const officialRustBook = {
+        __typename: "Book",
+        isbn: "1593278284",
+        title: "The Rust Programming Language",
+      };
+
+      const handsOnConcurrencyBook = {
+        __typename: "Book",
+        isbn: "1788399978",
+        title: "Hands-On Concurrency with Rust",
+      };
+
+      const wasmWithRustBook = {
+        __typename: "Book",
+        isbn: "1680506366",
+        title: "Programming WebAssembly with Rust",
+      };
+
+      function addBooks(...books: (typeof programmingRustBook)[]) {
+        cache.writeQuery({
+          query,
+          data: {
+            books,
+          },
+        });
+      }
+
+      addBooks(officialRustBook);
+
+      expect(cache.extract()).toEqual({
+        ROOT_QUERY: {
+          __typename: "Query",
+          books: [
+            { __ref: 'Book:{"isbn":"1593278284"}' },
+          ],
+        },
+        'Book:{"isbn":"1593278284"}': {
+          __typename: "Book",
+          isbn: "1593278284",
+          title: "The Rust Programming Language",
+        },
+      });
+
+      addBooks(
+        programmingRustBook,
+        officialRustBook,
+      );
+
+      expect(cache.extract()).toEqual({
+        ROOT_QUERY: {
+          __typename: "Query",
+          books: [
+            { __ref: 'Book:{"isbn":"1593278284"}' },
+            { __ref: 'Book:{"isbn":"9781491927281"}' },
+          ],
+        },
+        'Book:{"isbn":"1593278284"}': officialRustBook,
+        'Book:{"isbn":"9781491927281"}': programmingRustBook,
+      });
+
+      addBooks(
+        wasmWithRustBook,
+        wasmWithRustBook,
+        programmingRustBook,
+        wasmWithRustBook,
+      );
+
+      expect(cache.extract()).toEqual({
+        ROOT_QUERY: {
+          __typename: "Query",
+          books: [
+            { __ref: 'Book:{"isbn":"1593278284"}' },
+            { __ref: 'Book:{"isbn":"9781491927281"}' },
+            { __ref: 'Book:{"isbn":"1680506366"}' },
+          ],
+        },
+        'Book:{"isbn":"1593278284"}': officialRustBook,
+        'Book:{"isbn":"9781491927281"}': programmingRustBook,
+        'Book:{"isbn":"1680506366"}': wasmWithRustBook,
+      });
+
+      addBooks(
+        programmingRustBook,
+        officialRustBook,
+        handsOnConcurrencyBook,
+        wasmWithRustBook,
+      );
+
+      expect(cache.extract()).toEqual({
+        ROOT_QUERY: {
+          __typename: "Query",
+          books: [
+            { __ref: 'Book:{"isbn":"1593278284"}' },
+            { __ref: 'Book:{"isbn":"9781491927281"}' },
+            { __ref: 'Book:{"isbn":"1680506366"}' },
+            { __ref: 'Book:{"isbn":"1788399978"}' },
+          ],
+        },
+        'Book:{"isbn":"1593278284"}': officialRustBook,
+        'Book:{"isbn":"9781491927281"}': programmingRustBook,
+        'Book:{"isbn":"1680506366"}': wasmWithRustBook,
+        'Book:{"isbn":"1788399978"}': handsOnConcurrencyBook,
+      });
+
+      expect(cache.readQuery({ query })).toEqual({
+        // Note that these books have been sorted by title, thanks to the
+        // custom read function we defined above.
+        "books": [
+          {
+            "__typename": "Book",
+            "isbn": "1788399978",
+            "title": "Hands-On Concurrency with Rust",
+          },
+          {
+            "__typename": "Book",
+            "isbn": "9781491927281",
+            "title": "Programming Rust: Fast, Safe Systems Development",
+          },
+          {
+            "__typename": "Book",
+            "isbn": "1680506366",
+            "title": "Programming WebAssembly with Rust",
+          },
+          {
+            "__typename": "Book",
+            "isbn": "1593278284",
+            "title": "The Rust Programming Language",
+          },
+        ],
+      });
+    });
+
+    it("readField helper function calls custom read functions", function () {
+      // Rather than writing ownTime data into the cache, we maintain it
+      // externally in this object:
+      const ownTimes = {
+        "parent task": 2,
+        "child task 1": 3,
+        "child task 2": 4,
+        "grandchild task": 5,
+        "independent task": 11,
+      };
+
+      const invalidators: Record<string, () => void> = {};
+
+      const cache = new InMemoryCache({
+        typePolicies: {
+          Agenda: {
+            fields: {
+              taskCount(_, { readField }) {
+                return readField<Reference[]>("tasks").length;
+              },
+
+              tasks: {
+                // Thanks to this read function, the readField("tasks")
+                // call above will always return an array, so we don't
+                // have to guard against the possibility that the tasks
+                // data is undefined above.
+                read(existing = []) {
+                  return existing;
+                },
+
+                merge(existing: Reference[], incoming: Reference[]) {
+                  const merged = existing ? existing.slice(0) : [];
+                  merged.push(...incoming);
+                  return merged;
+                },
+              },
+            },
+          },
+
+          Task: {
+            fields: {
+              ownTime(_, { readField, invalidate }) {
+                const desc = readField<string>("description");
+                // Store the invalidate function so that we can call it
+                // after updating the external ownTimes data.
+                invalidators[desc] = invalidate;
+                return ownTimes[desc] || 0;
+              },
+
+              totalTime(_, { readField, toReference }) {
+                function total(
+                  blockers: Readonly<Reference[]> = [],
+                  seen = new Set<string>(),
+                ) {
+                  let time = 0;
+                  blockers.forEach(blocker => {
+                    if (!seen.has(blocker.__ref)) {
+                      seen.add(blocker.__ref);
+                      time += readField<number>("ownTime", blocker);
+                      time += total(
+                        readField<Reference[]>("blockers", blocker),
+                        seen,
+                      );
+                    }
+                  });
+                  return time;
+                }
+                return total([
+                  toReference({
+                    __typename: "Task",
+                    id: readField("id"),
+                  }),
+                ]);
+              },
+
+              blockers: {
+                merge(existing: Reference[] = [], incoming: Reference[]) {
+                  const seenIDs = new Set(existing.map(ref => ref.__ref));
+                  const merged = existing.slice(0);
+                  incoming.forEach(ref => {
+                    if (!seenIDs.has(ref.__ref)) {
+                      seenIDs.add(ref.__ref);
+                      merged.push(ref);
+                    }
+                  });
+                  return merged;
+                },
+              },
+            },
+          },
+        },
+      });
+
+      cache.writeQuery({
+        query: gql`
+          query {
+            agenda {
+              id
+              tasks {
+                id
+                description
+                blockers {
+                  id
+                }
+              }
+            }
+          }
+        `,
+        data: {
+          agenda: {
+            __typename: "Agenda",
+            id: 1,
+            tasks: [{
+              __typename: "Task",
+              id: 1,
+              description: "parent task",
+              blockers: [{
+                __typename: "Task",
+                id: 2,
+              }, {
+                __typename: "Task",
+                id: 3,
+              }],
+            }, {
+              __typename: "Task",
+              id: 2,
+              description: "child task 1",
+              blockers: [{
+                __typename: "Task",
+                id: 4,
+              }],
+            }, {
+              __typename: "Task",
+              id: 3,
+              description: "child task 2",
+              blockers: [{
+                __typename: "Task",
+                id: 4,
+              }],
+            }, {
+              __typename: "Task",
+              id: 4,
+              description: "grandchild task",
+            }],
+          },
+        },
+      });
+
+      expect(cache.extract()).toEqual({
+        ROOT_QUERY: {
+          __typename: "Query",
+          agenda: { __ref: "Agenda:1" },
+        },
+        "Agenda:1": {
+          __typename: "Agenda",
+          id: 1,
+          tasks: [
+            { __ref: "Task:1" },
+            { __ref: "Task:2" },
+            { __ref: "Task:3" },
+            { __ref: "Task:4" },
+          ],
+        },
+        "Task:1": {
+          __typename: "Task",
+          blockers: [
+            { __ref: "Task:2" },
+            { __ref: "Task:3" },
+          ],
+          description: "parent task",
+          id: 1,
+        },
+        "Task:2": {
+          __typename: "Task",
+          blockers: [
+            { __ref: "Task:4" },
+          ],
+          description: "child task 1",
+          id: 2,
+        },
+        "Task:3": {
+          __typename: "Task",
+          blockers: [
+            { __ref: "Task:4" },
+          ],
+          description: "child task 2",
+          id: 3,
+        },
+        "Task:4": {
+          __typename: "Task",
+          description: "grandchild task",
+          id: 4,
+        },
+      });
+
+      const query = gql`
+        query {
+          agenda {
+            taskCount
+            tasks {
+              description
+              ownTime
+              totalTime
+            }
+          }
+        }
+      `;
+
+      function read() {
+        return cache.readQuery<{ agenda: any }>({ query });
+      }
+
+      const firstResult = read();
+
+      expect(firstResult).toEqual({
+        agenda: {
+          __typename: "Agenda",
+          taskCount: 4,
+          tasks: [{
+            __typename: "Task",
+            description: "parent task",
+            ownTime: 2,
+            totalTime: 2 + 3 + 4 + 5,
+          }, {
+            __typename: "Task",
+            description: "child task 1",
+            ownTime: 3,
+            totalTime: 3 + 5,
+          }, {
+            __typename: "Task",
+            description: "child task 2",
+            ownTime: 4,
+            totalTime: 4 + 5,
+          }, {
+            __typename: "Task",
+            description: "grandchild task",
+            ownTime: 5,
+            totalTime: 5,
+          }],
+        },
+      });
+
+      expect(read()).toBe(firstResult);
+
+      ownTimes["child task 2"] = 6;
+
+      // The query will not be reevaluated until we invalidate the read
+      // function whose result we changed.
+      expect(read()).toBe(firstResult);
+
+      invalidators["child task 2"]();
+
+      const secondResult = read();
+      expect(secondResult).not.toBe(firstResult);
+      expect(secondResult).toEqual({
+        agenda: {
+          __typename: "Agenda",
+          taskCount: 4,
+          tasks: [{
+            __typename: "Task",
+            description: "parent task",
+            ownTime: 2,
+            totalTime: 2 + 3 + 6 + 5,
+          }, {
+            __typename: "Task",
+            description: "child task 1",
+            ownTime: 3,
+            totalTime: 3 + 5,
+          }, {
+            __typename: "Task",
+            description: "child task 2",
+            ownTime: 6,
+            totalTime: 6 + 5,
+          }, {
+            __typename: "Task",
+            description: "grandchild task",
+            ownTime: 5,
+            totalTime: 5,
+          }],
+        },
+      });
+      expect(secondResult.agenda.tasks[0]).not.toBe(firstResult.agenda.tasks[0]);
+      expect(secondResult.agenda.tasks[1]).toBe(firstResult.agenda.tasks[1]);
+      expect(secondResult.agenda.tasks[2]).not.toBe(firstResult.agenda.tasks[2]);
+      expect(secondResult.agenda.tasks[3]).toBe(firstResult.agenda.tasks[3]);
+
+      ownTimes["grandchild task"] = 7;
+      invalidators["grandchild task"]();
+
+      const thirdResult = read();
+      expect(thirdResult).not.toBe(secondResult);
+      expect(thirdResult).toEqual({
+        agenda: {
+          __typename: "Agenda",
+          taskCount: 4,
+          tasks: [{
+            __typename: "Task",
+            description: "parent task",
+            ownTime: 2,
+            totalTime: 2 + 3 + 6 + 7,
+          }, {
+            __typename: "Task",
+            description: "child task 1",
+            ownTime: 3,
+            totalTime: 3 + 7,
+          }, {
+            __typename: "Task",
+            description: "child task 2",
+            ownTime: 6,
+            totalTime: 6 + 7,
+          }, {
+            __typename: "Task",
+            description: "grandchild task",
+            ownTime: 7,
+            totalTime: 7,
+          }],
+        },
+      });
+
+      cache.writeQuery({
+        query: gql`
+          query {
+            agenda {
+              id
+              tasks {
+                id
+                description
+              }
+            }
+          }
+        `,
+        data: {
+          agenda: {
+            __typename: "Agenda",
+            id: 1,
+            tasks: [{
+              __typename: "Task",
+              id: 5,
+              description: "independent task",
+            }],
+          },
+        },
+      });
+
+      expect(cache.extract()).toEqual({
+        ROOT_QUERY: {
+          __typename: "Query",
+          agenda: { __ref: "Agenda:1" },
+        },
+        "Agenda:1": {
+          __typename: "Agenda",
+          id: 1,
+          tasks: [
+            { __ref: "Task:1" },
+            { __ref: "Task:2" },
+            { __ref: "Task:3" },
+            { __ref: "Task:4" },
+            { __ref: "Task:5" },
+          ],
+        },
+        "Task:1": {
+          __typename: "Task",
+          blockers: [
+            { __ref: "Task:2" },
+            { __ref: "Task:3" },
+          ],
+          description: "parent task",
+          id: 1,
+        },
+        "Task:2": {
+          __typename: "Task",
+          blockers: [
+            { __ref: "Task:4" },
+          ],
+          description: "child task 1",
+          id: 2,
+        },
+        "Task:3": {
+          __typename: "Task",
+          blockers: [
+            { __ref: "Task:4" },
+          ],
+          description: "child task 2",
+          id: 3,
+        },
+        "Task:4": {
+          __typename: "Task",
+          description: "grandchild task",
+          id: 4,
+        },
+        "Task:5": {
+          __typename: "Task",
+          description: "independent task",
+          id: 5,
+        },
+      });
+
+      const fourthResult = read();
+      expect(fourthResult).not.toBe(thirdResult);
+      expect(fourthResult).toEqual({
+        agenda: {
+          __typename: "Agenda",
+          taskCount: 5,
+          tasks: [{
+            __typename: "Task",
+            description: "parent task",
+            ownTime: 2,
+            totalTime: 2 + 3 + 6 + 7,
+          }, {
+            __typename: "Task",
+            description: "child task 1",
+            ownTime: 3,
+            totalTime: 3 + 7,
+          }, {
+            __typename: "Task",
+            description: "child task 2",
+            ownTime: 6,
+            totalTime: 6 + 7,
+          }, {
+            __typename: "Task",
+            description: "grandchild task",
+            ownTime: 7,
+            totalTime: 7,
+          }, {
+            __typename: "Task",
+            description: "independent task",
+            ownTime: 11,
+            totalTime: 11,
+          }],
+        },
+      });
+
+      function checkFirstFourIdentical(result: ReturnType<typeof read>) {
+        for (let i = 0; i < 4; ++i) {
+          expect(result.agenda.tasks[i]).toBe(thirdResult.agenda.tasks[i]);
+        }
+      }
+      // The four original task results should not have been altered by
+      // the addition of a fifth independent task.
+      checkFirstFourIdentical(fourthResult);
+
+      ++ownTimes["independent task"];
+      invalidators["independent task"]();
+
+      const fifthResult = read();
+      expect(fifthResult).not.toBe(fourthResult);
+      expect(fifthResult).toEqual({
+        agenda: {
+          __typename: "Agenda",
+          taskCount: 5,
+          tasks: [
+            fourthResult.agenda.tasks[0],
+            fourthResult.agenda.tasks[1],
+            fourthResult.agenda.tasks[2],
+            fourthResult.agenda.tasks[3],
+            {
+              __typename: "Task",
+              description: "independent task",
+              ownTime: 12,
+              totalTime: 12,
+            },
+          ],
+        },
+      });
+      checkFirstFourIdentical(fifthResult);
+    });
+
+    it("can return void to indicate missing field", function () {
+      let secretReadAttempted = false;
+
+      const cache = new InMemoryCache({
+        typePolicies: {
+          Person: {
+            fields: {
+              secret() {
+                secretReadAttempted = true;
+                // Return nothing to signal field is missing.
+              },
+            },
+          },
+        },
+      });
+
+      const query = gql`
+        query {
+          me {
+            name
+          }
+        }
+      `;
+
+      cache.writeQuery({
+        query,
+        data: {
+          me: {
+            __typename: "Person",
+            name: "Ben Newman",
+          },
+        },
+      });
+
+      expect(secretReadAttempted).toBe(false);
+
+      expect(() => {
+        cache.readQuery({
+          query: gql`
+            query {
+              me {
+                secret
+              }
+            }
+          `
+        });
+      }).toThrow("Can't find field secret");
+
+      expect(secretReadAttempted).toBe(true);
+    });
+
+    it("can define custom merge functions", function () {
+      const cache = new InMemoryCache({
+        typePolicies: {
+          Person: {
+            // Disables normalization for the Person type, which means the
+            // todos field will be nested inside a non-normalized object
+            // (with __typename "Person") directly under the ROOT_QUERY.me
+            // field, which exercises what happens when mergeOverrides
+            // becomes nested (see writeToStore.ts).
+            keyFields: false,
+
+            fields: {
+              todos: {
+                keyArgs: [],
+
+                read(existing: any[], {
+                  args,
+                  toReference,
+                  isReference,
+                  policies,
+                }) {
+                  expect(!existing || Object.isFrozen(existing)).toBe(true);
+                  expect(typeof toReference).toBe("function");
+                  expect(policies).toBeInstanceOf(Policies);
+                  const slice = existing.slice(
+                    args.offset,
+                    args.offset + args.limit,
+                  );
+                  slice.forEach(ref => expect(isReference(ref)).toBe(true));
+                  return slice;
+                },
+
+                merge(existing: any[], incoming: any[], {
+                  args,
+                  toReference,
+                  isReference,
+                  policies,
+                }) {
+                  expect(!existing || Object.isFrozen(existing)).toBe(true);
+                  expect(typeof toReference).toBe("function");
+                  expect(policies).toBeInstanceOf(Policies);
+                  const copy = existing ? existing.slice(0) : [];
+                  const limit = args.offset + args.limit;
+                  for (let i = args.offset; i < limit; ++i) {
+                    copy[i] = incoming[i - args.offset];
+                  }
+                  copy.forEach(todo => expect(isReference(todo)).toBe(true));
+                  return copy;
+                }
+              },
+            },
+          },
+
+          Todo: {
+            keyFields: ["id"],
+          },
+        },
+      });
+
+      const query = gql`
+        query {
+          me {
+            todos(offset: $offset, limit: $limit) {
+              text
+            }
+          }
+        }
+      `;
+
+      cache.writeQuery({
+        query,
+        data: {
+          me: {
+            __typename: "Person",
+            id: "ignored",
+            todos: [
+              { __typename: "Todo", id: 1, text: "Write more merge tests" },
+              { __typename: "Todo", id: 2, text: "Write pagination tests" },
+            ],
+          },
+        },
+        variables: {
+          offset: 0,
+          limit: 2,
+        },
+      });
+
+      expect(cache.extract(true)).toEqual({
+        ROOT_QUERY: {
+          __typename: "Query",
+          me: {
+            __typename: "Person",
+            "todos:{}": [
+              { __ref: 'Todo:{"id":1}' },
+              { __ref: 'Todo:{"id":2}' },
+            ],
+          },
+        },
+        'Todo:{"id":1}': {
+          __typename: "Todo",
+          text: "Write more merge tests",
+        },
+        'Todo:{"id":2}': {
+          __typename: "Todo",
+          text: "Write pagination tests",
+        },
+      });
+
+      cache.writeQuery({
+        query,
+        data: {
+          me: {
+            __typename: "Person",
+            todos: [
+              { __typename: "Todo", id: 5, text: "Submit pull request" },
+              { __typename: "Todo", id: 6, text: "Merge pull request" },
+            ],
+          },
+        },
+        variables: {
+          offset: 4,
+          limit: 2,
+        },
+      });
+
+      expect(cache.extract(true)).toEqual({
+        ROOT_QUERY: {
+          __typename: "Query",
+          me: {
+            __typename: "Person",
+            "todos:{}": [
+              { __ref: 'Todo:{"id":1}' },
+              { __ref: 'Todo:{"id":2}' },
+              void 0,
+              void 0,
+              { __ref: 'Todo:{"id":5}' },
+              { __ref: 'Todo:{"id":6}' },
+            ],
+          },
+        },
+        'Todo:{"id":1}': {
+          __typename: "Todo",
+          text: "Write more merge tests",
+        },
+        'Todo:{"id":2}': {
+          __typename: "Todo",
+          text: "Write pagination tests",
+        },
+        'Todo:{"id":5}': {
+          __typename: "Todo",
+          text: "Submit pull request",
+        },
+        'Todo:{"id":6}': {
+          __typename: "Todo",
+          text: "Merge pull request",
+        },
+      });
+
+      cache.writeQuery({
+        query,
+        data: {
+          me: {
+            __typename: "Person",
+            todos: [
+              { __typename: "Todo", id: 3, text: "Iron out merge API" },
+              { __typename: "Todo", id: 4, text: "Take a nap" },
+            ],
+          },
+        },
+        variables: {
+          offset: 2,
+          limit: 2,
+        },
+      });
+
+      expect(cache.extract(true)).toEqual({
+        ROOT_QUERY: {
+          __typename: "Query",
+          me: {
+            __typename: "Person",
+            "todos:{}": [
+              { __ref: 'Todo:{"id":1}' },
+              { __ref: 'Todo:{"id":2}' },
+              { __ref: 'Todo:{"id":3}' },
+              { __ref: 'Todo:{"id":4}' },
+              { __ref: 'Todo:{"id":5}' },
+              { __ref: 'Todo:{"id":6}' },
+            ],
+          },
+        },
+        'Todo:{"id":1}': {
+          __typename: "Todo",
+          text: "Write more merge tests",
+        },
+        'Todo:{"id":2}': {
+          __typename: "Todo",
+          text: "Write pagination tests",
+        },
+        'Todo:{"id":3}': {
+          __typename: "Todo",
+          text: "Iron out merge API",
+        },
+        'Todo:{"id":4}': {
+          __typename: "Todo",
+          text: "Take a nap",
+        },
+        'Todo:{"id":5}': {
+          __typename: "Todo",
+          text: "Submit pull request",
+        },
+        'Todo:{"id":6}': {
+          __typename: "Todo",
+          text: "Merge pull request",
+        },
+      });
+
+      expect(cache.gc()).toEqual([]);
+
+      // The moment of truth!
+      expect(
+        cache.readQuery({
+          query,
+          variables: {
+            offset: 1,
+            limit: 4,
+          },
+        })
+      ).toEqual({
+        me: {
+          __typename: "Person",
+          todos: [
+            { __typename: "Todo", text: "Write pagination tests" },
+            { __typename: "Todo", text: "Iron out merge API" },
+            { __typename: "Todo", text: "Take a nap" },
+            { __typename: "Todo", text: "Submit pull request" },
+          ],
+        },
+      });
+    });
+
+    it("runs nested merge functions as well as ancestors", function () {
+      let eventMergeCount = 0;
+      let attendeeMergeCount = 0;
+
+      const cache = new InMemoryCache({
+        typePolicies: {
+          Event: {
+            fields: {
+              attendees: {
+                merge(existing: any[], incoming: any[]) {
+                  ++eventMergeCount;
+                  expect(Array.isArray(incoming)).toBe(true);
+                  return existing ? existing.concat(incoming) : incoming;
+                },
+              },
+            },
+          },
+
+          Attendee: {
+            fields: {
+              events: {
+                merge(existing: any[], incoming: any[]) {
+                  ++attendeeMergeCount;
+                  expect(Array.isArray(incoming)).toBe(true);
+                  return existing ? existing.concat(incoming) : incoming;
+                },
+              },
+            },
+          },
+        },
+      });
+
+      cache.writeQuery({
+        query: gql`
+          query {
+            eventsToday {
+              name
+              attendees {
+                name
+                events {
+                  time
+                }
+              }
+            }
+          }
+        `,
+        data: {
+          eventsToday: [{
+            __typename: "Event",
+            id: 123,
+            name: "One-person party",
+            time: "noonish",
+            attendees: [{
+              __typename: "Attendee",
+              id: 234,
+              name: "Ben Newman",
+              events: [
+                { __typename: "Event", id: 123 },
+              ],
+            }],
+          }],
+        },
+      });
+
+      expect(eventMergeCount).toBe(1);
+      expect(attendeeMergeCount).toBe(1);
+
+      expect(cache.extract()).toEqual({
+        ROOT_QUERY: {
+          __typename: "Query",
+          eventsToday: [
+            { __ref: "Event:123" },
+          ],
+        },
+        "Event:123": {
+          __typename: "Event",
+          name: "One-person party",
+          attendees: [
+            { __ref: "Attendee:234" },
+          ],
+        },
+        "Attendee:234": {
+          __typename: "Attendee",
+          name: "Ben Newman",
+          events: [
+            { __ref: "Event:123" },
+          ],
+        },
+      });
+
+      cache.writeQuery({
+        query: gql`
+          query {
+            people {
+              name
+              events {
+                time
+                attendees {
+                  name
+                }
+              }
+            }
+          }
+        `,
+        data: {
+          people: [{
+            __typename: "Attendee",
+            id: 234,
+            name: "Ben Newman",
+            events: [{
+              __typename: "Event",
+              id: 345,
+              name: "Rooftop dog party",
+              attendees: [{
+                __typename: "Attendee",
+                id: 456,
+                name: "Inspector Beckett",
+              }, {
+                __typename: "Attendee",
+                id: 234,
+              }],
+            }],
+          }],
+        },
+      });
+
+      expect(eventMergeCount).toBe(2);
+      expect(attendeeMergeCount).toBe(2);
+
+      expect(cache.extract()).toEqual({
+        ROOT_QUERY: {
+          __typename: "Query",
+          eventsToday: [
+            { __ref: "Event:123" },
+          ],
+          people: [
+            { __ref: "Attendee:234" },
+          ],
+        },
+        "Event:123": {
+          __typename: "Event",
+          name: "One-person party",
+          attendees: [
+            { __ref: "Attendee:234" },
+          ],
+        },
+        "Event:345": {
+          __typename: "Event",
+          attendees: [
+            { __ref: "Attendee:456" },
+            { __ref: "Attendee:234" },
+          ],
+        },
+        "Attendee:234": {
+          __typename: "Attendee",
+          name: "Ben Newman",
+          events: [
+            { __ref: "Event:123" },
+            { __ref: "Event:345" },
+          ],
+        },
+        "Attendee:456": {
+          __typename: "Attendee",
+          name: "Inspector Beckett",
+        },
+      });
+
+      expect(cache.gc()).toEqual([]);
+    });
+  });
+
+  it("runs read and merge functions for unidentified data", function () {
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Book: {
+          keyFields: ["isbn"],
+        },
+
+        Author: {
+          // Passing false for keyFields disables normalization of Author
+          // objects, which should not interfere with the operation of
+          // their read and/or merge functions. However, disabling
+          // normalization means the merge function for the name field
+          // will be called only once, because we never merge fields when
+          // the IDs of the enclosing objects are unknown or unequal.
+          keyFields: false,
+
+          fields: {
+            name: {
+              read(name: string) {
+                return reverse(name).toUpperCase();
+              },
+              merge(oldName, newName: string) {
+                expect(oldName).toBe(void 0);
+                expect(typeof newName).toBe("string");
+                return reverse(newName);
+              },
+            },
+          },
+        },
+      },
+    });
+
+    const query = gql`
+      query {
+        currentlyReading {
+          title
+          authors {
+            name
+          }
+        }
+      }
+    `;
+
+    cache.writeQuery({
+      query,
+      data: {
+        currentlyReading: [{
+          __typename: "Book",
+          isbn: "0525558616",
+          title: "Human Compatible: Artificial Intelligence and the Problem of Control",
+          authors: [{
+            __typename: "Author",
+            name: "Stuart Russell",
+          }],
+        }, {
+          __typename: "Book",
+          isbn: "1541698967",
+          title: "The Book of Why: The New Science of Cause and Effect",
+          authors: [{
+            __typename: "Author",
+            name: "Judea Pearl",
+          }, {
+            __typename: "Author",
+            name: "Dana Mackenzie",
+          }],
+        }],
+      },
+    });
+
+    expect(cache.extract()).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        currentlyReading: [
+          { __ref: 'Book:{"isbn":"0525558616"}' },
+          { __ref: 'Book:{"isbn":"1541698967"}' },
+        ],
+      },
+      'Book:{"isbn":"0525558616"}': {
+        __typename: "Book",
+        authors: [{
+          __typename: "Author",
+          // Note the successful reversal of the Author names.
+          name: "llessuR trautS",
+        }],
+        title: "Human Compatible: Artificial Intelligence and the Problem of Control",
+      },
+      'Book:{"isbn":"1541698967"}': {
+        __typename: "Book",
+        authors: [{
+          __typename: "Author",
+          name: "lraeP aeduJ",
+        }, {
+          __typename: "Author",
+          name: "eiznekcaM anaD",
+        }],
+        title: "The Book of Why: The New Science of Cause and Effect",
+      },
+    });
+
+    expect(cache.readQuery({ query })).toEqual({
+      currentlyReading: [{
+        __typename: "Book",
+        title: "Human Compatible: Artificial Intelligence and the Problem of Control",
+        authors: [{
+          __typename: "Author",
+          name: "STUART RUSSELL",
+        }],
+      }, {
+        __typename: "Book",
+        title: "The Book of Why: The New Science of Cause and Effect",
+        authors: [{
+          __typename: "Author",
+          // Note the successful re-reversal and uppercasing, thanks to
+          // the custom read function.
+          name: "JUDEA PEARL",
+        }, {
+          __typename: "Author",
+          name: "DANA MACKENZIE",
+        }],
+      }],
+    });
+  });
+
+  it("can read from foreign references using read helper", function () {
+    const cache = new InMemoryCache({
+      typePolicies: {
+        Author: {
+          keyFields: ["name"],
+
+          fields: {
+            books: {
+              merge(existing: Reference[] = [], incoming: Reference[]) {
+                return [...existing, ...incoming];
+              },
+            },
+
+            // A dynamically computed field that returns the Book
+            // Reference with the earliest year, which requires reading
+            // fields from foreign references.
+            firstBook(_, { isReference, readField }) {
+              let firstBook: Reference;
+              let firstYear: number;
+              const bookRefs = readField<Reference[]>("books") || [];
+              bookRefs.forEach(bookRef => {
+                expect(isReference(bookRef)).toBe(true);
+                const year = readField<number>("year", bookRef);
+                if (firstYear === void 0 || year < firstYear) {
+                  firstBook = bookRef;
+                  firstYear = year;
+                }
+              });
+              // Return a Book Reference, which can have a nested
+              // selection set applied to it.
+              return firstBook;
+            },
+          },
+        },
+
+        Book: {
+          keyFields: ["isbn"],
+        },
+      },
+    });
+
+    function addBook(bookData) {
+      cache.writeQuery({
+        query: gql`
+          query {
+            author {
+              name
+              books {
+                isbn
+                title
+                year
+              }
+            }
+          }
+        `,
+        data: {
+          author: {
+            __typename: "Author",
+            name: "Virginia Woolf",
+            books: [{
+              __typename: "Book",
+              ...bookData,
+            }],
+          },
+        },
+      });
+    }
+
+    addBook({
+      __typename: "Book",
+      isbn: "1853262390",
+      title: "Orlando",
+      year: 1928,
+    });
+
+    addBook({
+      __typename: "Book",
+      isbn: "9353420717",
+      title: "A Room of One's Own",
+      year: 1929,
+    });
+
+    addBook({
+      __typename: "Book",
+      isbn: "0156907399",
+      title: "To the Lighthouse",
+      year: 1927,
+    });
+
+    expect(cache.extract()).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        author: {
+          __ref: 'Author:{"name":"Virginia Woolf"}',
+        },
+      },
+      'Author:{"name":"Virginia Woolf"}': {
+        __typename: "Author",
+        name: "Virginia Woolf",
+        books: [
+          { __ref: 'Book:{"isbn":"1853262390"}' },
+          { __ref: 'Book:{"isbn":"9353420717"}' },
+          { __ref: 'Book:{"isbn":"0156907399"}' },
+        ],
+      },
+      'Book:{"isbn":"1853262390"}': {
+        __typename: "Book",
+        isbn: "1853262390",
+        title: "Orlando",
+        year: 1928,
+      },
+      'Book:{"isbn":"9353420717"}': {
+        __typename: "Book",
+        isbn: "9353420717",
+        title: "A Room of One's Own",
+        year: 1929,
+      },
+      'Book:{"isbn":"0156907399"}': {
+        __typename: "Book",
+        isbn: "0156907399",
+        title: "To the Lighthouse",
+        year: 1927,
+      },
+    });
+
+    const firstBookQuery = gql`
+      query {
+        author {
+          name
+          firstBook {
+            title
+            year
+          }
+        }
+      }
+    `;
+
+    function readFirstBookResult() {
+      return cache.readQuery<{ author: any }>({
+        query: firstBookQuery,
+      });
+    }
+
+    const firstBookResult = readFirstBookResult();
+    expect(firstBookResult).toEqual({
+      author: {
+        __typename: "Author",
+        name: "Virginia Woolf",
+        firstBook: {
+          __typename: "Book",
+          title: "To the Lighthouse",
+          year: 1927,
+        },
+      },
+    });
+
+    expect(readFirstBookResult()).toBe(firstBookResult);
+
+    // Add an even earlier book.
+    addBook({
+      isbn: "1420959719",
+      title: "The Voyage Out",
+      year: 1915,
+    });
+
+    const secondFirstBookResult = readFirstBookResult();
+    expect(secondFirstBookResult).not.toBe(firstBookResult);
+    expect(secondFirstBookResult).toEqual({
+      author: {
+        __typename: "Author",
+        name: "Virginia Woolf",
+        firstBook: {
+          __typename: "Book",
+          title: "The Voyage Out",
+          year: 1915,
+        },
+      },
+    });
+
+    // Write a new, unrelated field.
+    cache.writeQuery({
+      query: gql`query { author { afraidCount } }`,
+      data: {
+        author: {
+          __typename: "Author",
+          name: "Virginia Woolf",
+          afraidCount: 2,
+        },
+      },
+    });
+
+    // Make sure afraidCount was written.
+    expect(cache.readFragment({
+      id: cache.identify({
+        __typename: "Author",
+        name: "Virginia Woolf",
+      }),
+      fragment: gql`
+        fragment AfraidFragment on Author {
+          name
+          afraidCount
+        }
+      `,
+    })).toEqual({
+      __typename: "Author",
+      name: "Virginia Woolf",
+      afraidCount: 2,
+    });
+
+    // Since we wrote only the afraidCount field, the firstBook result
+    // should be completely unchanged.
+    expect(readFirstBookResult()).toBe(secondFirstBookResult);
+
+    // Add another book, not published first.
+    addBook({
+      isbn: "9780156949606",
+      title: "The Waves",
+      year: 1931,
+    });
+
+    const thirdFirstBookResult = readFirstBookResult();
+
+    // A change in VW's books field triggers rereading of result objects
+    // that previously involved her books field.
+    expect(thirdFirstBookResult).not.toBe(secondFirstBookResult);
+
+    // However, since the new Book was not the earliest published, the
+    // second and third results are structurally the same.
+    expect(thirdFirstBookResult).toEqual(secondFirstBookResult);
+
+    // In fact, the original author.firstBook object has been reused!
+    expect(thirdFirstBookResult.author.firstBook).toBe(
+      secondFirstBookResult.author.firstBook,
+    );
+  });
+});
diff --git a/packages/apollo-cache-inmemory/src/__tests__/readFromStore.ts b/src/cache/inmemory/__tests__/readFromStore.ts
similarity index 80%
rename from packages/apollo-cache-inmemory/src/__tests__/readFromStore.ts
rename to src/cache/inmemory/__tests__/readFromStore.ts
--- a/packages/apollo-cache-inmemory/src/__tests__/readFromStore.ts
+++ b/src/cache/inmemory/__tests__/readFromStore.ts
@@ -1,35 +1,35 @@
 import { assign, omit } from 'lodash';
-import { IdValue, JsonValue } from 'apollo-utilities';
 import gql from 'graphql-tag';
-import { stripSymbols } from 'apollo-utilities';
 
-import { StoreObject, HeuristicFragmentMatcher } from '../';
+import { stripSymbols } from '../../../utilities/testing/stripSymbols';
+import { StoreObject } from '../types';
 import { StoreReader } from '../readFromStore';
-import { defaultNormalizedCacheFactory } from '../objectCache';
-
-const fragmentMatcherFunction = new HeuristicFragmentMatcher().match;
+import { makeReference } from '../../../utilities/graphql/storeUtils';
+import { defaultNormalizedCacheFactory } from '../entityStore';
 import { withError } from './diffAgainstStore';
+import { Policies } from '../policies';
 
 describe('reading from the store', () => {
-  const reader = new StoreReader();
+  const reader = new StoreReader({
+    policies: new Policies(),
+  });
 
   it('runs a nested query with proper fragment fields in arrays', () => {
     withError(() => {
       const store = defaultNormalizedCacheFactory({
         ROOT_QUERY: {
           __typename: 'Query',
-          nestedObj: { type: 'id', id: 'abcde', generated: false },
+          nestedObj: makeReference('abcde'),
         } as StoreObject,
         abcde: {
           id: 'abcde',
           innerArray: [
-            { type: 'id', generated: true, id: 'abcde.innerArray.0' } as any,
+            {
+              id: 'abcdef',
+              someField: 3,
+            },
           ],
         } as StoreObject,
-        'abcde.innerArray.0': {
-          id: 'abcdef',
-          someField: 3,
-        } as StoreObject,
       });
 
       const queryResult = reader.readQueryFromStore({
@@ -62,7 +62,6 @@ describe('reading from the store', () => {
             }
           }
         `,
-        fragmentMatcherFunction,
       });
 
       expect(stripSymbols(queryResult)).toEqual({
@@ -70,7 +69,7 @@ describe('reading from the store', () => {
           innerArray: [{ id: 'abcdef', someField: 3 }],
         },
       });
-    }, /queries contain union or interface types/);
+    });
   });
 
   it('rejects malformed queries', () => {
@@ -258,11 +257,7 @@ describe('reading from the store', () => {
 
     const store = defaultNormalizedCacheFactory({
       ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedObj')), {
-        nestedObj: {
-          type: 'id',
-          id: 'abcde',
-          generated: false,
-        },
+        nestedObj: makeReference('abcde'),
       } as StoreObject),
       abcde: result.nestedObj,
     });
@@ -319,19 +314,11 @@ describe('reading from the store', () => {
         assign({}, omit(result, 'nestedObj', 'deepNestedObj')),
         {
           __typename: 'Query',
-          nestedObj: {
-            type: 'id',
-            id: 'abcde',
-            generated: false,
-          },
+          nestedObj: makeReference('abcde'),
         } as StoreObject,
       ),
       abcde: assign({}, result.nestedObj, {
-        deepNestedObj: {
-          type: 'id',
-          id: 'abcdef',
-          generated: false,
-        },
+        deepNestedObj: makeReference('abcdef'),
       }) as StoreObject,
       abcdef: result.deepNestedObj as StoreObject,
     });
@@ -410,14 +397,7 @@ describe('reading from the store', () => {
     };
 
     const store = defaultNormalizedCacheFactory({
-      ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), {
-        nestedArray: [
-          { type: 'id', generated: true, id: 'abcd.nestedArray.0' } as IdValue,
-          { type: 'id', generated: true, id: 'abcd.nestedArray.1' } as IdValue,
-        ],
-      }) as StoreObject,
-      'abcd.nestedArray.0': result.nestedArray[0],
-      'abcd.nestedArray.1': result.nestedArray[1],
+      ROOT_QUERY: result,
     });
 
     const queryResult = reader.readQueryFromStore({
@@ -468,13 +448,7 @@ describe('reading from the store', () => {
     };
 
     const store = defaultNormalizedCacheFactory({
-      ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), {
-        nestedArray: [
-          null,
-          { type: 'id', generated: true, id: 'abcd.nestedArray.1' } as IdValue,
-        ],
-      }) as StoreObject,
-      'abcd.nestedArray.1': result.nestedArray[1],
+      ROOT_QUERY: result,
     });
 
     const queryResult = reader.readQueryFromStore({
@@ -524,7 +498,7 @@ describe('reading from the store', () => {
 
     const store = defaultNormalizedCacheFactory({
       ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), {
-        nestedArray: [null, { type: 'id', generated: false, id: 'abcde' }],
+        nestedArray: [null, makeReference('abcde')],
       }) as StoreObject,
       abcde: result.nestedArray[1],
     });
@@ -629,12 +603,7 @@ describe('reading from the store', () => {
     };
 
     const store = defaultNormalizedCacheFactory({
-      ROOT_QUERY: assign({}, assign({}, omit(result, 'simpleArray')), {
-        simpleArray: {
-          type: 'json',
-          json: result.simpleArray,
-        } as JsonValue,
-      }) as StoreObject,
+      ROOT_QUERY: result,
     });
 
     const queryResult = reader.readQueryFromStore({
@@ -666,12 +635,7 @@ describe('reading from the store', () => {
     };
 
     const store = defaultNormalizedCacheFactory({
-      ROOT_QUERY: assign({}, assign({}, omit(result, 'simpleArray')), {
-        simpleArray: {
-          type: 'json',
-          json: result.simpleArray,
-        } as JsonValue,
-      }) as StoreObject,
+      ROOT_QUERY: result,
     });
 
     const queryResult = reader.readQueryFromStore({
@@ -720,19 +684,11 @@ describe('reading from the store', () => {
         assign({}, omit(data, 'nestedObj', 'deepNestedObj')),
         {
           __typename: 'Query',
-          nestedObj: {
-            type: 'id',
-            id: 'abcde',
-            generated: false,
-          } as IdValue,
+          nestedObj: makeReference('abcde'),
         },
       ) as StoreObject,
       abcde: assign({}, data.nestedObj, {
-        deepNestedObj: {
-          type: 'id',
-          id: 'abcdef',
-          generated: false,
-        },
+        deepNestedObj: makeReference('abcdef'),
       }) as StoreObject,
       abcdef: data.deepNestedObj as StoreObject,
     });
@@ -784,20 +740,15 @@ describe('reading from the store', () => {
     });
   });
 
-  it('properly handles the connection directive', () => {
+  it('properly handles the @connection directive', () => {
     const store = defaultNormalizedCacheFactory({
       ROOT_QUERY: {
-        abc: [
+        'books:abc': [
           {
-            generated: true,
-            id: 'ROOT_QUERY.abc.0',
-            type: 'id',
+            name: 'efgh',
           },
         ],
       },
-      'ROOT_QUERY.abc.0': {
-        name: 'efgh',
-      },
     });
 
     const queryResult = reader.readQueryFromStore({
@@ -819,4 +770,150 @@ describe('reading from the store', () => {
       ],
     });
   });
+
+  it('can use keyArgs function instead of @connection directive', () => {
+    const reader = new StoreReader({
+      policies: new Policies({
+        typePolicies: {
+          Query: {
+            fields: {
+              books: {
+                // Even though we're returning an arbitrary string here,
+                // the InMemoryCache will ensure the actual key begins
+                // with "books".
+                keyArgs: () => "abc",
+              },
+            },
+          },
+        },
+      }),
+    });
+
+    const store = defaultNormalizedCacheFactory({
+      ROOT_QUERY: {
+        "books:abc": [
+          {
+            name: 'efgh',
+          },
+        ],
+      },
+    });
+
+    const queryResult = reader.readQueryFromStore({
+      store,
+      query: gql`
+        {
+          books(skip: 0, limit: 2) {
+            name
+          }
+        }
+      `,
+    });
+
+    expect(stripSymbols(queryResult)).toEqual({
+      books: [
+        {
+          name: 'efgh',
+        },
+      ],
+    });
+  });
+
+  it('refuses to return raw Reference objects', () => {
+    const store = defaultNormalizedCacheFactory({
+      ROOT_QUERY: {
+        author: {
+          __typename: 'Author',
+          name: 'Toni Morrison',
+          books: [
+            {
+              title: 'The Bluest Eye',
+              publisher: makeReference('Publisher1'),
+            },
+            {
+              title: 'Song of Solomon',
+              publisher: makeReference('Publisher2'),
+            },
+            {
+              title: 'Beloved',
+              publisher: makeReference('Publisher2'),
+            },
+          ],
+        },
+      },
+      Publisher1: {
+        __typename: 'Publisher',
+        id: 1,
+        name: 'Holt, Rinehart and Winston',
+      },
+      Publisher2: {
+        __typename: 'Publisher',
+        id: 2,
+        name: 'Alfred A. Knopf, Inc.',
+      },
+    });
+
+    expect(() => {
+      reader.readQueryFromStore({
+        store,
+        query: gql`
+          {
+            author {
+              name
+              books
+            }
+          }
+        `,
+      });
+    }).toThrow(
+      /Missing selection set for object of type Publisher returned for query field books/,
+    );
+
+    expect(
+      reader.readQueryFromStore({
+        store,
+        query: gql`
+          {
+            author {
+              name
+              books {
+                title
+                publisher {
+                  name
+                }
+              }
+            }
+          }
+        `,
+      }),
+    ).toEqual({
+      author: {
+        __typename: 'Author',
+        name: 'Toni Morrison',
+        books: [
+          {
+            title: 'The Bluest Eye',
+            publisher: {
+              __typename: 'Publisher',
+              name: 'Holt, Rinehart and Winston',
+            },
+          },
+          {
+            title: 'Song of Solomon',
+            publisher: {
+              __typename: 'Publisher',
+              name: 'Alfred A. Knopf, Inc.',
+            },
+          },
+          {
+            title: 'Beloved',
+            publisher: {
+              __typename: 'Publisher',
+              name: 'Alfred A. Knopf, Inc.',
+            },
+          },
+        ],
+      },
+    });
+  });
 });
diff --git a/packages/apollo-cache-inmemory/src/__tests__/recordingCache.ts b/src/cache/inmemory/__tests__/recordingCache.ts
similarity index 53%
rename from packages/apollo-cache-inmemory/src/__tests__/recordingCache.ts
rename to src/cache/inmemory/__tests__/recordingCache.ts
--- a/packages/apollo-cache-inmemory/src/__tests__/recordingCache.ts
+++ b/src/cache/inmemory/__tests__/recordingCache.ts
@@ -1,10 +1,9 @@
-import { OptimisticCacheLayer } from '../inMemoryCache';
-import { ObjectCache } from '../objectCache';
 import { NormalizedCacheObject } from '../types';
+import { EntityStore } from '../entityStore';
 
-describe('OptimisticCacheLayer', () => {
-  function makeLayer(root: ObjectCache) {
-    return new OptimisticCacheLayer('whatever', root, () => {});
+describe('Optimistic EntityStore layering', () => {
+  function makeLayer(root: EntityStore) {
+    return root.addLayer('whatever', () => {});
   }
 
   describe('returns correct values during recording', () => {
@@ -17,31 +16,31 @@ describe('OptimisticCacheLayer', () => {
       Human: { __typename: 'Human', name: 'John' },
     };
 
-    const underlyingCache = new ObjectCache(data);
+    const underlyingStore = new EntityStore.Root({ seed: data });
 
-    let cache = makeLayer(underlyingCache);
+    let store = makeLayer(underlyingStore);
     beforeEach(() => {
-      cache = makeLayer(underlyingCache);
+      store = makeLayer(underlyingStore);
     });
 
     it('should passthrough values if not defined in recording', () => {
-      expect(cache.get('Human')).toBe(data.Human);
-      expect(cache.get('Animal')).toBe(data.Animal);
+      expect(store.get('Human')).toBe(data.Human);
+      expect(store.get('Animal')).toBe(data.Animal);
     });
 
     it('should return values defined during recording', () => {
-      cache.set('Human', dataToRecord.Human);
-      expect(cache.get('Human')).toBe(dataToRecord.Human);
-      expect(underlyingCache.get('Human')).toBe(data.Human);
+      store.merge('Human', dataToRecord.Human);
+      expect(store.get('Human')).toEqual(dataToRecord.Human);
+      expect(underlyingStore.get('Human')).toBe(data.Human);
     });
 
     it('should return undefined for values deleted during recording', () => {
-      expect(cache.get('Animal')).toBe(data.Animal);
+      expect(store.get('Animal')).toBe(data.Animal);
       // delete should be registered in the recording:
-      cache.delete('Animal');
-      expect(cache.get('Animal')).toBeUndefined();
-      expect(cache.toObject()).toHaveProperty('Animal');
-      expect(underlyingCache.get('Animal')).toBe(data.Animal);
+      store.delete('Animal');
+      expect(store.get('Animal')).toBeUndefined();
+      expect(store.toObject()).toHaveProperty('Animal');
+      expect(underlyingStore.get('Animal')).toBe(data.Animal);
     });
   });
 
@@ -55,15 +54,15 @@ describe('OptimisticCacheLayer', () => {
       Human: { __typename: 'Human', name: 'John' },
     };
 
-    const underlyingCache = new ObjectCache(data);
-    let cache = makeLayer(underlyingCache);
+    const underlyingStore = new EntityStore.Root({ seed: data });
+    let store = makeLayer(underlyingStore);
     let recording: NormalizedCacheObject;
 
     beforeEach(() => {
-      cache = makeLayer(underlyingCache);
-      cache.set('Human', dataToRecord.Human);
-      cache.delete('Animal');
-      recording = cache.toObject();
+      store = makeLayer(underlyingStore);
+      store.merge('Human', dataToRecord.Human);
+      store.delete('Animal');
+      recording = store.toObject();
     });
 
     it('should contain the property indicating deletion', () => {
@@ -78,7 +77,7 @@ describe('OptimisticCacheLayer', () => {
     });
 
     it('should keep the original data unaffected', () => {
-      expect(underlyingCache.toObject()).toEqual(data);
+      expect(underlyingStore.toObject()).toEqual(data);
     });
   });
 });
diff --git a/packages/apollo-cache-inmemory/src/__tests__/roundtrip.ts b/src/cache/inmemory/__tests__/roundtrip.ts
similarity index 87%
rename from packages/apollo-cache-inmemory/src/__tests__/roundtrip.ts
rename to src/cache/inmemory/__tests__/roundtrip.ts
--- a/packages/apollo-cache-inmemory/src/__tests__/roundtrip.ts
+++ b/src/cache/inmemory/__tests__/roundtrip.ts
@@ -3,12 +3,10 @@ import gql from 'graphql-tag';
 
 import { withError } from './diffAgainstStore';
 import { withWarning } from './writeToStore';
-
-import { DepTrackingCache } from '../depTrackingCache';
-
-import { HeuristicFragmentMatcher, StoreReader, StoreWriter } from '../';
-
-const fragmentMatcherFunction = new HeuristicFragmentMatcher().match;
+import { EntityStore } from '../entityStore';
+import { StoreReader } from '../readFromStore';
+import { StoreWriter } from '../writeToStore';
+import { Policies } from '../policies';
 
 function assertDeeplyFrozen(value: any, stack: any[] = []) {
   if (value !== null && typeof value === 'object' && stack.indexOf(value) < 0) {
@@ -23,9 +21,14 @@ function assertDeeplyFrozen(value: any, stack: any[] = []) {
 }
 
 function storeRoundtrip(query: DocumentNode, result: any, variables = {}) {
-  const reader = new StoreReader();
-  const immutableReader = new StoreReader({ freezeResults: true });
-  const writer = new StoreWriter();
+  const policies = new Policies({
+    possibleTypes: {
+      Character: ["Jedi", "Droid"],
+    },
+  });
+
+  const reader = new StoreReader({ policies });
+  const writer = new StoreWriter({ policies });
 
   const store = writer.writeQueryToStore({
     result,
@@ -37,7 +40,6 @@ function storeRoundtrip(query: DocumentNode, result: any, variables = {}) {
     store,
     query,
     variables,
-    fragmentMatcherFunction,
   };
 
   const reconstructedResult = reader.readQueryFromStore<any>(readOptions);
@@ -45,12 +47,12 @@ function storeRoundtrip(query: DocumentNode, result: any, variables = {}) {
 
   // Make sure the result is identical if we haven't written anything new
   // to the store. https://github.com/apollographql/apollo-client/pull/3394
-  expect(store).toBeInstanceOf(DepTrackingCache);
+  expect(store).toBeInstanceOf(EntityStore);
   expect(reader.readQueryFromStore(readOptions)).toBe(reconstructedResult);
 
-  const immutableResult = immutableReader.readQueryFromStore(readOptions);
+  const immutableResult = reader.readQueryFromStore(readOptions);
   expect(immutableResult).toEqual(reconstructedResult);
-  expect(immutableReader.readQueryFromStore(readOptions)).toBe(immutableResult);
+  expect(reader.readQueryFromStore(readOptions)).toBe(immutableResult);
   if (process.env.NODE_ENV !== 'production') {
     try {
       // Note: this illegal assignment will only throw in strict mode, but that's
@@ -237,8 +239,8 @@ describe('roundtrip', () => {
       },
     );
 
-    // Just because we read from the store using { freezeResults: true }, the
-    // original data should not be frozen.
+    // Reading immutable results from the store does not mean the original
+    // data should get frozen.
     expect(Object.isExtensible(updateClub)).toBe(true);
     expect(Object.isFrozen(updateClub)).toBe(false);
   });
@@ -306,42 +308,10 @@ describe('roundtrip', () => {
       );
     });
 
-    it('should resolve on union types with inline fragments without typenames with warning', () => {
-      return withWarning(() => {
-        storeRoundtrip(
-          gql`
-            query {
-              all_people {
-                name
-                ... on Jedi {
-                  side
-                }
-                ... on Droid {
-                  model
-                }
-              }
-            }
-          `,
-          {
-            all_people: [
-              {
-                name: 'Luke Skywalker',
-                side: 'bright',
-              },
-              {
-                name: 'R2D2',
-                model: 'astromech',
-              },
-            ],
-          },
-        );
-      }, /using fragments/);
-    });
-
     // XXX this test is weird because it assumes the server returned an incorrect result
     // However, the user may have written this result with client.writeQuery.
     it('should throw an error on two of the same inline fragment types', () => {
-      return expect(() => {
+      return withWarning(() => expect(() => {
         storeRoundtrip(
           gql`
             query {
@@ -367,7 +337,7 @@ describe('roundtrip', () => {
             ],
           },
         );
-      }).toThrowError(/Can\'t find field rank on object/);
+      }).toThrowError(/Can\'t find field rank on object/));
     });
 
     it('should resolve fields it can on interface with non matching inline fragments', () => {
@@ -398,7 +368,7 @@ describe('roundtrip', () => {
             ],
           },
         );
-      }, /IntrospectionFragmentMatcher/);
+      });
     });
 
     it('should resolve on union types with spread fragments', () => {
@@ -437,7 +407,7 @@ describe('roundtrip', () => {
             ],
           },
         );
-      }, /IntrospectionFragmentMatcher/);
+      });
     });
 
     it('should work with a fragment on the actual interface or union', () => {
@@ -472,15 +442,16 @@ describe('roundtrip', () => {
                 __typename: 'Droid',
                 name: 'R2D2',
                 model: 'astromech',
+                side: 'bright',
               },
             ],
           },
         );
-      }, /IntrospectionFragmentMatcher/);
+      });
     });
 
     it('should throw on error on two of the same spread fragment types', () => {
-      expect(() =>
+      withWarning(() => expect(() => {
         storeRoundtrip(
           gql`
             fragment jediSide on Jedi {
@@ -509,8 +480,8 @@ describe('roundtrip', () => {
               },
             ],
           },
-        ),
-      ).toThrowError(/Can\'t find field rank on object/);
+        );
+      }).toThrowError(/Can\'t find field rank on object/));
     });
 
     it('should resolve on @include and @skip with inline fragments', () => {
diff --git a/packages/apollo-cache-inmemory/src/__tests__/writeToStore.ts b/src/cache/inmemory/__tests__/writeToStore.ts
similarity index 74%
rename from packages/apollo-cache-inmemory/src/__tests__/writeToStore.ts
rename to src/cache/inmemory/__tests__/writeToStore.ts
--- a/packages/apollo-cache-inmemory/src/__tests__/writeToStore.ts
+++ b/src/cache/inmemory/__tests__/writeToStore.ts
@@ -1,38 +1,35 @@
-import { cloneDeep, assign, omit } from 'lodash';
-
+import { assign, omit } from 'lodash';
 import {
   SelectionNode,
   FieldNode,
   DefinitionNode,
   OperationDefinitionNode,
   ASTNode,
+  DocumentNode,
 } from 'graphql';
-
 import gql from 'graphql-tag';
+
 import {
   storeKeyNameFromField,
-  IdValue,
-  addTypenameToDocument,
-} from 'apollo-utilities';
-
+  makeReference,
+} from '../../../utilities/graphql/storeUtils';
+import { addTypenameToDocument } from '../../../utilities/graphql/transform';
+import { cloneDeep } from '../../../utilities/common/cloneDeep';
 import { StoreWriter } from '../writeToStore';
+import { defaultNormalizedCacheFactory } from '../entityStore';
+import { InMemoryCache } from '../inMemoryCache';
+import { Policies } from '../policies';
 
-import { defaultNormalizedCacheFactory } from '../objectCache';
-
-import {
-  HeuristicFragmentMatcher,
-  IntrospectionFragmentMatcher,
-  StoreObject,
-} from '../';
-
-export function withWarning(func: Function, regex: RegExp) {
+export function withWarning(func: Function, regex?: RegExp) {
   let message: string = null as never;
   const oldWarn = console.warn;
 
   console.warn = (m: string) => (message = m);
 
   return Promise.resolve(func()).then(val => {
-    expect(message).toMatch(regex);
+    if (regex) {
+      expect(message).toMatch(regex);
+    }
     console.warn = oldWarn;
     return val;
   });
@@ -41,7 +38,15 @@ export function withWarning(func: Function, regex: RegExp) {
 const getIdField = ({ id }: { id: string }) => id;
 
 describe('writing to the store', () => {
-  const writer = new StoreWriter();
+  const policies = new Policies({
+    dataIdFromObject(object: any) {
+      if (object.__typename && object.id) {
+        return object.__typename + '__' + object.id;
+      }
+    },
+  });
+
+  const writer = new StoreWriter({ policies });
 
   it('properly normalizes a trivial item', () => {
     const query = gql`
@@ -68,7 +73,10 @@ describe('writing to the store', () => {
         })
         .toObject(),
     ).toEqual({
-      ROOT_QUERY: result,
+      ROOT_QUERY: {
+        __typename: 'Query',
+        ...result,
+      },
     });
   });
 
@@ -96,6 +104,7 @@ describe('writing to the store', () => {
 
     expect(normalized.toObject()).toEqual({
       ROOT_QUERY: {
+        __typename: "Query",
         id: 'abcd',
         stringField: 'This is a string!',
         numberField: 5,
@@ -130,6 +139,7 @@ describe('writing to the store', () => {
 
     expect(normalized.toObject()).toEqual({
       ROOT_QUERY: {
+        __typename: "Query",
         id: 'abcd',
         'stringField({"arg":1})': 'The arg was 1!',
         'stringField({"arg":2})': 'The arg was 2!',
@@ -170,6 +180,7 @@ describe('writing to the store', () => {
 
     expect(normalized.toObject()).toEqual({
       ROOT_QUERY: {
+        __typename: "Query",
         id: 'abcd',
         nullField: null,
         'numberField({"floatArg":3.14,"intArg":5})': 5,
@@ -212,6 +223,7 @@ describe('writing to the store', () => {
 
     expect(normalized.toObject()).toEqual({
       ROOT_QUERY: {
+        __typename: "Query",
         id: 'abcd',
         nullField: null,
         'numberField({"floatArg":3.14,"intArg":5})': 5,
@@ -244,6 +256,7 @@ describe('writing to the store', () => {
 
     expect(normalized.toObject()).toEqual({
       ROOT_QUERY: {
+        __typename: "Query",
         id: 'abcd',
         firstName: 'James',
         'lastName@upperCase': 'BOND',
@@ -281,22 +294,25 @@ describe('writing to the store', () => {
       },
     };
 
+    const writer = new StoreWriter({
+      policies: new Policies({
+        dataIdFromObject: getIdField,
+      }),
+    });
+
     expect(
       writer
         .writeQueryToStore({
           query,
           result: cloneDeep(result),
-          dataIdFromObject: getIdField,
         })
         .toObject(),
     ).toEqual({
-      ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedObj')), {
-        nestedObj: {
-          type: 'id',
-          id: result.nestedObj.id,
-          generated: false,
-        },
-      }),
+      ROOT_QUERY: {
+        __typename: 'Query',
+        ...result,
+        nestedObj: makeReference(result.nestedObj.id),
+      },
       [result.nestedObj.id]: result.nestedObj,
     });
   });
@@ -336,14 +352,10 @@ describe('writing to the store', () => {
         })
         .toObject(),
     ).toEqual({
-      ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedObj')), {
-        nestedObj: {
-          type: 'id',
-          id: `$ROOT_QUERY.nestedObj`,
-          generated: true,
-        },
-      }),
-      [`$ROOT_QUERY.nestedObj`]: result.nestedObj,
+      ROOT_QUERY: {
+        __typename: 'Query',
+        ...result,
+      },
     });
   });
 
@@ -382,14 +394,10 @@ describe('writing to the store', () => {
         })
         .toObject(),
     ).toEqual({
-      ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedObj')), {
-        'nestedObj({"arg":"val"})': {
-          type: 'id',
-          id: `$ROOT_QUERY.nestedObj({"arg":"val"})`,
-          generated: true,
-        },
+      ROOT_QUERY: assign(omit(result, 'nestedObj'), {
+        __typename: "Query",
+        'nestedObj({"arg":"val"})': result.nestedObj,
       }),
-      [`$ROOT_QUERY.nestedObj({"arg":"val"})`]: result.nestedObj,
     });
   });
 
@@ -430,21 +438,25 @@ describe('writing to the store', () => {
       ],
     };
 
+    const writer = new StoreWriter({
+      policies: new Policies({
+        dataIdFromObject: getIdField,
+      }),
+    });
+
     expect(
       writer
         .writeQueryToStore({
           query,
           result: cloneDeep(result),
-          dataIdFromObject: getIdField,
         })
         .toObject(),
     ).toEqual({
       ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), {
-        nestedArray: result.nestedArray.map((obj: any) => ({
-          type: 'id',
-          id: obj.id,
-          generated: false,
-        })),
+        __typename: "Query",
+        nestedArray: result.nestedArray.map(
+          (obj: any) => makeReference(obj.id),
+        ),
       }),
       [result.nestedArray[0].id]: result.nestedArray[0],
       [result.nestedArray[1].id]: result.nestedArray[1],
@@ -483,20 +495,23 @@ describe('writing to the store', () => {
       ],
     };
 
+    const writer = new StoreWriter({
+      policies: new Policies({
+        dataIdFromObject: getIdField,
+      }),
+    });
+
     expect(
       writer
         .writeQueryToStore({
           query,
           result: cloneDeep(result),
-          dataIdFromObject: getIdField,
         })
         .toObject(),
     ).toEqual({
       ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), {
-        nestedArray: [
-          { type: 'id', id: result.nestedArray[0].id, generated: false },
-          null,
-        ],
+        __typename: "Query",
+        nestedArray: [makeReference(result.nestedArray[0].id), null],
       }),
       [result.nestedArray[0].id]: result.nestedArray[0],
     });
@@ -542,14 +557,10 @@ describe('writing to the store', () => {
     });
 
     expect(normalized.toObject()).toEqual({
-      ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), {
-        nestedArray: [
-          { type: 'id', generated: true, id: `ROOT_QUERY.nestedArray.0` },
-          { type: 'id', generated: true, id: `ROOT_QUERY.nestedArray.1` },
-        ],
-      }),
-      [`ROOT_QUERY.nestedArray.0`]: result.nestedArray[0],
-      [`ROOT_QUERY.nestedArray.1`]: result.nestedArray[1],
+      ROOT_QUERY: {
+        __typename: 'Query',
+        ...result,
+      },
     });
   });
 
@@ -589,13 +600,10 @@ describe('writing to the store', () => {
     });
 
     expect(normalized.toObject()).toEqual({
-      ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), {
-        nestedArray: [
-          null,
-          { type: 'id', generated: true, id: `ROOT_QUERY.nestedArray.1` },
-        ],
-      }),
-      [`ROOT_QUERY.nestedArray.1`]: result.nestedArray[1],
+      ROOT_QUERY: {
+        __typename: 'Query',
+        ...result,
+      },
     });
   });
 
@@ -618,23 +626,22 @@ describe('writing to the store', () => {
       simpleArray: ['one', 'two', 'three'],
     };
 
+    const writer = new StoreWriter({
+      policies: new Policies({
+        dataIdFromObject: getIdField,
+      }),
+    });
+
     const normalized = writer.writeQueryToStore({
       query,
       result: cloneDeep(result),
-      dataIdFromObject: getIdField,
     });
 
     expect(normalized.toObject()).toEqual({
-      ROOT_QUERY: assign({}, assign({}, omit(result, 'simpleArray')), {
-        simpleArray: {
-          type: 'json',
-          json: [
-            result.simpleArray[0],
-            result.simpleArray[1],
-            result.simpleArray[2],
-          ],
-        },
-      }),
+      ROOT_QUERY: {
+        __typename: 'Query',
+        ...result,
+      },
     });
   });
 
@@ -663,16 +670,10 @@ describe('writing to the store', () => {
     });
 
     expect(normalized.toObject()).toEqual({
-      ROOT_QUERY: assign({}, assign({}, omit(result, 'simpleArray')), {
-        simpleArray: {
-          type: 'json',
-          json: [
-            result.simpleArray[0],
-            result.simpleArray[1],
-            result.simpleArray[2],
-          ],
-        },
-      }),
+      ROOT_QUERY: {
+        __typename: 'Query',
+        ...result,
+      },
     });
   });
 
@@ -703,25 +704,23 @@ describe('writing to the store', () => {
       },
     };
 
+    const writer = new StoreWriter({
+      policies: new Policies({
+        dataIdFromObject: getIdField,
+      }),
+    });
+
     const normalized = writer.writeQueryToStore({
       query,
       result: cloneDeep(result),
-      dataIdFromObject: getIdField,
     });
 
     expect(normalized.toObject()).toEqual({
       ROOT_QUERY: {
+        __typename: "Query",
         id: 'a',
-        object1: {
-          type: 'id',
-          id: 'aa',
-          generated: false,
-        },
-        object2: {
-          type: 'id',
-          id: 'aa',
-          generated: false,
-        },
+        object1: makeReference('aa'),
+        object2: makeReference('aa'),
       },
       aa: {
         id: 'aa',
@@ -778,47 +777,33 @@ describe('writing to the store', () => {
       ],
     };
 
+    const writer = new StoreWriter({
+      policies: new Policies({
+        dataIdFromObject: getIdField,
+      }),
+    });
+
     const normalized = writer.writeQueryToStore({
       query,
       result: cloneDeep(result),
-      dataIdFromObject: getIdField,
     });
 
     expect(normalized.toObject()).toEqual({
       ROOT_QUERY: {
+        __typename: "Query",
         id: 'a',
-        array1: [
-          {
-            type: 'id',
-            id: 'aa',
-            generated: false,
-          },
-        ],
-        array2: [
-          {
-            type: 'id',
-            id: 'ab',
-            generated: false,
-          },
-        ],
+        array1: [makeReference('aa')],
+        array2: [makeReference('ab')],
       },
       aa: {
         id: 'aa',
         stringField: 'string',
-        obj: {
-          type: 'id',
-          id: 'aaa',
-          generated: false,
-        },
+        obj: makeReference('aaa'),
       },
       ab: {
         id: 'ab',
         stringField: 'string2',
-        obj: {
-          type: 'id',
-          id: 'aaa',
-          generated: false,
-        },
+        obj: makeReference('aaa'),
       },
       aaa: {
         id: 'aaa',
@@ -868,45 +853,32 @@ describe('writing to the store', () => {
       ],
     };
 
+    const writer = new StoreWriter({
+      policies: new Policies({
+        dataIdFromObject: getIdField,
+      }),
+    });
+
     const normalized = writer.writeQueryToStore({
       query,
       result: cloneDeep(result),
-      dataIdFromObject: getIdField,
     });
 
     expect(normalized.toObject()).toEqual({
       ROOT_QUERY: {
+        __typename: "Query",
         id: 'a',
-        array1: [
-          {
-            type: 'id',
-            id: 'aa',
-            generated: false,
-          },
-          {
-            type: 'id',
-            id: 'ab',
-            generated: false,
-          },
-        ],
+        array1: [makeReference('aa'), makeReference('ab')],
       },
       aa: {
         id: 'aa',
         stringField: 'string',
-        obj: {
-          type: 'id',
-          id: 'aaa',
-          generated: false,
-        },
+        obj: makeReference('aaa'),
       },
       ab: {
         id: 'ab',
         stringField: 'string2',
-        obj: {
-          type: 'id',
-          id: 'aaa',
-          generated: false,
-        },
+        obj: makeReference('aaa'),
       },
       aaa: {
         id: 'aaa',
@@ -931,10 +903,15 @@ describe('writing to the store', () => {
       nullField: null,
     };
 
+    const writer = new StoreWriter({
+      policies: new Policies({
+        dataIdFromObject: getIdField,
+      }),
+    });
+
     const store = writer.writeQueryToStore({
       query,
       result: cloneDeep(result),
-      dataIdFromObject: getIdField,
     });
 
     const query2 = gql`
@@ -955,11 +932,14 @@ describe('writing to the store', () => {
       store,
       query: query2,
       result: result2,
-      dataIdFromObject: getIdField,
     });
 
     expect(store2.toObject()).toEqual({
-      ROOT_QUERY: assign({}, result, result2),
+      ROOT_QUERY: {
+        __typename: 'Query',
+        ...result,
+        ...result2,
+      },
     });
   });
 
@@ -995,9 +975,11 @@ describe('writing to the store', () => {
         })
         .toObject(),
     ).toEqual({
-      ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedObj')), {
+      ROOT_QUERY: {
+        __typename: 'Query',
+        ...result,
         nestedObj: null,
-      }),
+      },
     });
   });
 
@@ -1027,16 +1009,12 @@ describe('writing to the store', () => {
         .toObject(),
     ).toEqual({
       ROOT_QUERY: {
+        __typename: "Query",
         'people_one({"id":"5"})': {
-          type: 'id',
-          id: '$ROOT_QUERY.people_one({"id":"5"})',
-          generated: true,
+          id: 'abcd',
+          stringField: 'This is a string!',
         },
       },
-      '$ROOT_QUERY.people_one({"id":"5"})': {
-        id: 'abcd',
-        stringField: 'This is a string!',
-      },
     });
   });
 
@@ -1187,32 +1165,29 @@ describe('writing to the store', () => {
 
     mutation.definitions.map((def: OperationDefinitionNode) => {
       if (isOperationDefinition(def)) {
+        const writer = new StoreWriter({
+          policies: new Policies({
+            dataIdFromObject() {
+              return '5';
+            },
+          }),
+        });
+
         expect(
-          writer
-            .writeSelectionSetToStore({
-              dataId: '5',
-              selectionSet: def.selectionSet,
-              result: cloneDeep(result),
-              context: {
-                store: defaultNormalizedCacheFactory(),
-                variables,
-                dataIdFromObject: () => '5',
-              },
-            })
-            .toObject(),
+          writer.writeQueryToStore({
+            query: {
+              kind: 'Document',
+              definitions: [def],
+            } as DocumentNode,
+            dataId: '5',
+            result,
+            variables,
+          }).toObject(),
         ).toEqual({
           '5': {
             id: 'id',
-            'some_mutation({"input":{"arr":[1,{"a":"b"}],"bo":true,"id":"5","nil":null,"num":5.5,"obj":{"a":"b"}}})': {
-              generated: false,
-              id: '5',
-              type: 'id',
-            },
-            'some_mutation_with_variables({"input":{"arr":[1,{"a":"b"}],"bo":true,"id":"5","nil":null,"num":5.5,"obj":{"a":"b"}}})': {
-              generated: false,
-              id: '5',
-              type: 'id',
-            },
+            'some_mutation({"input":{"arr":[1,{"a":"b"}],"bo":true,"id":"5","nil":null,"num":5.5,"obj":{"a":"b"}}})': makeReference('5'),
+            'some_mutation_with_variables({"input":{"arr":[1,{"a":"b"}],"bo":true,"id":"5","nil":null,"num":5.5,"obj":{"a":"b"}}})': makeReference('5'),
           },
         });
       } else {
@@ -1221,58 +1196,7 @@ describe('writing to the store', () => {
     });
   });
 
-  it('should write to store if `dataIdFromObject` returns an ID of 0', () => {
-    const query = gql`
-      query {
-        author {
-          firstName
-          id
-          __typename
-        }
-      }
-    `;
-    const data = {
-      author: {
-        id: 0,
-        __typename: 'Author',
-        firstName: 'John',
-      },
-    };
-    const expStore = defaultNormalizedCacheFactory({
-      ROOT_QUERY: {
-        author: {
-          id: 0 as any,
-          typename: 'Author',
-          type: 'id',
-          generated: false,
-        },
-      },
-      0: {
-        id: data.author.id,
-        __typename: data.author.__typename,
-        firstName: data.author.firstName,
-      },
-    });
-
-    expect(
-      writer
-        .writeQueryToStore({
-          result: data,
-          query,
-          dataIdFromObject: () => (0 as any) as string,
-        })
-        .toObject(),
-    ).toEqual(expStore.toObject());
-  });
-
   describe('type escaping', () => {
-    const dataIdFromObject = (object: any) => {
-      if (object.__typename && object.id) {
-        return object.__typename + '__' + object.id;
-      }
-      return undefined;
-    };
-
     it('should correctly escape generated ids', () => {
       const query = gql`
         query {
@@ -1290,13 +1214,9 @@ describe('writing to the store', () => {
       };
       const expStore = defaultNormalizedCacheFactory({
         ROOT_QUERY: {
-          author: {
-            type: 'id' as any,
-            id: '$ROOT_QUERY.author',
-            generated: true,
-          },
+          __typename: 'Query',
+          ...data,
         },
-        '$ROOT_QUERY.author': data.author,
       });
       expect(
         writer
@@ -1327,14 +1247,10 @@ describe('writing to the store', () => {
       };
       const expStore = defaultNormalizedCacheFactory({
         ROOT_QUERY: {
-          author: {
-            type: 'id',
-            id: dataIdFromObject(data.author)!,
-            generated: false,
-            typename: 'Author',
-          },
+          __typename: 'Query',
+          author: policies.toReference(data.author),
         },
-        [dataIdFromObject(data.author)!]: {
+        [policies.identify(data.author)!]: {
           firstName: data.author.firstName,
           id: data.author.id,
           __typename: data.author.__typename,
@@ -1345,13 +1261,12 @@ describe('writing to the store', () => {
           .writeQueryToStore({
             result: data,
             query,
-            dataIdFromObject,
           })
           .toObject(),
       ).toEqual(expStore.toObject());
     });
 
-    it('should correctly escape json blobs', () => {
+    it('should not need to escape json blobs', () => {
       const query = gql`
         query {
           author {
@@ -1372,20 +1287,13 @@ describe('writing to the store', () => {
       };
       const expStore = defaultNormalizedCacheFactory({
         ROOT_QUERY: {
-          author: {
-            type: 'id',
-            id: dataIdFromObject(data.author)!,
-            generated: false,
-            typename: 'Author',
-          },
+          __typename: 'Query',
+          author: policies.toReference(data.author),
         },
-        [dataIdFromObject(data.author)!]: {
+        [policies.identify(data.author)!]: {
           __typename: data.author.__typename,
           id: data.author.id,
-          info: {
-            type: 'json',
-            json: data.author.info,
-          },
+          info: data.author.info,
         },
       });
       expect(
@@ -1393,14 +1301,13 @@ describe('writing to the store', () => {
           .writeQueryToStore({
             result: data,
             query,
-            dataIdFromObject,
           })
           .toObject(),
       ).toEqual(expStore.toObject());
     });
   });
 
-  it('should merge objects when overwriting a generated id with a real id', () => {
+  it('should not merge unidentified data when replacing with ID reference', () => {
     const dataWithoutId = {
       author: {
         firstName: 'John',
@@ -1416,12 +1323,7 @@ describe('writing to the store', () => {
         __typename: 'Author',
       },
     };
-    const dataIdFromObject = (object: any) => {
-      if (object.__typename && object.id) {
-        return object.__typename + '__' + object.id;
-      }
-      return undefined;
-    };
+
     const queryWithoutId = gql`
       query {
         author {
@@ -1440,50 +1342,40 @@ describe('writing to the store', () => {
         }
       }
     `;
-    const expStoreWithoutId = defaultNormalizedCacheFactory({
-      '$ROOT_QUERY.author': {
-        firstName: 'John',
-        lastName: 'Smith',
-        __typename: 'Author',
-      },
+    const expectedStoreWithoutId = defaultNormalizedCacheFactory({
       ROOT_QUERY: {
+        __typename: 'Query',
         author: {
-          type: 'id',
-          id: '$ROOT_QUERY.author',
-          generated: true,
-          typename: 'Author',
+          firstName: 'John',
+          lastName: 'Smith',
+          __typename: 'Author',
         },
       },
     });
-    const expStoreWithId = defaultNormalizedCacheFactory({
+    const expectedStoreWithId = defaultNormalizedCacheFactory({
       Author__129: {
         firstName: 'John',
-        lastName: 'Smith',
         id: '129',
         __typename: 'Author',
       },
       ROOT_QUERY: {
-        author: {
-          type: 'id',
-          id: 'Author__129',
-          generated: false,
-          typename: 'Author',
-        },
+        __typename: 'Query',
+        author: makeReference('Author__129'),
       },
     });
+
     const storeWithoutId = writer.writeQueryToStore({
       result: dataWithoutId,
       query: queryWithoutId,
-      dataIdFromObject,
     });
-    expect(storeWithoutId.toObject()).toEqual(expStoreWithoutId.toObject());
+    expect(storeWithoutId.toObject()).toEqual(expectedStoreWithoutId.toObject());
+
     const storeWithId = writer.writeQueryToStore({
       result: dataWithId,
       query: queryWithId,
       store: storeWithoutId,
-      dataIdFromObject,
     });
-    expect(storeWithId.toObject()).toEqual(expStoreWithId.toObject());
+    expect(storeWithId.toObject()).toEqual(expectedStoreWithId.toObject());
   });
 
   it('should allow a union of objects of a different type, when overwriting a generated id with a real id', () => {
@@ -1501,12 +1393,6 @@ describe('writing to the store', () => {
         __typename: 'Author',
       },
     };
-    const dataIdFromObject = (object: any) => {
-      if (object.__typename && object.id) {
-        return object.__typename + '__' + object.id;
-      }
-      return undefined;
-    };
     const query = gql`
       query {
         author {
@@ -1524,16 +1410,11 @@ describe('writing to the store', () => {
       }
     `;
     const expStoreWithPlaceholder = defaultNormalizedCacheFactory({
-      '$ROOT_QUERY.author': {
-        hello: 'Foo',
-        __typename: 'Placeholder',
-      },
       ROOT_QUERY: {
+        __typename: 'Query',
         author: {
-          type: 'id',
-          id: '$ROOT_QUERY.author',
-          generated: true,
-          typename: 'Placeholder',
+          hello: 'Foo',
+          __typename: 'Placeholder',
         },
       },
     });
@@ -1545,12 +1426,8 @@ describe('writing to the store', () => {
         __typename: 'Author',
       },
       ROOT_QUERY: {
-        author: {
-          type: 'id',
-          id: 'Author__129',
-          generated: false,
-          typename: 'Author',
-        },
+        __typename: 'Query',
+        author: makeReference('Author__129'),
       },
     });
 
@@ -1558,7 +1435,6 @@ describe('writing to the store', () => {
     const store = writer.writeQueryToStore({
       result: dataWithPlaceholder,
       query,
-      dataIdFromObject,
     });
     expect(store.toObject()).toEqual(expStoreWithPlaceholder.toObject());
 
@@ -1567,7 +1443,6 @@ describe('writing to the store', () => {
       result: dataWithAuthor,
       query,
       store,
-      dataIdFromObject,
     });
     expect(store.toObject()).toEqual(expStoreWithAuthor.toObject());
 
@@ -1576,7 +1451,6 @@ describe('writing to the store', () => {
       result: dataWithPlaceholder,
       query,
       store,
-      dataIdFromObject,
     });
     // Author__129 will remain in the store,
     // but will not be referenced by any of the fields,
@@ -1658,19 +1532,21 @@ describe('writing to the store', () => {
         ],
       };
 
-      const newStore = writer.writeResultToStore({
-        dataId: 'ROOT_QUERY',
+      const writer = new StoreWriter({
+        policies: new Policies({
+          dataIdFromObject: getIdField,
+        }),
+      });
+
+      const newStore = writer.writeQueryToStore({
+        query,
         result,
-        document: query,
-        dataIdFromObject: getIdField,
       });
 
       expect(newStore.get('1')).toEqual(result.todos[0]);
     });
 
-    it('should warn when it receives the wrong data with non-union fragments (using an heuristic matcher)', () => {
-      const fragmentMatcherFunction = new HeuristicFragmentMatcher().match;
-
+    it('should warn when it receives the wrong data with non-union fragments', () => {
       const result = {
         todos: [
           {
@@ -1680,37 +1556,24 @@ describe('writing to the store', () => {
         ],
       };
 
+      const writer = new StoreWriter({
+        policies: new Policies({
+          dataIdFromObject: getIdField,
+          possibleTypes: {},
+        }),
+      });
+
       return withWarning(() => {
-        const newStore = writer.writeResultToStore({
-          dataId: 'ROOT_QUERY',
+        const newStore = writer.writeQueryToStore({
+          query,
           result,
-          document: query,
-          dataIdFromObject: getIdField,
-          fragmentMatcherFunction,
         });
 
         expect(newStore.get('1')).toEqual(result.todos[0]);
       }, /Missing field description/);
     });
 
-    it('should warn when it receives the wrong data inside a fragment (using an introspection matcher)', () => {
-      const fragmentMatcherFunction = new IntrospectionFragmentMatcher({
-        introspectionQueryResultData: {
-          __schema: {
-            types: [
-              {
-                kind: 'UNION',
-                name: 'Todo',
-                possibleTypes: [
-                  { name: 'ShoppingCartItem' },
-                  { name: 'TaskItem' },
-                ],
-              },
-            ],
-          },
-        },
-      }).match;
-
+    it('should warn when it receives the wrong data inside a fragment', () => {
       const queryWithInterface = gql`
         query {
           todos {
@@ -1745,22 +1608,26 @@ describe('writing to the store', () => {
         ],
       };
 
+      const writer = new StoreWriter({
+        policies: new Policies({
+          dataIdFromObject: getIdField,
+          possibleTypes: {
+            Todo: ["ShoppingCartItem", "TaskItem"],
+          },
+        }),
+      });
+
       return withWarning(() => {
-        const newStore = writer.writeResultToStore({
-          dataId: 'ROOT_QUERY',
+        const newStore = writer.writeQueryToStore({
+          query: queryWithInterface,
           result,
-          document: queryWithInterface,
-          dataIdFromObject: getIdField,
-          fragmentMatcherFunction,
         });
 
         expect(newStore.get('1')).toEqual(result.todos[0]);
       }, /Missing field price/);
     });
 
-    it('should warn if a result is missing __typename when required (using an heuristic matcher)', () => {
-      const fragmentMatcherFunction = new HeuristicFragmentMatcher().match;
-
+    it('should warn if a result is missing __typename when required', () => {
       const result: any = {
         todos: [
           {
@@ -1771,13 +1638,17 @@ describe('writing to the store', () => {
         ],
       };
 
+      const writer = new StoreWriter({
+        policies: new Policies({
+          dataIdFromObject: getIdField,
+          possibleTypes: {},
+        }),
+      });
+
       return withWarning(() => {
-        const newStore = writer.writeResultToStore({
-          dataId: 'ROOT_QUERY',
+        const newStore = writer.writeQueryToStore({
+          query: addTypenameToDocument(query),
           result,
-          document: addTypenameToDocument(query),
-          dataIdFromObject: getIdField,
-          fragmentMatcherFunction,
         });
 
         expect(newStore.get('1')).toEqual(result.todos[0]);
@@ -1789,14 +1660,21 @@ describe('writing to the store', () => {
         todos: null,
       };
 
-      const newStore = writer.writeResultToStore({
-        dataId: 'ROOT_QUERY',
+      const writer = new StoreWriter({
+        policies: new Policies({
+          dataIdFromObject: getIdField,
+        }),
+      });
+
+      const newStore = writer.writeQueryToStore({
+        query,
         result,
-        document: query,
-        dataIdFromObject: getIdField,
       });
 
-      expect(newStore.get('ROOT_QUERY')).toEqual({ todos: null });
+      expect(newStore.get('ROOT_QUERY')).toEqual({
+        __typename: 'Query',
+        todos: null,
+      });
     });
     it('should not warn if a field is defered', () => {
       let originalWarn = console.warn;
@@ -1811,16 +1689,18 @@ describe('writing to the store', () => {
         id: 1,
       };
 
-      const fragmentMatcherFunction = new HeuristicFragmentMatcher().match;
-      const newStore = writer.writeResultToStore({
-        dataId: 'ROOT_QUERY',
+      const writer = new StoreWriter({
+        policies: new Policies({
+          dataIdFromObject: getIdField,
+        }),
+      });
+
+      const newStore = writer.writeQueryToStore({
+        query: defered,
         result,
-        document: defered,
-        dataIdFromObject: getIdField,
-        fragmentMatcherFunction,
       });
 
-      expect(newStore.get('ROOT_QUERY')).toEqual({ id: 1 });
+      expect(newStore.get('ROOT_QUERY')).toEqual({ __typename: 'Query', id: 1 });
       expect(console.warn).not.toBeCalled();
       console.warn = originalWarn;
     });
@@ -1828,25 +1708,21 @@ describe('writing to the store', () => {
 
   it('throws when trying to write an object without id that was previously queried with id', () => {
     const store = defaultNormalizedCacheFactory({
-      ROOT_QUERY: assign(
-        {},
-        {
-          __typename: 'Query',
-          item: {
-            type: 'id',
-            id: 'abcd',
-            generated: false,
-          } as IdValue,
-        },
-      ) as StoreObject,
-      abcd: assign(
-        {},
-        {
-          id: 'abcd',
-          __typename: 'Item',
-          stringField: 'This is a string!',
-        },
-      ) as StoreObject,
+      ROOT_QUERY: {
+        __typename: 'Query',
+        item: makeReference('abcd'),
+      },
+      abcd: {
+        id: 'abcd',
+        __typename: 'Item',
+        stringField: 'This is a string!',
+      },
+    });
+
+    const writer = new StoreWriter({
+      policies: new Policies({
+        dataIdFromObject: getIdField,
+      }),
     });
 
     expect(() => {
@@ -1865,33 +1741,30 @@ describe('writing to the store', () => {
             }
           }
         `,
-        dataIdFromObject: getIdField,
       });
     }).toThrowErrorMatchingSnapshot();
 
     expect(() => {
-      writer.writeResultToStore({
+      writer.writeQueryToStore({
         store,
-        result: {
-          item: {
-            __typename: 'Item',
-            stringField: 'This is still a string!',
-          },
-        },
-        dataId: 'ROOT_QUERY',
-        document: gql`
+        query: gql`
           query {
             item {
               stringField
             }
           }
         `,
-        dataIdFromObject: getIdField,
+        result: {
+          item: {
+            __typename: 'Item',
+            stringField: 'This is still a string!',
+          },
+        },
       });
-    }).toThrowError(/stringField(.|\n)*abcd/g);
+    }).toThrowError(/contains an id of abcd/g);
   });
 
-  it('properly handles the connection directive', () => {
+  it('properly handles the @connection directive', () => {
     const store = defaultNormalizedCacheFactory();
 
     writer.writeQueryToStore({
@@ -1932,16 +1805,87 @@ describe('writing to the store', () => {
 
     expect(store.toObject()).toEqual({
       ROOT_QUERY: {
-        abc: [
+        __typename: "Query",
+        'books:abc': [
           {
-            generated: true,
-            id: 'ROOT_QUERY.abc.0',
-            type: 'id',
+            name: 'efgh',
           },
         ],
       },
-      'ROOT_QUERY.abc.0': {
-        name: 'efgh',
+    });
+  });
+
+  it('can use keyArgs function instead of @connection directive', () => {
+    const store = defaultNormalizedCacheFactory();
+    const writer = new StoreWriter({
+      policies: new Policies({
+        typePolicies: {
+          Query: {
+            fields: {
+              books: {
+                keyArgs: () => "abc",
+              },
+            },
+          },
+        },
+      }),
+    });
+
+    writer.writeQueryToStore({
+      query: gql`
+        {
+          books(skip: 0, limit: 2) {
+            name
+          }
+        }
+      `,
+      result: {
+        books: [
+          {
+            name: 'abcd',
+          },
+        ],
+      },
+      store,
+    });
+
+    expect(store.toObject()).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        "books:abc": [
+          {
+            name: 'abcd',
+          },
+        ],
+      },
+    });
+
+    writer.writeQueryToStore({
+      query: gql`
+        {
+          books(skip: 2, limit: 4) {
+            name
+          }
+        }
+      `,
+      result: {
+        books: [
+          {
+            name: 'efgh',
+          },
+        ],
+      },
+      store,
+    });
+
+    expect(store.toObject()).toEqual({
+      ROOT_QUERY: {
+        __typename: "Query",
+        "books:abc": [
+          {
+            name: 'efgh',
+          },
+        ],
       },
     });
   });
@@ -1976,25 +1920,18 @@ describe('writing to the store', () => {
     });
 
     expect(store.toObject()).toEqual({
-      '$ROOT_QUERY.animals.0.species': { name: 'cat' },
       ROOT_QUERY: {
+        __typename: "Query",
         animals: [
           {
-            generated: true,
-            id: 'ROOT_QUERY.animals.0',
-            type: 'id',
-            typename: 'Animal',
+            __typename: 'Animal',
+            species: {
+              __typename: 'Cat',
+              name: 'cat',
+            },
           },
         ],
       },
-      'ROOT_QUERY.animals.0': {
-        species: {
-          generated: true,
-          id: '$ROOT_QUERY.animals.0.species',
-          type: 'id',
-          typename: 'Cat',
-        },
-      },
     });
 
     writer.writeQueryToStore({
@@ -2014,38 +1951,24 @@ describe('writing to the store', () => {
     });
 
     expect(store.toObject()).toEqual({
-      '$ROOT_QUERY.animals.0.species': { name: 'dog' },
       ROOT_QUERY: {
+        __typename: "Query",
         animals: [
           {
-            generated: true,
-            id: 'ROOT_QUERY.animals.0',
-            type: 'id',
-            typename: 'Animal',
+            __typename: 'Animal',
+            species: {
+              __typename: 'Dog',
+              name: 'dog',
+            },
           },
         ],
       },
-      'ROOT_QUERY.animals.0': {
-        species: {
-          generated: true,
-          id: '$ROOT_QUERY.animals.0.species',
-          type: 'id',
-          typename: 'Dog',
-        },
-      },
     });
   });
 
   it('should not keep reference when type of mixed inlined field changes to non-inlined field', () => {
     const store = defaultNormalizedCacheFactory();
 
-    const dataIdFromObject = (object: any) => {
-      if (object.__typename && object.id) {
-        return object.__typename + '__' + object.id;
-      }
-      return undefined;
-    };
-
     const query = gql`
       query {
         animals {
@@ -2070,30 +1993,22 @@ describe('writing to the store', () => {
           },
         ],
       },
-      dataIdFromObject,
       store,
     });
 
     expect(store.toObject()).toEqual({
-      '$ROOT_QUERY.animals.0.species': { name: 'cat' },
       ROOT_QUERY: {
+        __typename: "Query",
         animals: [
           {
-            generated: true,
-            id: 'ROOT_QUERY.animals.0',
-            type: 'id',
-            typename: 'Animal',
+            __typename: 'Animal',
+            species: {
+              __typename: 'Cat',
+              name: 'cat',
+            },
           },
         ],
       },
-      'ROOT_QUERY.animals.0': {
-        species: {
-          generated: true,
-          id: '$ROOT_QUERY.animals.0.species',
-          type: 'id',
-          typename: 'Cat',
-        },
-      },
     });
 
     writer.writeQueryToStore({
@@ -2110,34 +2025,60 @@ describe('writing to the store', () => {
           },
         ],
       },
-      dataIdFromObject,
       store,
     });
 
     expect(store.toObject()).toEqual({
-      '$ROOT_QUERY.animals.0.species': undefined,
       'Dog__dog-species': {
         id: 'dog-species',
+        __typename: 'Dog',
         name: 'dog',
       },
       ROOT_QUERY: {
+        __typename: "Query",
         animals: [
           {
-            generated: true,
-            id: 'ROOT_QUERY.animals.0',
-            type: 'id',
-            typename: 'Animal',
+            __typename: 'Animal',
+            species: makeReference('Dog__dog-species'),
           },
         ],
       },
-      'ROOT_QUERY.animals.0': {
-        species: {
-          generated: false,
-          id: 'Dog__dog-species',
-          type: 'id',
-          typename: 'Dog',
-        },
+    });
+  });
+
+  it('should not deep-freeze scalar objects', () => {
+    const query = gql`
+      query {
+        scalarFieldWithObjectValue
+      }
+    `;
+
+    const scalarObject = {
+      a: 1,
+      b: [2, 3],
+      c: {
+        d: 4,
+        e: 5,
+      },
+    };
+
+    const cache = new InMemoryCache();
+
+    cache.writeQuery({
+      query,
+      data: {
+        scalarFieldWithObjectValue: scalarObject,
       },
     });
+
+    expect(Object.isFrozen(scalarObject)).toBe(false);
+    expect(Object.isFrozen(scalarObject.b)).toBe(false);
+    expect(Object.isFrozen(scalarObject.c)).toBe(false);
+
+    const result = cache.readQuery<any>({ query });
+    expect(result.scalarFieldWithObjectValue).not.toBe(scalarObject);
+    expect(Object.isFrozen(result.scalarFieldWithObjectValue)).toBe(true);
+    expect(Object.isFrozen(result.scalarFieldWithObjectValue.b)).toBe(true);
+    expect(Object.isFrozen(result.scalarFieldWithObjectValue.c)).toBe(true);
   });
 });
diff --git a/packages/apollo-client/src/core/__tests__/LocalState.ts b/src/core/__tests__/LocalState.ts
similarity index 93%
rename from packages/apollo-client/src/core/__tests__/LocalState.ts
rename to src/core/__tests__/LocalState.ts
--- a/packages/apollo-client/src/core/__tests__/LocalState.ts
+++ b/src/core/__tests__/LocalState.ts
@@ -1,6 +1,4 @@
-import ApolloClient from '../..';
-import { InMemoryCache } from 'apollo-cache-inmemory';
-import gql from 'graphql-tag';
+import { ApolloClient, InMemoryCache, gql } from '../..';
 
 /**
  * Creates an apollo-client instance with a local query resolver named 'localQuery'.
diff --git a/packages/apollo-client/src/core/__tests__/ObservableQuery.ts b/src/core/__tests__/ObservableQuery.ts
similarity index 79%
rename from packages/apollo-client/src/core/__tests__/ObservableQuery.ts
rename to src/core/__tests__/ObservableQuery.ts
--- a/packages/apollo-client/src/core/__tests__/ObservableQuery.ts
+++ b/src/core/__tests__/ObservableQuery.ts
@@ -1,24 +1,22 @@
 import gql from 'graphql-tag';
-import { ApolloLink, Observable } from 'apollo-link';
-import {
-  InMemoryCache,
-  IntrospectionFragmentMatcher,
-} from 'apollo-cache-inmemory';
 import { GraphQLError } from 'graphql';
 
-import mockQueryManager from '../../__mocks__/mockQueryManager';
-import mockWatchQuery from '../../__mocks__/mockWatchQuery';
-import { mockSingleLink } from '../../__mocks__/mockLinks';
+import { Observable } from '../../utilities/observables/Observable';
+import { ApolloLink } from '../../link/core/ApolloLink';
+import { InMemoryCache } from '../../cache/inmemory/inMemoryCache';
+import mockQueryManager from '../../utilities/testing/mocking/mockQueryManager';
+import mockWatchQuery from '../../utilities/testing/mocking/mockWatchQuery';
+import { mockSingleLink } from '../../utilities/testing/mocking/mockLink';
 
 import { ObservableQuery } from '../ObservableQuery';
 import { NetworkStatus } from '../networkStatus';
 import { QueryManager } from '../QueryManager';
-import { DataStore } from '../../data/store';
-import ApolloClient from '../../';
+import { ApolloClient } from '../../';
 
-import wrap from '../../util/wrap';
-import subscribeAndCount from '../../util/subscribeAndCount';
-import { stripSymbols } from 'apollo-utilities';
+import wrap from '../../utilities/testing/wrap';
+import subscribeAndCount from '../../utilities/testing/subscribeAndCount';
+import { stripSymbols } from '../../utilities/testing/stripSymbols';
+import { itAsync } from '../../utilities/testing/itAsync';
 import { ApolloError } from '../../errors/ApolloError';
 
 describe('ObservableQuery', () => {
@@ -48,26 +46,21 @@ describe('ObservableQuery', () => {
     message: 'is offline.',
   };
 
-  const createQueryManager = ({ link }: { link?: ApolloLink }) => {
+  const createQueryManager = ({ link }: { link: ApolloLink }) => {
     return new QueryManager({
-      link: link || mockSingleLink(),
+      link,
       assumeImmutableResults: true,
-      store: new DataStore(
-        new InMemoryCache({
-          addTypename: false,
-          freezeResults: true,
-        }),
-      ),
+      cache: new InMemoryCache({
+        addTypename: false,
+      }),
     });
   };
 
   describe('setOptions', () => {
     describe('to change pollInterval', () => {
-      beforeEach(() => jest.useFakeTimers());
-      afterEach(() => jest.useRealTimers());
-
-      it('starts polling if goes from 0 -> something', done => {
+      itAsync('starts polling if goes from 0 -> something', (resolve, reject) => {
         const manager = mockQueryManager(
+          reject,
           {
             request: { query, variables },
             result: { data: dataOne },
@@ -87,23 +80,22 @@ describe('ObservableQuery', () => {
           variables,
           notifyOnNetworkStatusChange: false,
         });
-        subscribeAndCount(done, observable, (handleCount, result) => {
+
+        subscribeAndCount(reject, observable, (handleCount, result) => {
           if (handleCount === 1) {
             expect(stripSymbols(result.data)).toEqual(dataOne);
             observable.setOptions({ pollInterval: 10 });
-            // 10 for the poll and an extra 1 for network requests
-            jest.runTimersToTime(11);
           } else if (handleCount === 2) {
             expect(stripSymbols(result.data)).toEqual(dataTwo);
-            done();
+            observable.stopPolling();
+            resolve();
           }
         });
-
-        jest.runTimersToTime(1);
       });
 
-      it('stops polling if goes from something -> 0', done => {
+      itAsync('stops polling if goes from something -> 0', (resolve, reject) => {
         const manager = mockQueryManager(
+          reject,
           {
             request: { query, variables },
             result: { data: dataOne },
@@ -123,24 +115,21 @@ describe('ObservableQuery', () => {
           variables,
           pollInterval: 10,
         });
-        subscribeAndCount(done, observable, (handleCount, result) => {
+
+        subscribeAndCount(reject, observable, (handleCount, result) => {
           if (handleCount === 1) {
             expect(stripSymbols(result.data)).toEqual(dataOne);
             observable.setOptions({ pollInterval: 0 });
-
-            jest.runTimersToTime(5);
-            done();
+            setTimeout(resolve, 5);
           } else if (handleCount === 2) {
-            done.fail(new Error('Should not get more than one result'));
+            reject(new Error('Should not get more than one result'));
           }
         });
-
-        // trigger the first subscription callback
-        jest.runTimersToTime(1);
       });
 
-      it('can change from x>0 to y>0', done => {
+      itAsync('can change from x>0 to y>0', (resolve, reject) => {
         const manager = mockQueryManager(
+          reject,
           {
             request: { query, variables },
             result: { data: dataOne },
@@ -161,34 +150,21 @@ describe('ObservableQuery', () => {
           pollInterval: 100,
           notifyOnNetworkStatusChange: false,
         });
-        subscribeAndCount(done, observable, (handleCount, result) => {
+
+        subscribeAndCount(reject, observable, (handleCount, result) => {
           if (handleCount === 1) {
             expect(stripSymbols(result.data)).toEqual(dataOne);
-
-            // It's confusing but we need to ensure we let the scheduler
-            // come back from fetching before we mess with it.
-            setImmediate(() => {
-              observable.setOptions({ pollInterval: 10 });
-
-              // Again, the scheduler needs to complete setting up the poll
-              // before the timer goes off
-              setImmediate(() => {
-                // just enough to trigger a second data
-                jest.runTimersToTime(11);
-              });
-            });
+            observable.setOptions({ pollInterval: 10 });
           } else if (handleCount === 2) {
             expect(stripSymbols(result.data)).toEqual(dataTwo);
-            done();
+            observable.stopPolling();
+            resolve();
           }
         });
-
-        // trigger the first subscription callback
-        jest.runTimersToTime(0);
       });
     });
 
-    it('does not break refetch', done => {
+    itAsync('does not break refetch', (resolve, reject) => {
       // This query and variables are copied from react-apollo
       const queryWithVars = gql`
         query people($first: Int) {
@@ -207,6 +183,7 @@ describe('ObservableQuery', () => {
       const variables2 = { first: 1 };
 
       const observable: ObservableQuery<any> = mockWatchQuery(
+        reject,
         {
           request: {
             query: queryWithVars,
@@ -223,7 +200,7 @@ describe('ObservableQuery', () => {
         },
       );
 
-      subscribeAndCount(done, observable, (handleCount, result) => {
+      subscribeAndCount(reject, observable, (handleCount, result) => {
         if (handleCount === 1) {
           expect(stripSymbols(result.data)).toEqual(data);
           return observable.refetch(variables2);
@@ -232,12 +209,12 @@ describe('ObservableQuery', () => {
           expect(result.loading).toBe(true);
         } else if (handleCount === 3) {
           expect(stripSymbols(result.data)).toEqual(data2);
-          done();
+          resolve();
         }
       });
     });
 
-    it('rerenders when refetch is called', done => {
+    itAsync('rerenders when refetch is called', (resolve, reject) => {
       // This query and variables are copied from react-apollo
       const query = gql`
         query people($first: Int) {
@@ -255,6 +232,7 @@ describe('ObservableQuery', () => {
       const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } };
 
       const observable: ObservableQuery<any> = mockWatchQuery(
+        reject,
         {
           request: {
             query,
@@ -271,18 +249,18 @@ describe('ObservableQuery', () => {
         },
       );
 
-      subscribeAndCount(done, observable, (handleCount, result) => {
+      subscribeAndCount(reject, observable, (handleCount, result) => {
         if (handleCount === 1) {
           expect(stripSymbols(result.data)).toEqual(data);
           return observable.refetch();
         } else if (handleCount === 2) {
           expect(stripSymbols(result.data)).toEqual(data2);
-          done();
+          resolve();
         }
       });
     });
 
-    it('rerenders with new variables then shows correct data for previous variables', done => {
+    itAsync('rerenders with new variables then shows correct data for previous variables', (resolve, reject) => {
       // This query and variables are copied from react-apollo
       const query = gql`
         query people($first: Int) {
@@ -301,6 +279,7 @@ describe('ObservableQuery', () => {
       const variables2 = { first: 1 };
 
       const observable: ObservableQuery<any> = mockWatchQuery(
+        reject,
         {
           request: {
             query,
@@ -317,7 +296,7 @@ describe('ObservableQuery', () => {
         },
       );
 
-      subscribeAndCount(done, observable, async (handleCount, result) => {
+      subscribeAndCount(reject, observable, async (handleCount, result) => {
         if (handleCount === 1) {
           expect(stripSymbols(result.data)).toEqual(data);
           await observable.setOptions({ variables: variables2 });
@@ -327,20 +306,18 @@ describe('ObservableQuery', () => {
         } else if (handleCount === 3) {
           expect(stripSymbols(result.data)).toEqual(data2);
           // go back to first set of variables
-          await observable.setOptions({ variables });
-          const current = observable.getCurrentResult();
+          const current = await observable.setOptions({ variables });
           expect(stripSymbols(current.data)).toEqual(data);
-          const secondCurrent = observable.getCurrentResult();
-          expect(current.data).toEqual(secondCurrent.data);
-          done();
+          resolve();
         }
       });
     });
 
     // TODO: Something isn't quite right with this test. It's failing but not
     // for the right reasons.
-    it.skip('if query is refetched, and an error is returned, no other observer callbacks will be called', done => {
+    itAsync.skip('if query is refetched, and an error is returned, no other observer callbacks will be called', (resolve, reject) => {
       const observable: ObservableQuery<any> = mockWatchQuery(
+        reject,
         {
           request: { query, variables },
           result: { data: dataOne },
@@ -370,13 +347,14 @@ describe('ObservableQuery', () => {
           handleCount++;
           expect(handleCount).toBe(2);
           observable.refetch();
-          setTimeout(done, 25);
+          setTimeout(resolve, 25);
         },
       });
     });
 
-    it('does a network request if fetchPolicy becomes networkOnly', done => {
+    itAsync('does a network request if fetchPolicy becomes networkOnly', (resolve, reject) => {
       const observable: ObservableQuery<any> = mockWatchQuery(
+        reject,
         {
           request: { query, variables },
           result: { data: dataOne },
@@ -387,18 +365,18 @@ describe('ObservableQuery', () => {
         },
       );
 
-      subscribeAndCount(done, observable, (handleCount, result) => {
+      subscribeAndCount(reject, observable, (handleCount, result) => {
         if (handleCount === 1) {
           expect(stripSymbols(result.data)).toEqual(dataOne);
           return observable.setOptions({ fetchPolicy: 'network-only' });
         } else if (handleCount === 2) {
           expect(stripSymbols(result.data)).toEqual(dataTwo);
-          done();
+          resolve();
         }
       });
     });
 
-    it('does a network request if fetchPolicy is cache-only then store is reset then fetchPolicy becomes not cache-only', done => {
+    itAsync('does a network request if fetchPolicy is cache-only then store is reset then fetchPolicy becomes not cache-only', (resolve, reject) => {
       let queryManager: QueryManager;
       let observable: ObservableQuery<any>;
       const testQuery = gql`
@@ -431,7 +409,7 @@ describe('ObservableQuery', () => {
       // fetch first data from server
       observable = queryManager.watchQuery({ query: testQuery });
 
-      subscribeAndCount(done, observable, async (handleCount, result) => {
+      subscribeAndCount(reject, observable, async (handleCount, result) => {
         try {
           if (handleCount === 1) {
             expect(stripSymbols(result.data)).toEqual(data);
@@ -446,15 +424,15 @@ describe('ObservableQuery', () => {
           } else if (handleCount === 3) {
             expect(stripSymbols(result.data)).toEqual(data);
             expect(timesFired).toBe(2);
-            done();
+            resolve();
           }
         } catch (e) {
-          done.fail(e);
+          reject(e);
         }
       });
     });
 
-    it('does a network request if fetchPolicy changes from cache-only', done => {
+    itAsync('does a network request if fetchPolicy changes from cache-only', (resolve, reject) => {
       let queryManager: QueryManager;
       let observable: ObservableQuery<any>;
       const testQuery = gql`
@@ -490,20 +468,20 @@ describe('ObservableQuery', () => {
         notifyOnNetworkStatusChange: false,
       });
 
-      subscribeAndCount(done, observable, async (handleCount, result) => {
-        if (handleCount === 2) {
+      subscribeAndCount(reject, observable, async (handleCount, result) => {
+        if (handleCount === 1) {
           expect(stripSymbols(result.data)).toEqual({});
           expect(timesFired).toBe(0);
           await observable.setOptions({ fetchPolicy: 'cache-first' });
-        } else if (handleCount === 3) {
+        } else if (handleCount === 2) {
           expect(stripSymbols(result.data)).toEqual(data);
           expect(timesFired).toBe(1);
-          done();
+          resolve();
         }
       });
     });
 
-    it('can set queries to standby and will not fetch when doing so', done => {
+    itAsync('can set queries to standby and will not fetch when doing so', (resolve, reject) => {
       let queryManager: QueryManager;
       let observable: ObservableQuery<any>;
       const testQuery = gql`
@@ -539,21 +517,21 @@ describe('ObservableQuery', () => {
         notifyOnNetworkStatusChange: false,
       });
 
-      subscribeAndCount(done, observable, async (handleCount, result) => {
+      subscribeAndCount(reject, observable, async (handleCount, result) => {
         if (handleCount === 1) {
           expect(stripSymbols(result.data)).toEqual(data);
           expect(timesFired).toBe(1);
           await observable.setOptions({ fetchPolicy: 'standby' });
           // make sure the query didn't get fired again.
           expect(timesFired).toBe(1);
-          done();
+          resolve();
         } else if (handleCount === 2) {
           throw new Error('Handle should not be triggered on standby query');
         }
       });
     });
 
-    it('will not fetch when setting a cache-only query to standby', done => {
+    itAsync('will not fetch when setting a cache-only query to standby', (resolve, reject) => {
       let queryManager: QueryManager;
       let observable: ObservableQuery<any>;
       const testQuery = gql`
@@ -591,22 +569,24 @@ describe('ObservableQuery', () => {
           notifyOnNetworkStatusChange: false,
         });
 
-        subscribeAndCount(done, observable, async (handleCount, result) => {
+        subscribeAndCount(reject, observable, async (handleCount, result) => {
           if (handleCount === 1) {
             expect(stripSymbols(result.data)).toEqual(data);
             expect(timesFired).toBe(1);
             await observable.setOptions({ fetchPolicy: 'standby' });
             // make sure the query didn't get fired again.
             expect(timesFired).toBe(1);
-            done();
+            resolve();
           } else if (handleCount === 2) {
             throw new Error('Handle should not be triggered on standby query');
           }
         });
       });
     });
-    it('returns a promise which eventually returns data', done => {
+
+    itAsync('returns a promise which eventually returns data', (resolve, reject) => {
       const observable: ObservableQuery<any> = mockWatchQuery(
+        reject,
         {
           request: { query, variables },
           result: { data: dataOne },
@@ -617,7 +597,7 @@ describe('ObservableQuery', () => {
         },
       );
 
-      subscribeAndCount(done, observable, handleCount => {
+      subscribeAndCount(reject, observable, handleCount => {
         if (handleCount !== 1) {
           return;
         }
@@ -626,12 +606,14 @@ describe('ObservableQuery', () => {
           .then(res => {
             // returns dataOne from cache
             expect(stripSymbols(res.data)).toEqual(dataOne);
-            done();
+            resolve();
           });
       });
     });
-    it('can bypass looking up results if passed to options', done => {
+
+    itAsync('can bypass looking up results if passed to options', (resolve, reject) => {
       const observable: ObservableQuery<any> = mockWatchQuery(
+        reject,
         {
           request: { query, variables },
           result: { data: dataOne },
@@ -643,13 +625,13 @@ describe('ObservableQuery', () => {
       );
 
       let errored = false;
-      subscribeAndCount(done, observable, handleCount => {
+      subscribeAndCount(reject, observable, handleCount => {
         if (handleCount === 1) {
           observable
             .setOptions({ fetchResults: false, fetchPolicy: 'standby' })
             .then(res => {
               expect(res).toBeUndefined();
-              setTimeout(() => !errored && done(), 5);
+              setTimeout(() => !errored && resolve(), 5);
             });
         } else if (handleCount > 1) {
           errored = true;
@@ -660,8 +642,9 @@ describe('ObservableQuery', () => {
   });
 
   describe('setVariables', () => {
-    it('reruns query if the variables change', done => {
+    itAsync('reruns query if the variables change', (resolve, reject) => {
       const observable: ObservableQuery<any> = mockWatchQuery(
+        reject,
         {
           request: { query, variables },
           result: { data: dataOne },
@@ -672,7 +655,7 @@ describe('ObservableQuery', () => {
         },
       );
 
-      subscribeAndCount(done, observable, (handleCount, result) => {
+      subscribeAndCount(reject, observable, (handleCount, result) => {
         if (handleCount === 1) {
           expect(stripSymbols(result.data)).toEqual(dataOne);
           return observable.setVariables(differentVariables);
@@ -682,13 +665,14 @@ describe('ObservableQuery', () => {
         } else if (handleCount === 3) {
           expect(result.loading).toBe(false);
           expect(stripSymbols(result.data)).toEqual(dataTwo);
-          done();
+          resolve();
         }
       });
     });
 
-    it('does invalidate the currentResult data if the variables change', done => {
+    itAsync('does invalidate the currentResult data if the variables change', (resolve, reject) => {
       const observable: ObservableQuery<any> = mockWatchQuery(
+        reject,
         {
           request: { query, variables },
           result: { data: dataOne },
@@ -700,7 +684,7 @@ describe('ObservableQuery', () => {
         },
       );
 
-      subscribeAndCount(done, observable, async (handleCount, result) => {
+      subscribeAndCount(reject, observable, async (handleCount, result) => {
         if (handleCount === 1) {
           expect(stripSymbols(result.data)).toEqual(dataOne);
           expect(stripSymbols(observable.getCurrentResult().data)).toEqual(
@@ -717,11 +701,12 @@ describe('ObservableQuery', () => {
             dataTwo,
           );
           expect(observable.getCurrentResult().loading).toBe(false);
-          done();
+          resolve();
         }
       });
     });
-    it('does invalidate the currentResult data if the variables change', done => {
+
+    itAsync('does invalidate the currentResult data if the variables change', (resolve, reject) => {
       // Standard data for all these tests
       const query = gql`
         query UsersQuery($page: Int) {
@@ -756,6 +741,7 @@ describe('ObservableQuery', () => {
       };
 
       const observable: ObservableQuery<any> = mockWatchQuery(
+        reject,
         {
           request: { query, variables },
           result: { data: dataOne },
@@ -767,7 +753,7 @@ describe('ObservableQuery', () => {
         },
       );
 
-      subscribeAndCount(done, observable, async (handleCount, result) => {
+      subscribeAndCount(reject, observable, async (handleCount, result) => {
         if (handleCount === 1) {
           expect(stripSymbols(result.data)).toEqual(dataOne);
           expect(stripSymbols(observable.getCurrentResult().data)).toEqual(
@@ -784,13 +770,14 @@ describe('ObservableQuery', () => {
             dataTwo,
           );
           expect(observable.getCurrentResult().loading).toBe(false);
-          done();
+          resolve();
         }
       });
     });
 
-    it('does not invalidate the currentResult errors if the variables change', done => {
+    itAsync('does not invalidate the currentResult errors if the variables change', (resolve, reject) => {
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query, variables },
           result: { errors: [error] },
@@ -807,7 +794,7 @@ describe('ObservableQuery', () => {
         errorPolicy: 'all',
       });
 
-      subscribeAndCount(done, observable, (handleCount, result) => {
+      subscribeAndCount(reject, observable, async (handleCount, result) => {
         if (handleCount === 1) {
           expect(result.errors).toEqual([error]);
           expect(observable.getCurrentResult().errors).toEqual([error]);
@@ -821,20 +808,20 @@ describe('ObservableQuery', () => {
             dataTwo,
           );
           expect(observable.getCurrentResult().loading).toBe(false);
-          done();
+          resolve();
         }
       });
     });
 
-    it('does not perform a query when unsubscribed if variables change', () => {
+    itAsync('does not perform a query when unsubscribed if variables change', (resolve, reject) => {
       // Note: no responses, will throw if a query is made
-      const queryManager = mockQueryManager();
+      const queryManager = mockQueryManager(reject);
       const observable = queryManager.watchQuery({ query, variables });
-
-      return observable.setVariables(differentVariables);
+      return observable.setVariables(differentVariables)
+        .then(resolve, reject);
     });
 
-    it('sets networkStatus to `setVariables` when fetching', done => {
+    itAsync('sets networkStatus to `setVariables` when fetching', (resolve, reject) => {
       const mockedResponses = [
         {
           request: { query, variables },
@@ -846,7 +833,7 @@ describe('ObservableQuery', () => {
         },
       ];
 
-      const queryManager = mockQueryManager(...mockedResponses);
+      const queryManager = mockQueryManager(reject, ...mockedResponses);
       const firstRequest = mockedResponses[0].request;
       const observable = queryManager.watchQuery({
         query: firstRequest.query,
@@ -854,7 +841,7 @@ describe('ObservableQuery', () => {
         notifyOnNetworkStatusChange: true,
       });
 
-      subscribeAndCount(done, observable, (handleCount, result) => {
+      subscribeAndCount(reject, observable, (handleCount, result) => {
         if (handleCount === 1) {
           expect(stripSymbols(result.data)).toEqual(dataOne);
           expect(result.networkStatus).toBe(NetworkStatus.ready);
@@ -867,12 +854,12 @@ describe('ObservableQuery', () => {
           expect(result.loading).toBe(false);
           expect(result.networkStatus).toBe(NetworkStatus.ready);
           expect(stripSymbols(result.data)).toEqual(dataTwo);
-          done();
+          resolve();
         }
       });
     });
 
-    it('sets networkStatus to `setVariables` when calling refetch with new variables', done => {
+    itAsync('sets networkStatus to `setVariables` when calling refetch with new variables', (resolve, reject) => {
       const mockedResponses = [
         {
           request: { query, variables },
@@ -884,7 +871,7 @@ describe('ObservableQuery', () => {
         },
       ];
 
-      const queryManager = mockQueryManager(...mockedResponses);
+      const queryManager = mockQueryManager(reject, ...mockedResponses);
       const firstRequest = mockedResponses[0].request;
       const observable = queryManager.watchQuery({
         query: firstRequest.query,
@@ -892,7 +879,7 @@ describe('ObservableQuery', () => {
         notifyOnNetworkStatusChange: true,
       });
 
-      subscribeAndCount(done, observable, (handleCount, result) => {
+      subscribeAndCount(reject, observable, (handleCount, result) => {
         if (handleCount === 1) {
           expect(stripSymbols(result.data)).toEqual(dataOne);
           expect(result.networkStatus).toBe(NetworkStatus.ready);
@@ -905,13 +892,14 @@ describe('ObservableQuery', () => {
           expect(result.loading).toBe(false);
           expect(result.networkStatus).toBe(NetworkStatus.ready);
           expect(stripSymbols(result.data)).toEqual(dataTwo);
-          done();
+          resolve();
         }
       });
     });
 
-    it('reruns observer callback if the variables change but data does not', done => {
+    itAsync('reruns observer callback if the variables change but data does not', (resolve, reject) => {
       const observable: ObservableQuery<any> = mockWatchQuery(
+        reject,
         {
           request: { query, variables },
           result: { data: dataOne },
@@ -922,7 +910,7 @@ describe('ObservableQuery', () => {
         },
       );
 
-      subscribeAndCount(done, observable, (handleCount, result) => {
+      subscribeAndCount(reject, observable, (handleCount, result) => {
         if (handleCount === 1) {
           expect(stripSymbols(result.data)).toEqual(dataOne);
           observable.setVariables(differentVariables);
@@ -931,13 +919,14 @@ describe('ObservableQuery', () => {
           expect(stripSymbols(result.data)).toEqual(dataOne);
         } else if (handleCount === 3) {
           expect(stripSymbols(result.data)).toEqual(dataOne);
-          done();
+          resolve();
         }
       });
     });
 
-    it('does not rerun observer callback if the variables change but new data is in store', done => {
+    itAsync('does not rerun observer callback if the variables change but new data is in store', (resolve, reject) => {
       const manager = mockQueryManager(
+        reject,
         {
           request: { query, variables },
           result: { data: dataOne },
@@ -956,13 +945,13 @@ describe('ObservableQuery', () => {
         });
 
         let errored = false;
-        subscribeAndCount(done, observable, (handleCount, result) => {
+        subscribeAndCount(reject, observable, (handleCount, result) => {
           if (handleCount === 1) {
             expect(stripSymbols(result.data)).toEqual(dataOne);
             observable.setVariables(differentVariables);
 
             // Nothing should happen, so we'll wait a moment to check that
-            setTimeout(() => !errored && done(), 10);
+            setTimeout(() => !errored && resolve(), 10);
           } else if (handleCount === 2) {
             throw new Error('Observable callback should not fire twice');
           }
@@ -970,8 +959,9 @@ describe('ObservableQuery', () => {
       });
     });
 
-    it('does not rerun query if variables do not change', done => {
+    itAsync('does not rerun query if variables do not change', (resolve, reject) => {
       const observable: ObservableQuery<any> = mockWatchQuery(
+        reject,
         {
           request: { query, variables },
           result: { data: dataOne },
@@ -983,13 +973,13 @@ describe('ObservableQuery', () => {
       );
 
       let errored = false;
-      subscribeAndCount(done, observable, (handleCount, result) => {
+      subscribeAndCount(reject, observable, (handleCount, result) => {
         if (handleCount === 1) {
           expect(stripSymbols(result.data)).toEqual(dataOne);
           observable.setVariables(variables);
 
           // Nothing should happen, so we'll wait a moment to check that
-          setTimeout(() => !errored && done(), 10);
+          setTimeout(() => !errored && resolve(), 10);
         } else if (handleCount === 2) {
           errored = true;
           throw new Error('Observable callback should not fire twice');
@@ -997,8 +987,9 @@ describe('ObservableQuery', () => {
       });
     });
 
-    it('does not rerun query if set to not refetch', done => {
+    itAsync('does not rerun query if set to not refetch', (resolve, reject) => {
       const observable: ObservableQuery<any> = mockWatchQuery(
+        reject,
         {
           request: { query, variables },
           result: { data: dataOne },
@@ -1010,13 +1001,13 @@ describe('ObservableQuery', () => {
       );
 
       let errored = false;
-      subscribeAndCount(done, observable, (handleCount, result) => {
+      subscribeAndCount(reject, observable, (handleCount, result) => {
         if (handleCount === 1) {
           expect(stripSymbols(result.data)).toEqual(dataOne);
           observable.setVariables(variables, true, false);
 
           // Nothing should happen, so we'll wait a moment to check that
-          setTimeout(() => !errored && done(), 10);
+          setTimeout(() => !errored && resolve(), 10);
         } else if (handleCount === 2) {
           errored = true;
           throw new Error('Observable callback should not fire twice');
@@ -1024,11 +1015,12 @@ describe('ObservableQuery', () => {
       });
     });
 
-    it('handles variables changing while a query is in-flight', done => {
+    itAsync('handles variables changing while a query is in-flight', (resolve, reject) => {
       // The expected behavior is that the original variables are forgotten
       // and the query stays in loading state until the result for the new variables
       // has returned.
       const observable: ObservableQuery<any> = mockWatchQuery(
+        reject,
         {
           request: { query, variables },
           result: { data: dataOne },
@@ -1043,19 +1035,19 @@ describe('ObservableQuery', () => {
 
       setTimeout(() => observable.setVariables(differentVariables), 10);
 
-      subscribeAndCount(done, observable, (handleCount, result) => {
+      subscribeAndCount(reject, observable, (handleCount, result) => {
         if (handleCount === 1) {
           expect(result.networkStatus).toBe(NetworkStatus.ready);
           expect(result.loading).toBe(false);
           expect(stripSymbols(result.data)).toEqual(dataTwo);
-          done();
+          resolve();
         }
       });
     });
   });
 
   describe('refetch', () => {
-    it('calls fetchRequest with fetchPolicy `network-only` when using a non-networked fetch policy', done => {
+    itAsync('calls fetchRequest with fetchPolicy `network-only` when using a non-networked fetch policy', (resolve, reject) => {
       const mockedResponses = [
         {
           request: { query, variables },
@@ -1067,7 +1059,7 @@ describe('ObservableQuery', () => {
         },
       ];
 
-      const queryManager = mockQueryManager(...mockedResponses);
+      const queryManager = mockQueryManager(reject, ...mockedResponses);
       const firstRequest = mockedResponses[0].request;
       const observable = queryManager.watchQuery({
         query: firstRequest.query,
@@ -1080,22 +1072,21 @@ describe('ObservableQuery', () => {
         origFetchQuery.apply(queryManager, arguments),
       );
 
-      subscribeAndCount(done, observable, (handleCount, result) => {
+      subscribeAndCount(reject, observable, (handleCount, result) => {
         if (handleCount === 1) {
           observable.refetch(differentVariables);
         } else if (handleCount === 3) {
           expect(queryManager.fetchQuery.mock.calls[1][1].fetchPolicy).toEqual(
             'network-only',
           );
-          done();
+          resolve();
         }
       });
     });
 
-    it(
-      'calls fetchRequest with fetchPolicy `no-cache` when using `no-cache` ' +
-        'fetch policy',
-      done => {
+    itAsync(
+      'calls fetchRequest with fetchPolicy `no-cache` when using `no-cache` fetch policy',
+      (resolve, reject) => {
         const mockedResponses = [
           {
             request: { query, variables },
@@ -1107,7 +1098,7 @@ describe('ObservableQuery', () => {
           },
         ];
 
-        const queryManager = mockQueryManager(...mockedResponses);
+        const queryManager = mockQueryManager(reject, ...mockedResponses);
         const firstRequest = mockedResponses[0].request;
         const observable = queryManager.watchQuery({
           query: firstRequest.query,
@@ -1120,20 +1111,20 @@ describe('ObservableQuery', () => {
           origFetchQuery.apply(queryManager, arguments),
         );
 
-        subscribeAndCount(done, observable, (handleCount, result) => {
+        subscribeAndCount(reject, observable, (handleCount, result) => {
           if (handleCount === 1) {
             observable.refetch(differentVariables);
           } else if (handleCount === 2) {
             expect(
               queryManager.fetchQuery.mock.calls[1][1].fetchPolicy,
             ).toEqual('no-cache');
-            done();
+            resolve();
           }
         });
-      },
+      }
     );
 
-    it('calls ObservableQuery.next even after hitting cache', done => {
+    itAsync('calls ObservableQuery.next even after hitting cache', (resolve, reject) => {
       // This query and variables are copied from react-apollo
       const queryWithVars = gql`
         query people($first: Int) {
@@ -1152,6 +1143,7 @@ describe('ObservableQuery', () => {
       const variables2 = { first: 1 };
 
       const observable: ObservableQuery<any> = mockWatchQuery(
+        reject,
         {
           request: {
             query: queryWithVars,
@@ -1177,7 +1169,7 @@ describe('ObservableQuery', () => {
 
       observable.setOptions({ fetchPolicy: 'cache-and-network' });
 
-      subscribeAndCount(done, observable, (handleCount, result) => {
+      subscribeAndCount(reject, observable, (handleCount, result) => {
         if (handleCount === 1) {
           expect(result.data).toBeUndefined();
           expect(result.loading).toBe(true);
@@ -1198,12 +1190,12 @@ describe('ObservableQuery', () => {
         } else if (handleCount === 6) {
           expect(stripSymbols(result.data)).toEqual(data);
           expect(result.loading).toBe(false);
-          done();
+          resolve();
         }
       });
     });
 
-    it('cache-and-network refetch should run @client(always: true) resolvers when network request fails', done => {
+    itAsync('cache-and-network refetch should run @client(always: true) resolvers when network request fails', (resolve, reject) => {
       const query = gql`
         query MixedQuery {
           counter @client(always: true)
@@ -1320,9 +1312,9 @@ describe('ObservableQuery', () => {
               stale: false,
             });
 
-            done();
+            resolve();
           } else if (handleCount > 5) {
-            done.fail(new Error('should not get here'));
+            reject(new Error('should not get here'));
           }
         },
       });
@@ -1330,7 +1322,7 @@ describe('ObservableQuery', () => {
   });
 
   describe('currentResult', () => {
-    it('returns the same value as observableQuery.next got', done => {
+    itAsync('returns the same value as observableQuery.next got', (resolve, reject) => {
       const queryWithFragment = gql`
         fragment CatInfo on Cat {
           isTabby
@@ -1397,33 +1389,20 @@ describe('ObservableQuery', () => {
         pets: petData.slice(0, 3),
       };
 
-      const ni = mockSingleLink(
-        {
-          request: { query: queryWithFragment, variables },
-          result: { data: dataOneWithTypename },
-        },
-        {
-          request: { query: queryWithFragment, variables },
-          result: { data: dataTwoWithTypename },
-        },
-      );
+      const ni = mockSingleLink({
+        request: { query: queryWithFragment, variables },
+        result: { data: dataOneWithTypename },
+      }, {
+        request: { query: queryWithFragment, variables },
+        result: { data: dataTwoWithTypename },
+      }).setOnError(reject);
 
       const client = new ApolloClient({
         link: ni,
         cache: new InMemoryCache({
-          fragmentMatcher: new IntrospectionFragmentMatcher({
-            introspectionQueryResultData: {
-              __schema: {
-                types: [
-                  {
-                    kind: 'UNION',
-                    name: 'Creature',
-                    possibleTypes: [{ name: 'Pet' }],
-                  },
-                ],
-              },
-            },
-          }),
+          possibleTypes: {
+            Creature: ['Pet'],
+          },
         }),
       });
 
@@ -1433,7 +1412,7 @@ describe('ObservableQuery', () => {
         notifyOnNetworkStatusChange: true,
       });
 
-      subscribeAndCount(done, observable, (count, result) => {
+      subscribeAndCount(reject, observable, (count, result) => {
         const { data, loading, networkStatus } = observable.getCurrentResult();
         try {
           expect(result).toEqual({
@@ -1443,86 +1422,60 @@ describe('ObservableQuery', () => {
             stale: false,
           });
         } catch (e) {
-          done.fail(e);
+          reject(e);
         }
 
         if (count === 1) {
           observable.refetch();
         }
         if (count === 3) {
-          setTimeout(done, 5);
+          setTimeout(resolve, 5);
         }
         if (count > 3) {
-          done.fail(new Error('Observable.next called too many times'));
+          reject(new Error('Observable.next called too many times'));
         }
       });
     });
 
-    it('returns the current query status immediately', done => {
-      const observable: ObservableQuery<any> = mockWatchQuery({
+    itAsync('returns the current query status immediately', (resolve, reject) => {
+      const observable: ObservableQuery<any> = mockWatchQuery(reject, {
         request: { query, variables },
         result: { data: dataOne },
         delay: 100,
       });
 
-      subscribeAndCount(done, observable, () => {
+      subscribeAndCount(reject, observable, () => {
         expect(stripSymbols(observable.getCurrentResult())).toEqual({
           data: dataOne,
           loading: false,
           networkStatus: 7,
-          partial: false,
+          stale: false,
         });
-        done();
+        resolve();
       });
 
       expect(observable.getCurrentResult()).toEqual({
         loading: true,
         data: undefined,
         networkStatus: 1,
-        partial: true,
+        stale: false,
       });
 
       setTimeout(
-        wrap(done, () => {
+        wrap(reject, () => {
           expect(observable.getCurrentResult()).toEqual({
             loading: true,
             data: undefined,
             networkStatus: 1,
-            partial: true,
+            stale: false,
           });
         }),
         0,
       );
     });
 
-    it('returns results from the store immediately', () => {
-      const queryManager = mockQueryManager({
-        request: { query, variables },
-        result: { data: dataOne },
-      });
-
-      return queryManager.query({ query, variables }).then((result: any) => {
-        expect(stripSymbols(result)).toEqual({
-          data: dataOne,
-          loading: false,
-          networkStatus: 7,
-          stale: false,
-        });
-        const observable = queryManager.watchQuery({
-          query,
-          variables,
-        });
-        expect(stripSymbols(observable.getCurrentResult())).toEqual({
-          data: dataOne,
-          loading: false,
-          networkStatus: 7,
-          partial: false,
-        });
-      });
-    });
-
-    it('returns errors from the store immediately', done => {
-      const queryManager = mockQueryManager({
+    itAsync('returns errors from the store immediately', (resolve, reject) => {
+      const queryManager = mockQueryManager(reject, {
         request: { query, variables },
         result: { errors: [error] },
       });
@@ -1539,13 +1492,13 @@ describe('ObservableQuery', () => {
           const currentResult = observable.getCurrentResult();
           expect(currentResult.loading).toBe(false);
           expect(currentResult.error!.graphQLErrors).toEqual([error]);
-          done();
+          resolve();
         },
       });
     });
 
-    it('returns referentially equal errors', () => {
-      const queryManager = mockQueryManager({
+    itAsync('returns referentially equal errors', (resolve, reject) => {
+      const queryManager = mockQueryManager(reject, {
         request: { query, variables },
         result: { errors: [error] },
       });
@@ -1563,11 +1516,11 @@ describe('ObservableQuery', () => {
         expect(currentResult.error!.graphQLErrors).toEqual([error]);
         const currentResult2 = observable.getCurrentResult();
         expect(currentResult.error === currentResult2.error).toBe(true);
-      });
+      }).then(resolve, reject);
     });
 
-    it('returns errors with data if errorPolicy is all', () => {
-      const queryManager = mockQueryManager({
+    itAsync('returns errors with data if errorPolicy is all', (resolve, reject) => {
+      const queryManager = mockQueryManager(reject, {
         request: { query, variables },
         result: { data: dataOne, errors: [error] },
       });
@@ -1585,11 +1538,11 @@ describe('ObservableQuery', () => {
         expect(currentResult.loading).toBe(false);
         expect(currentResult.errors).toEqual([error]);
         expect(currentResult.error).toBeUndefined();
-      });
+      }).then(resolve, reject);
     });
 
-    it('ignores errors with data if errorPolicy is ignore', () => {
-      const queryManager = mockQueryManager({
+    itAsync('ignores errors with data if errorPolicy is ignore', (resolve, reject) => {
+      const queryManager = mockQueryManager(reject, {
         request: { query, variables },
         result: { errors: [error], data: dataOne },
       });
@@ -1607,10 +1560,10 @@ describe('ObservableQuery', () => {
         expect(currentResult.loading).toBe(false);
         expect(currentResult.errors).toBeUndefined();
         expect(currentResult.error).toBeUndefined();
-      });
+      }).then(resolve, reject);
     });
 
-    it('returns partial data from the store immediately', done => {
+    itAsync('returns partial data from the store', (resolve, reject) => {
       const superQuery = gql`
         query superQuery($id: ID!) {
           people_one(id: $id) {
@@ -1628,6 +1581,7 @@ describe('ObservableQuery', () => {
       };
 
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query, variables },
           result: { data: dataOne },
@@ -1645,16 +1599,16 @@ describe('ObservableQuery', () => {
           returnPartialData: true,
         });
 
-        expect(observable.currentResult()).toEqual({
-          data: dataOne,
+        expect(observable.getCurrentResult()).toEqual({
+          data: void 0,
           loading: true,
           networkStatus: 1,
-          partial: true,
+          stale: false,
         });
 
         // we can use this to trigger the query
-        subscribeAndCount(done, observable, (handleCount, subResult) => {
-          const { data, loading, networkStatus } = observable.currentResult();
+        subscribeAndCount(reject, observable, (handleCount, subResult) => {
+          const { data, loading, networkStatus } = observable.getCurrentResult();
           expect(subResult).toEqual({
             data,
             loading,
@@ -1664,27 +1618,37 @@ describe('ObservableQuery', () => {
 
           if (handleCount === 1) {
             expect(subResult).toEqual({
-              data: dataOne,
+              data: void 0,
               loading: true,
               networkStatus: 1,
               stale: false,
             });
 
           } else if (handleCount === 2) {
+            expect(subResult).toEqual({
+              data: dataOne,
+              loading: false,
+              networkStatus: 7,
+              stale: false,
+            });
+
+          } else if (handleCount === 3) {
             expect(subResult).toEqual({
               data: superDataOne,
               loading: false,
               networkStatus: 7,
               stale: false,
             });
-            done();
+
+            resolve();
           }
         });
       });
     });
 
-    it('returns loading even if full data is available when using network-only fetchPolicy', done => {
+    itAsync('returns loading even if full data is available when using network-only fetchPolicy', (resolve, reject) => {
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query, variables },
           result: { data: dataOne },
@@ -1705,15 +1669,16 @@ describe('ObservableQuery', () => {
           data: undefined,
           loading: true,
           networkStatus: 1,
-          partial: false,
+          stale: false,
         });
 
-        subscribeAndCount(done, observable, (handleCount, subResult) => {
+        subscribeAndCount(reject, observable, (handleCount, subResult) => {
           const {
             data,
             loading,
             networkStatus,
           } = observable.getCurrentResult();
+
           expect(subResult).toEqual({
             data,
             loading,
@@ -1721,21 +1686,31 @@ describe('ObservableQuery', () => {
             stale: false,
           });
 
-          if (handleCount === 2) {
+          if (handleCount === 1) {
+            expect(stripSymbols(subResult)).toEqual({
+              data: void 0,
+              loading: true,
+              networkStatus: NetworkStatus.loading,
+              stale: false,
+            });
+
+          } else if (handleCount === 2) {
             expect(stripSymbols(subResult)).toEqual({
               data: dataTwo,
               loading: false,
-              networkStatus: 7,
+              networkStatus: NetworkStatus.ready,
               stale: false,
             });
-            done();
+
+            resolve();
           }
         });
       });
     });
 
-    it('returns loading on no-cache fetchPolicy queries when calling getCurrentResult', done => {
+    itAsync('returns loading on no-cache fetchPolicy queries when calling getCurrentResult', (resolve, reject) => {
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query, variables },
           result: { data: dataOne },
@@ -1752,34 +1727,36 @@ describe('ObservableQuery', () => {
           variables,
           fetchPolicy: 'no-cache',
         });
+
         expect(stripSymbols(observable.getCurrentResult())).toEqual({
           data: undefined,
           loading: true,
           networkStatus: 1,
-          partial: false,
+          stale: false,
         });
 
-        subscribeAndCount(done, observable, (handleCount, subResult) => {
+        subscribeAndCount(reject, observable, (handleCount, subResult) => {
           const {
             data,
             loading,
             networkStatus,
           } = observable.getCurrentResult();
-          expect(subResult).toEqual({
-            data,
-            loading,
-            networkStatus,
-            stale: false,
-          });
 
-          if (handleCount === 2) {
+          if (handleCount === 1) {
+            expect(subResult).toEqual({
+              data,
+              loading,
+              networkStatus,
+              stale: false,
+            });
+          } else if (handleCount === 2) {
             expect(stripSymbols(subResult)).toEqual({
               data: dataTwo,
               loading: false,
               networkStatus: 7,
               stale: false,
             });
-            done();
+            resolve();
           }
         });
       });
@@ -1808,8 +1785,9 @@ describe('ObservableQuery', () => {
         },
       };
 
-      it('returns optimistic mutation results from the store', done => {
+      itAsync('returns optimistic mutation results from the store', (resolve, reject) => {
         const queryManager = mockQueryManager(
+          reject,
           {
             request: { query, variables },
             result: { data: dataOne },
@@ -1825,7 +1803,7 @@ describe('ObservableQuery', () => {
           variables,
         });
 
-        subscribeAndCount(done, observable, (count, result) => {
+        subscribeAndCount(reject, observable, (count, result) => {
           const {
             data,
             loading,
@@ -1856,7 +1834,7 @@ describe('ObservableQuery', () => {
             );
           } else if (count === 3) {
             expect(stripSymbols(result.data.people_one)).toEqual(mutationData);
-            done();
+            resolve();
           }
         });
       });
@@ -1864,7 +1842,7 @@ describe('ObservableQuery', () => {
   });
 
   describe('assumeImmutableResults', () => {
-    it('should prevent costly (but safe) cloneDeep calls', async () => {
+    itAsync('should prevent costly (but safe) cloneDeep calls', async (resolve, reject) => {
       const queryOptions = {
         query: gql`
           query {
@@ -1874,15 +1852,18 @@ describe('ObservableQuery', () => {
         pollInterval: 20,
       };
 
-      function check({ assumeImmutableResults, freezeResults }) {
+      function check({
+        assumeImmutableResults = true,
+        assertFrozenResults = false,
+      }) {
         const client = new ApolloClient({
           link: mockSingleLink(
             { request: queryOptions, result: { data: { value: 1 } } },
             { request: queryOptions, result: { data: { value: 2 } } },
-            { request: queryOptions, result: { data: { value: 3 } } },
-          ),
+            { request: queryOptions, result: { data: { value: 3 } } }
+          ).setOnError(error => { throw error }),
           assumeImmutableResults,
-          cache: new InMemoryCache({ freezeResults }),
+          cache: new InMemoryCache(),
         });
 
         const observable = client.watchQuery(queryOptions);
@@ -1892,10 +1873,20 @@ describe('ObservableQuery', () => {
           observable.subscribe({
             next(result) {
               values.push(result.data.value);
-              try {
-                result.data.value = 'oyez';
-              } catch (error) {
-                reject(error);
+              if (assertFrozenResults) {
+                try {
+                  result.data.value = 'oyez';
+                } catch (error) {
+                  reject(error);
+                }
+              } else {
+                result = {
+                  ...result,
+                  data: {
+                    ...result.data,
+                    value: 'oyez',
+                  },
+                };
               }
               client.writeData(result);
             },
@@ -1907,20 +1898,6 @@ describe('ObservableQuery', () => {
         });
       }
 
-      // When we assume immutable results, the next method will not fire as a
-      // result of destructively modifying result.data.value, because the data
-      // object is still === to the previous object. This behavior might seem
-      // like a bug, if you are relying on the mutability of results, but the
-      // cloneDeep calls required to prevent that bug are expensive. Assuming
-      // immutability is safe only when you write your code in an immutable
-      // style, but the benefits are well worth the extra effort.
-      expect(
-        await check({
-          assumeImmutableResults: true,
-          freezeResults: false,
-        }),
-      ).toEqual([1, 2, 3]);
-
       // When we do not assume immutable results, the observable must do
       // extra work to take snapshots of past results, just in case those
       // results are destructively modified. The benefit of that work is
@@ -1931,7 +1908,7 @@ describe('ObservableQuery', () => {
       expect(
         await check({
           assumeImmutableResults: false,
-          freezeResults: false,
+          assertFrozenResults: false,
         }),
       ).toEqual([1, 'oyez', 2, 'oyez', 3, 'oyez']);
 
@@ -1944,7 +1921,7 @@ describe('ObservableQuery', () => {
             // modifications of the result objects will become fatal. Once you
             // start enforcing immutability in this way, you might as well pass
             // assumeImmutableResults: true, to prevent calling cloneDeep.
-            freezeResults: true,
+            assertFrozenResults: true,
           });
           throw new Error('not reached');
         } catch (error) {
@@ -1956,12 +1933,15 @@ describe('ObservableQuery', () => {
       }
       await checkThrows(true);
       await checkThrows(false);
+
+      resolve();
     });
   });
 
   describe('stopPolling', () => {
-    it('does not restart polling after stopping and resubscribing', done => {
+    itAsync('does not restart polling after stopping and resubscribing', (resolve, reject) => {
       const observable = mockWatchQuery(
+        reject,
         {
           request: { query, variables },
           result: { data: dataOne },
@@ -1976,7 +1956,7 @@ describe('ObservableQuery', () => {
       observable.stopPolling();
 
       let startedPolling = false;
-      subscribeAndCount(done, observable, handleCount => {
+      subscribeAndCount(reject, observable, handleCount => {
         if (handleCount === 1) {
           // first call to subscribe is the immediate result when
           // subscribing. later calls to this callback indicate that
@@ -1988,23 +1968,23 @@ describe('ObservableQuery', () => {
             if (!startedPolling) {
               // if we're not polling for data, it means this test
               // is ok
-              done();
+              resolve();
             }
           }, 60);
         } else if (handleCount === 2) {
           // oops! we are polling for data, this should not happen.
           startedPolling = true;
-          done.fail(new Error('should not start polling, already stopped'));
+          reject(new Error('should not start polling, already stopped'));
         }
       });
     });
   });
 
   describe('resetQueryStoreErrors', () => {
-    it("should remove any GraphQLError's stored in the query store", (done) => {
+    itAsync("should remove any GraphQLError's stored in the query store", (resolve, reject) => {
       const graphQLError = new GraphQLError('oh no!');
 
-      const observable: ObservableQuery<any> = mockWatchQuery({
+      const observable: ObservableQuery<any> = mockWatchQuery(reject, {
         request: { query, variables },
         result: { errors: [graphQLError] },
       });
@@ -2018,15 +1998,15 @@ describe('ObservableQuery', () => {
           observable.resetQueryStoreErrors();
           expect(queryStore.graphQLErrors).toEqual([]);
 
-          done();
+          resolve();
         }
       });
     });
 
-    it("should remove network error's stored in the query store", (done) => {
+    itAsync("should remove network error's stored in the query store", (resolve, reject) => {
       const networkError = new Error('oh no!');
 
-      const observable: ObservableQuery<any> = mockWatchQuery({
+      const observable: ObservableQuery<any> = mockWatchQuery(reject, {
         request: { query, variables },
         result: { data: dataOne },
       });
@@ -2038,7 +2018,7 @@ describe('ObservableQuery', () => {
           queryStore.networkError = networkError;
           observable.resetQueryStoreErrors();
           expect(queryStore.networkError).toBeNull();
-          done();
+          resolve();
         }
       });
     });
diff --git a/packages/apollo-client/src/core/__tests__/QueryManager/index.ts b/src/core/__tests__/QueryManager/index.ts
similarity index 84%
rename from packages/apollo-client/src/core/__tests__/QueryManager/index.ts
rename to src/core/__tests__/QueryManager/index.ts
--- a/packages/apollo-client/src/core/__tests__/QueryManager/index.ts
+++ b/src/core/__tests__/QueryManager/index.ts
@@ -4,20 +4,20 @@ import { map } from 'rxjs/operators';
 import { assign } from 'lodash';
 import gql from 'graphql-tag';
 import { DocumentNode, ExecutionResult, GraphQLError } from 'graphql';
-import { ApolloLink, Operation, Observable } from 'apollo-link';
+
+import { Observable, Observer } from '../../../utilities/observables/Observable';
+import { ApolloLink } from '../../../link/core/ApolloLink';
+import { Operation } from '../../../link/core/types';
+import { InMemoryCache } from '../../../cache/inmemory/inMemoryCache';
 import {
-  InMemoryCache,
   ApolloReducerConfig,
-  NormalizedCacheObject,
-} from 'apollo-cache-inmemory';
+  NormalizedCacheObject
+} from '../../../cache/inmemory/types';
 
 // mocks
-import mockQueryManager from '../../../__mocks__/mockQueryManager';
-import mockWatchQuery from '../../../__mocks__/mockWatchQuery';
-import {
-  mockSingleLink,
-  MockSubscriptionLink,
-} from '../../../__mocks__/mockLinks';
+import mockQueryManager from '../../../utilities/testing/mocking/mockQueryManager';
+import mockWatchQuery from '../../../utilities/testing/mocking/mockWatchQuery';
+import { mockSingleLink } from '../../../utilities/testing/mocking/mockLink';
 
 // core
 import { ApolloQueryResult } from '../../types';
@@ -27,15 +27,15 @@ import { WatchQueryOptions } from '../../watchQueryOptions';
 import { QueryManager } from '../../QueryManager';
 
 import { ApolloError } from '../../../errors/ApolloError';
-import { DataStore } from '../../../data/store';
-import { Observer } from '../../../util/Observable';
 
 // testing utils
-import wrap from '../../../util/wrap';
+import wrap from '../../../utilities/testing/wrap';
 import observableToPromise, {
   observableToPromiseAndSubscription,
-} from '../../../util/observableToPromise';
-import { stripSymbols } from 'apollo-utilities';
+} from '../../../utilities/testing/observableToPromise';
+import subscribeAndCount from '../../../utilities/testing/subscribeAndCount';
+import { stripSymbols } from '../../../utilities/testing/stripSymbols';
+import { itAsync } from '../../../utilities/testing/itAsync';
 
 describe('QueryManager', () => {
   // Standard "get id from object" method.
@@ -54,15 +54,13 @@ describe('QueryManager', () => {
     config = {},
     clientAwareness = {},
   }: {
-    link?: ApolloLink;
+    link: ApolloLink;
     config?: ApolloReducerConfig;
     clientAwareness?: { [key: string]: string };
   }) => {
     return new QueryManager({
-      link: link || mockSingleLink(),
-      store: new DataStore(
-        new InMemoryCache({ addTypename: false, ...config }),
-      ),
+      link,
+      cache: new InMemoryCache({ addTypename: false, ...config }),
       clientAwareness,
     });
   };
@@ -70,7 +68,7 @@ describe('QueryManager', () => {
   // Helper method that sets up a mockQueryManager and then passes on the
   // results to an observer.
   const assertWithObserver = ({
-    done,
+    reject,
     query,
     variables = {},
     queryOptions = {},
@@ -79,7 +77,7 @@ describe('QueryManager', () => {
     delay,
     observer,
   }: {
-    done: () => void;
+    reject: (reason: any) => any;
     query: DocumentNode;
     variables?: Object;
     queryOptions?: Object;
@@ -88,7 +86,7 @@ describe('QueryManager', () => {
     delay?: number;
     observer: Observer<ApolloQueryResult<any>>;
   }) => {
-    const queryManager = mockQueryManager({
+    const queryManager = mockQueryManager(reject, {
       request: { query, variables },
       result,
       error,
@@ -99,18 +97,20 @@ describe('QueryManager', () => {
       queryOptions,
     ) as WatchQueryOptions;
     return queryManager.watchQuery<any>(finalOptions).subscribe({
-      next: wrap(done, observer.next!),
+      next: wrap(reject, observer.next!),
       error: observer.error,
     });
   };
 
   const mockMutation = ({
+    reject,
     mutation,
     data,
     errors,
     variables = {},
     config = {},
   }: {
+    reject: (reason: any) => any;
     mutation: DocumentNode;
     data?: Object;
     errors?: GraphQLError[];
@@ -120,7 +120,7 @@ describe('QueryManager', () => {
     const link = mockSingleLink({
       request: { query: mutation, variables },
       result: { data, errors },
-    });
+    }).setOnError(reject);
     const queryManager = createQueryManager({
       link,
       config,
@@ -141,23 +141,28 @@ describe('QueryManager', () => {
   };
 
   const assertMutationRoundtrip = (opts: {
+    resolve: (result: any) => any;
+    reject: (reason: any) => any;
     mutation: DocumentNode;
     data: Object;
     variables?: Object;
   }) => {
+    const { resolve, reject } = opts;
     return mockMutation(opts).then(({ result }) => {
       expect(stripSymbols(result.data)).toEqual(opts.data);
-    });
+    }).then(resolve, reject);
   };
 
   // Helper method that takes a query with a first response and a second response.
   // Used to assert stuff about refetches.
   const mockRefetch = ({
+    reject,
     request,
     firstResult,
     secondResult,
     thirdResult,
   }: {
+    reject: (reason: any) => any;
     request: Operation;
     firstResult: ExecutionResult;
     secondResult: ExecutionResult;
@@ -178,12 +183,12 @@ describe('QueryManager', () => {
       args.push({ request, result: thirdResult });
     }
 
-    return mockQueryManager(...args);
+    return mockQueryManager(reject, ...args);
   };
 
-  it('handles GraphQL errors', done => {
+  itAsync('handles GraphQL errors', (resolve, reject) => {
     assertWithObserver({
-      done,
+      reject,
       query: gql`
         query people {
           allPeople(first: 1) {
@@ -204,22 +209,22 @@ describe('QueryManager', () => {
       },
       observer: {
         next() {
-          done.fail(
+          reject(
             new Error('Returned a result when it was supposed to error out'),
           );
         },
 
         error(apolloError) {
           expect(apolloError).toBeDefined();
-          done();
+          resolve();
         },
       },
     });
   });
 
-  it('handles GraphQL errors as data', done => {
+  itAsync('handles GraphQL errors as data', (resolve, reject) => {
     assertWithObserver({
-      done,
+      reject,
       query: gql`
         query people {
           allPeople(first: 1) {
@@ -246,7 +251,7 @@ describe('QueryManager', () => {
           expect(errors).toBeDefined();
           expect(errors[0].name).toBe('Name');
           expect(errors[0].message).toBe('This is an error message.');
-          done();
+          resolve();
         },
         error(apolloError) {
           throw new Error(
@@ -257,9 +262,9 @@ describe('QueryManager', () => {
     });
   });
 
-  it('handles GraphQL errors with data returned', done => {
+  itAsync('handles GraphQL errors with data returned', (resolve, reject) => {
     assertWithObserver({
-      done,
+      reject,
       query: gql`
         query people {
           allPeople(first: 1) {
@@ -286,22 +291,22 @@ describe('QueryManager', () => {
       },
       observer: {
         next() {
-          done.fail(
+          reject(
             new Error('Returned data when it was supposed to error out.'),
           );
         },
 
         error(apolloError) {
           expect(apolloError).toBeDefined();
-          done();
+          resolve();
         },
       },
     });
   });
 
-  it('empty error array (handle non-spec-compliant server) #156', done => {
+  itAsync('empty error array (handle non-spec-compliant server) #156', (resolve, reject) => {
     assertWithObserver({
-      done,
+      reject,
       query: gql`
         query people {
           allPeople(first: 1) {
@@ -325,7 +330,7 @@ describe('QueryManager', () => {
         next(result) {
           expect(result.data['allPeople'].people.name).toBe('Ada Lovelace');
           expect(result['errors']).toBeUndefined();
-          done();
+          resolve();
         },
       },
     });
@@ -333,9 +338,9 @@ describe('QueryManager', () => {
 
   // Easy to get into this state if you write an incorrect `formatError`
   // function with graphql-server or express-graphql
-  it('error array with nulls (handle non-spec-compliant server) #1185', done => {
+  itAsync('error array with nulls (handle non-spec-compliant server) #1185', (resolve, reject) => {
     assertWithObserver({
-      done,
+      reject,
       query: gql`
         query people {
           allPeople(first: 1) {
@@ -350,20 +355,20 @@ describe('QueryManager', () => {
       },
       observer: {
         next() {
-          done.fail(new Error('Should not fire next for an error'));
+          reject(new Error('Should not fire next for an error'));
         },
         error(error) {
           expect((error as any).graphQLErrors).toEqual([null]);
           expect(error.message).toBe('GraphQL error: Error message not found.');
-          done();
+          resolve();
         },
       },
     });
   });
 
-  it('handles network errors', done => {
+  itAsync('handles network errors', (resolve, reject) => {
     assertWithObserver({
-      done,
+      reject,
       query: gql`
         query people {
           allPeople(first: 1) {
@@ -376,19 +381,19 @@ describe('QueryManager', () => {
       error: new Error('Network error'),
       observer: {
         next: () => {
-          done.fail(new Error('Should not deliver result'));
+          reject(new Error('Should not deliver result'));
         },
         error: error => {
           const apolloError = error as ApolloError;
           expect(apolloError.networkError).toBeDefined();
           expect(apolloError.networkError!.message).toMatch('Network error');
-          done();
+          resolve();
         },
       },
     });
   });
 
-  it('uses console.error to log unhandled errors', done => {
+  itAsync('uses console.error to log unhandled errors', (resolve, reject) => {
     const oldError = console.error;
     let printed: any;
     console.error = (...args: any[]) => {
@@ -396,7 +401,7 @@ describe('QueryManager', () => {
     };
 
     assertWithObserver({
-      done,
+      reject,
       query: gql`
         query people {
           allPeople(first: 1) {
@@ -409,7 +414,7 @@ describe('QueryManager', () => {
       error: new Error('Network error'),
       observer: {
         next: () => {
-          done.fail(new Error('Should not deliver result'));
+          reject(new Error('Should not deliver result'));
         },
       },
     });
@@ -417,15 +422,15 @@ describe('QueryManager', () => {
     setTimeout(() => {
       expect(printed[0]).toMatch(/error/);
       console.error = oldError;
-      done();
+      resolve();
     }, 10);
   });
 
   // XXX this looks like a bug in zen-observable but we should figure
   // out a solution for it
-  xit('handles an unsubscribe action that happens before data returns', done => {
+  itAsync.skip('handles an unsubscribe action that happens before data returns', (resolve, reject) => {
     const subscription = assertWithObserver({
-      done,
+      reject,
       query: gql`
         query people {
           allPeople(first: 1) {
@@ -438,10 +443,10 @@ describe('QueryManager', () => {
       delay: 1000,
       observer: {
         next: () => {
-          done.fail(new Error('Should not deliver result'));
+          reject(new Error('Should not deliver result'));
         },
         error: () => {
-          done.fail(new Error('Should not deliver result'));
+          reject(new Error('Should not deliver result'));
         },
       },
     });
@@ -449,7 +454,7 @@ describe('QueryManager', () => {
     expect(subscription.unsubscribe).not.toThrow();
   });
 
-  it('supports interoperability with other Observable implementations like RxJS', done => {
+  itAsync('supports interoperability with other Observable implementations like RxJS', (resolve, reject) => {
     const expResult = {
       data: {
         allPeople: {
@@ -462,7 +467,7 @@ describe('QueryManager', () => {
       },
     };
 
-    const handle = mockWatchQuery({
+    const handle = mockWatchQuery(reject, {
       request: {
         query: gql`
           query people {
@@ -480,18 +485,18 @@ describe('QueryManager', () => {
     const observable = from(handle);
 
     observable.pipe(map(result => assign({ fromRx: true }, result))).subscribe({
-      next: wrap(done, newResult => {
+      next: wrap(reject, newResult => {
         const expectedResult = assign(
           { fromRx: true, loading: false, networkStatus: 7, stale: false },
           expResult,
         );
         expect(stripSymbols(newResult)).toEqual(expectedResult);
-        done();
+        resolve();
       }),
     });
   });
 
-  it('allows you to subscribe twice to one query', done => {
+  itAsync('allows you to subscribe twice to one query', (resolve, reject) => {
     const request = {
       query: gql`
         query fetchLuke($id: String) {
@@ -523,6 +528,7 @@ describe('QueryManager', () => {
     };
 
     const queryManager = mockQueryManager(
+      reject,
       {
         request,
         result: { data: data1 },
@@ -574,16 +580,16 @@ describe('QueryManager', () => {
                 subOne.unsubscribe();
                 handle.refetch();
               } catch (e) {
-                done.fail(e);
+                reject(e);
               }
             }, 0);
           } else if (subTwoCount === 3) {
             setTimeout(() => {
               try {
                 expect(subOneCount).toBe(2);
-                done();
+                resolve();
               } catch (e) {
-                done.fail(e);
+                reject(e);
               }
             }, 0);
           }
@@ -591,7 +597,8 @@ describe('QueryManager', () => {
       });
     });
   });
-  it('resolves all queries when one finishes after another', done => {
+
+  itAsync('resolves all queries when one finishes after another', (resolve, reject) => {
     const request = {
       query: gql`
         query fetchLuke($id: String) {
@@ -649,6 +656,7 @@ describe('QueryManager', () => {
     };
 
     const queryManager = mockQueryManager(
+      reject,
       {
         request,
         result: { data: data1 },
@@ -679,7 +687,7 @@ describe('QueryManager', () => {
     ob2.subscribe(result => {
       expect(stripSymbols(result.data)).toEqual(data2);
       expect(finishCount).toBe(2);
-      done();
+      resolve();
     });
     ob3.subscribe(result => {
       expect(stripSymbols(result.data)).toEqual(data3);
@@ -687,7 +695,7 @@ describe('QueryManager', () => {
     });
   });
 
-  it('allows you to refetch queries', () => {
+  itAsync('allows you to refetch queries', (resolve, reject) => {
     const request = {
       query: gql`
         query fetchLuke($id: String) {
@@ -714,6 +722,7 @@ describe('QueryManager', () => {
     };
 
     const queryManager = mockRefetch({
+      reject,
       request,
       firstResult: { data: data1 },
       secondResult: { data: data2 },
@@ -727,10 +736,10 @@ describe('QueryManager', () => {
         observable.refetch();
       },
       result => expect(stripSymbols(result.data)).toEqual(data2),
-    );
+    ).then(resolve, reject);
   });
 
-  it('will return referentially equivalent data if nothing changed in a refetch', done => {
+  itAsync('will return referentially equivalent data if nothing changed in a refetch', (resolve, reject) => {
     const request = {
       query: gql`
         {
@@ -768,6 +777,7 @@ describe('QueryManager', () => {
     };
 
     const queryManager = mockRefetch({
+      reject,
       request,
       firstResult: { data: data1 },
       secondResult: { data: data2 },
@@ -802,20 +812,20 @@ describe('QueryManager', () => {
               expect(result.data.b).toEqual(firstResultData.b);
               expect(result.data.d).not.toBe(firstResultData.d);
               expect(result.data.d.f).toEqual(firstResultData.d.f);
-              done();
+              resolve();
               break;
             default:
               throw new Error('Next run too many times.');
           }
         } catch (error) {
-          done.fail(error);
+          reject(error);
         }
       },
-      error: error => done.fail(error),
+      error: reject,
     });
   });
 
-  it('will return referentially equivalent data in getCurrentResult if nothing changed', done => {
+  itAsync('will return referentially equivalent data in getCurrentResult if nothing changed', (resolve, reject) => {
     const request = {
       query: gql`
         {
@@ -840,7 +850,7 @@ describe('QueryManager', () => {
       d: { e: 3, f: { g: 4 } },
     };
 
-    const queryManager = mockQueryManager({
+    const queryManager = mockQueryManager(reject, {
       request,
       result: { data: data1 },
     });
@@ -854,16 +864,16 @@ describe('QueryManager', () => {
           expect(stripSymbols(result.data)).toEqual(
             stripSymbols(observable.getCurrentResult().data),
           );
-          done();
+          resolve();
         } catch (error) {
-          done.fail(error);
+          reject(error);
         }
       },
-      error: error => done.fail(error),
+      error: reject,
     });
   });
 
-  it('sets networkStatus to `refetch` when refetching', () => {
+  itAsync('sets networkStatus to `refetch` when refetching', (resolve, reject) => {
     const request = {
       query: gql`
         query fetchLuke($id: String) {
@@ -890,6 +900,7 @@ describe('QueryManager', () => {
     };
 
     const queryManager = mockRefetch({
+      reject,
       request,
       firstResult: { data: data1 },
       secondResult: { data: data2 },
@@ -907,10 +918,10 @@ describe('QueryManager', () => {
         expect(result.networkStatus).toBe(NetworkStatus.ready);
         expect(stripSymbols(result.data)).toEqual(data2);
       },
-    );
+    ).then(resolve, reject);
   });
 
-  it('allows you to refetch queries with promises', () => {
+  itAsync('allows you to refetch queries with promises', async (resolve, reject) => {
     const request = {
       query: gql`
         {
@@ -933,6 +944,7 @@ describe('QueryManager', () => {
     };
 
     const queryManager = mockRefetch({
+      reject,
       request,
       firstResult: { data: data1 },
       secondResult: { data: data2 },
@@ -943,10 +955,11 @@ describe('QueryManager', () => {
 
     return handle
       .refetch()
-      .then(result => expect(stripSymbols(result.data)).toEqual(data2));
+      .then(result => expect(stripSymbols(result.data)).toEqual(data2))
+      .then(resolve, reject);
   });
 
-  it('allows you to refetch queries with new variables', () => {
+  itAsync('allows you to refetch queries with new variables', (resolve, reject) => {
     const query = gql`
       {
         people_one(id: 1) {
@@ -988,6 +1001,7 @@ describe('QueryManager', () => {
     };
 
     const queryManager = mockQueryManager(
+      reject,
       {
         request: { query: query },
         result: { data: data1 },
@@ -1035,10 +1049,10 @@ describe('QueryManager', () => {
       result => {
         expect(stripSymbols(result.data)).toEqual(data4);
       },
-    );
+    ).then(resolve, reject);
   });
 
-  it('only modifies varaibles when refetching', () => {
+  itAsync('only modifies varaibles when refetching', (resolve, reject) => {
     const query = gql`
       {
         people_one(id: 1) {
@@ -1060,6 +1074,7 @@ describe('QueryManager', () => {
     };
 
     const queryManager = mockQueryManager(
+      reject,
       {
         request: { query: query },
         result: { data: data1 },
@@ -1088,10 +1103,10 @@ describe('QueryManager', () => {
         delete updatedOptions.variables;
         expect(updatedOptions).toEqual(originalOptions);
       },
-    );
+    ).then(resolve, reject);
   });
 
-  it('continues to poll after refetch', () => {
+  itAsync('continues to poll after refetch', (resolve, reject) => {
     const query = gql`
       {
         people_one(id: 1) {
@@ -1119,6 +1134,7 @@ describe('QueryManager', () => {
     };
 
     const queryManager = mockQueryManager(
+      reject,
       {
         request: { query },
         result: { data: data1 },
@@ -1150,10 +1166,10 @@ describe('QueryManager', () => {
         expect(stripSymbols(result.data)).toEqual(data3);
         observable.stopPolling();
       },
-    );
+    ).then(resolve, reject);
   });
 
-  it('sets networkStatus to `poll` if a polling query is in flight', done => {
+  itAsync('sets networkStatus to `poll` if a polling query is in flight', (resolve, reject) => {
     const query = gql`
       {
         people_one(id: 1) {
@@ -1181,6 +1197,7 @@ describe('QueryManager', () => {
     };
 
     const queryManager = mockQueryManager(
+      reject,
       {
         request: { query },
         result: { data: data1 },
@@ -1211,13 +1228,13 @@ describe('QueryManager', () => {
         } else if (counter === 2) {
           expect(result.networkStatus).toBe(NetworkStatus.poll);
           handle.unsubscribe();
-          done();
+          resolve();
         }
       },
     });
   });
 
-  it('supports returnPartialData #193', () => {
+  itAsync('supports returnPartialData #193', (resolve, reject) => {
     const primeQuery = gql`
       query primeQuery {
         people_one(id: 1) {
@@ -1258,6 +1275,7 @@ describe('QueryManager', () => {
     };
 
     const queryManager = mockQueryManager(
+      error => { throw error },
       {
         request: { query: primeQuery },
         result: { data: data1 },
@@ -1284,10 +1302,11 @@ describe('QueryManager', () => {
           expect(result.data['luke'].name).toBe('Luke Skywalker');
           expect(result.data).not.toHaveProperty('vader');
         });
-      });
+      })
+      .then(resolve, reject);
   });
 
-  it('can handle null values in arrays (#1551)', done => {
+  itAsync('can handle null values in arrays (#1551)', (resolve, reject) => {
     const query = gql`
       {
         list {
@@ -1296,7 +1315,7 @@ describe('QueryManager', () => {
       }
     `;
     const data = { list: [null, { value: 1 }] };
-    const queryManager = mockQueryManager({
+    const queryManager = mockQueryManager(reject, {
       request: { query },
       result: { data },
     });
@@ -1306,18 +1325,19 @@ describe('QueryManager', () => {
       next: result => {
         expect(stripSymbols(result.data)).toEqual(data);
         expect(stripSymbols(observable.getCurrentResult().data)).toEqual(data);
-        done();
+        resolve();
       },
     });
   });
 
-  it('should error if we pass fetchPolicy = cache-first or cache-only on a polling query', () => {
+  itAsync('should error if we pass fetchPolicy = cache-only on a polling query', (resolve, reject) => {
     assertWithObserver({
-      done: () => {},
+      reject,
       observer: {
         next() {},
         error(error) {
           expect(error).toBeInstanceOf(Error);
+          resolve();
         },
       },
       query: gql`
@@ -1330,14 +1350,18 @@ describe('QueryManager', () => {
       `,
       queryOptions: { pollInterval: 200, fetchPolicy: 'cache-only' },
     });
+  });
+
+  itAsync('should error if we pass fetchPolicy = cache-first on a polling query', (resolve, reject) => {
     assertWithObserver({
-      done: () => {},
+      reject,
       observer: {
         next() {
-          // done.fail(new Error('Returned a result when it should not have.'));
+          // reject(new Error('Returned a result when it should not have.'));
         },
         error(error) {
           expect(error).toBeInstanceOf(Error);
+          resolve();
         },
       },
       query: gql`
@@ -1352,7 +1376,7 @@ describe('QueryManager', () => {
     });
   });
 
-  it('supports cache-only fetchPolicy fetching only cached data', () => {
+  itAsync('supports cache-only fetchPolicy fetching only cached data', (resolve, reject) => {
     const primeQuery = gql`
       query primeQuery {
         luke: people_one(id: 1) {
@@ -1378,7 +1402,7 @@ describe('QueryManager', () => {
       },
     };
 
-    const queryManager = mockQueryManager({
+    const queryManager = mockQueryManager(reject, {
       request: { query: primeQuery },
       result: { data: data1 },
     });
@@ -1398,11 +1422,14 @@ describe('QueryManager', () => {
           expect(result.data['luke'].name).toBe('Luke Skywalker');
           expect(result.data).not.toHaveProperty('vader');
         });
-      });
+      })
+      .then(resolve, reject);
   });
 
-  it('runs a mutation', () => {
+  itAsync('runs a mutation', (resolve, reject) => {
     return assertMutationRoundtrip({
+      resolve,
+      reject,
       mutation: gql`
         mutation makeListPrivate {
           makeListPrivate(id: "5")
@@ -1412,9 +1439,11 @@ describe('QueryManager', () => {
     });
   });
 
-  it('runs a mutation even when errors is empty array #2912', () => {
+  itAsync('runs a mutation even when errors is empty array #2912', (resolve, reject) => {
     const errors = [];
     return assertMutationRoundtrip({
+      resolve,
+      reject,
       mutation: gql`
         mutation makeListPrivate {
           makeListPrivate(id: "5")
@@ -1425,10 +1454,11 @@ describe('QueryManager', () => {
     });
   });
 
-  it('runs a mutation with default errorPolicy equal to "none"', () => {
+  itAsync('runs a mutation with default errorPolicy equal to "none"', (resolve, reject) => {
     const errors = [new GraphQLError('foo')];
 
     return mockMutation({
+      reject,
       mutation: gql`
         mutation makeListPrivate {
           makeListPrivate(id: "5")
@@ -1444,11 +1474,13 @@ describe('QueryManager', () => {
       error => {
         expect(error.graphQLErrors).toEqual(errors);
       },
-    );
+    ).then(resolve, reject);
   });
 
-  it('runs a mutation with variables', () => {
+  itAsync('runs a mutation with variables', (resolve, reject) => {
     return assertMutationRoundtrip({
+      resolve,
+      reject,
       mutation: gql`
         mutation makeListPrivate($listId: ID!) {
           makeListPrivate(id: $listId)
@@ -1461,7 +1493,7 @@ describe('QueryManager', () => {
 
   const getIdField = ({ id }: { id: string }) => id;
 
-  it('runs a mutation with object parameters and puts the result in the store', () => {
+  itAsync('runs a mutation with object parameters and puts the result in the store', (resolve, reject) => {
     const data = {
       makeListPrivate: {
         id: '5',
@@ -1469,6 +1501,7 @@ describe('QueryManager', () => {
       },
     };
     return mockMutation({
+      reject,
       mutation: gql`
         mutation makeListPrivate {
           makeListPrivate(input: { id: "5" }) {
@@ -1484,15 +1517,15 @@ describe('QueryManager', () => {
 
       // Make sure we updated the store with the new data
       expect(
-        (queryManager.dataStore.getCache() as InMemoryCache).extract()['5'],
+        queryManager.cache.extract()['5'],
       ).toEqual({
         id: '5',
         isPrivate: true,
       });
-    });
+    }).then(resolve, reject);
   });
 
-  it('runs a mutation and puts the result in the store', () => {
+  itAsync('runs a mutation and puts the result in the store', (resolve, reject) => {
     const data = {
       makeListPrivate: {
         id: '5',
@@ -1501,6 +1534,7 @@ describe('QueryManager', () => {
     };
 
     return mockMutation({
+      reject,
       mutation: gql`
         mutation makeListPrivate {
           makeListPrivate(id: "5") {
@@ -1516,15 +1550,15 @@ describe('QueryManager', () => {
 
       // Make sure we updated the store with the new data
       expect(
-        (queryManager.dataStore.getCache() as InMemoryCache).extract()['5'],
+        queryManager.cache.extract()['5'],
       ).toEqual({
         id: '5',
         isPrivate: true,
       });
-    });
+    }).then(resolve, reject);
   });
 
-  it('runs a mutation and puts the result in the store with root key', () => {
+  itAsync('runs a mutation and puts the result in the store with root key', (resolve, reject) => {
     const mutation = gql`
       mutation makeListPrivate {
         makeListPrivate(id: "5") {
@@ -1545,7 +1579,7 @@ describe('QueryManager', () => {
       link: mockSingleLink({
         request: { query: mutation },
         result: { data },
-      }),
+      }).setOnError(reject),
       config: { dataIdFromObject: getIdField },
     });
 
@@ -1558,15 +1592,15 @@ describe('QueryManager', () => {
 
         // Make sure we updated the store with the new data
         expect(
-          (queryManager.dataStore.getCache() as InMemoryCache).extract()['5'],
+          queryManager.cache.extract()['5'],
         ).toEqual({
           id: '5',
           isPrivate: true,
         });
-      });
+      }).then(resolve, reject);
   });
 
-  it(`doesn't return data while query is loading`, () => {
+  itAsync(`doesn't return data while query is loading`, (resolve, reject) => {
     const query1 = gql`
       {
         people_one(id: 1) {
@@ -1596,6 +1630,7 @@ describe('QueryManager', () => {
     };
 
     const queryManager = mockQueryManager(
+      reject,
       {
         request: { query: query1 },
         result: { data: data1 },
@@ -1617,10 +1652,10 @@ describe('QueryManager', () => {
       observableToPromise({ observable: observable2 }, result =>
         expect(stripSymbols(result.data)).toEqual(data2),
       ),
-    ]);
+    ]).then(resolve, reject);
   });
 
-  it(`updates result of previous query if the result of a new query overlaps`, () => {
+  itAsync('updates result of previous query if the result of a new query overlaps', (resolve, reject) => {
     const query1 = gql`
       {
         people_one(id: 1) {
@@ -1654,6 +1689,7 @@ describe('QueryManager', () => {
     };
 
     const queryManager = mockQueryManager(
+      reject,
       {
         request: { query: query1 },
         result: { data: data1 },
@@ -1666,25 +1702,25 @@ describe('QueryManager', () => {
     );
 
     const observable = queryManager.watchQuery<any>({ query: query1 });
-    return observableToPromise(
-      { observable },
-      result => {
-        expect(stripSymbols(result.data)).toEqual(data1);
+
+    subscribeAndCount(reject, observable, (handleCount, result) => {
+      if (handleCount === 1) {
+        expect(result.data).toEqual(data1);
         queryManager.query<any>({ query: query2 });
-      },
-      // 3 because the query init action for the second query causes a callback
-      result =>
-        expect(stripSymbols(result.data)).toEqual({
+      } else if (handleCount === 2) {
+        expect(result.data).toEqual({
           people_one: {
-            name: 'Luke Skywalker has a new name',
+            name: 'Luke Skywalker',
             age: 50,
           },
-        }),
-    );
+        });
+        resolve();
+      }
+    });
   });
 
-  it('warns if you forget the template literal tag', async () => {
-    const queryManager = mockQueryManager();
+  itAsync('warns if you forget the template literal tag', async (resolve, reject) => {
+    const queryManager = mockQueryManager(reject);
     expect(() => {
       queryManager.query<any>({
         // Bamboozle TypeScript into letting us do this
@@ -1705,9 +1741,11 @@ describe('QueryManager', () => {
         query: ('string' as any) as DocumentNode,
       });
     }).toThrowError(/wrap the query string in a "gql" tag/);
+
+    resolve();
   });
 
-  it('should transform queries correctly when given a QueryTransformer', done => {
+  itAsync('should transform queries correctly when given a QueryTransformer', (resolve, reject) => {
     const query = gql`
       query {
         author {
@@ -1740,17 +1778,17 @@ describe('QueryManager', () => {
       link: mockSingleLink({
         request: { query: transformedQuery },
         result: { data: transformedQueryResult },
-      }),
+      }).setOnError(reject),
       config: { addTypename: true },
     })
       .query({ query: query })
       .then(result => {
         expect(stripSymbols(result.data)).toEqual(transformedQueryResult);
-        done();
-      });
+      })
+      .then(resolve, reject);
   });
 
-  it('should transform mutations correctly', done => {
+  itAsync('should transform mutations correctly', (resolve, reject) => {
     const mutation = gql`
       mutation {
         createAuthor(firstName: "John", lastName: "Smith") {
@@ -1781,17 +1819,17 @@ describe('QueryManager', () => {
       link: mockSingleLink({
         request: { query: transformedMutation },
         result: { data: transformedMutationResult },
-      }),
+      }).setOnError(reject),
       config: { addTypename: true },
     })
       .mutate({ mutation: mutation })
       .then(result => {
         expect(stripSymbols(result.data)).toEqual(transformedMutationResult);
-        done();
+        resolve();
       });
   });
 
-  it('should reject a query promise given a network error', done => {
+  itAsync('should reject a query promise given a network error', (resolve, reject) => {
     const query = gql`
       query {
         author {
@@ -1801,13 +1839,13 @@ describe('QueryManager', () => {
       }
     `;
     const networkError = new Error('Network error');
-    mockQueryManager({
+    mockQueryManager(reject, {
       request: { query },
       error: networkError,
     })
       .query({ query })
       .then(() => {
-        done.fail(new Error('Returned result on an errored fetchQuery'));
+        reject(new Error('Returned result on an errored fetchQuery'));
       })
       .catch(error => {
         const apolloError = error as ApolloError;
@@ -1815,50 +1853,12 @@ describe('QueryManager', () => {
         expect(apolloError.message).toBeDefined();
         expect(apolloError.networkError).toBe(networkError);
         expect(apolloError.graphQLErrors).toEqual([]);
-        done();
-      })
-      .catch(done.fail);
-  });
-
-  it('should error when we attempt to give an id beginning with $', done => {
-    const query = gql`
-      query {
-        author {
-          firstName
-          lastName
-          id
-          __typename
-        }
-      }
-    `;
-    const data = {
-      author: {
-        firstName: 'John',
-        lastName: 'Smith',
-        id: '129',
-        __typename: 'Author',
-      },
-    };
-    const reducerConfig = {
-      dataIdFromObject: (x: any) => '$' + dataIdFromObject(x),
-    };
-    createQueryManager({
-      link: mockSingleLink({
-        request: { query },
-        result: { data },
-      }),
-      config: reducerConfig,
-    })
-      .query({ query })
-      .then(() => {
-        done.fail(new Error('Returned a result when it should not have.'));
+        resolve();
       })
-      .catch(() => {
-        done();
-      });
+      .then(resolve, reject);
   });
 
-  it('should reject a query promise given a GraphQL error', () => {
+  itAsync('should reject a query promise given a GraphQL error', (resolve, reject) => {
     const query = gql`
       query {
         author {
@@ -1868,7 +1868,7 @@ describe('QueryManager', () => {
       }
     `;
     const graphQLErrors = [new Error('GraphQL error')];
-    return mockQueryManager({
+    return mockQueryManager(reject, {
       request: { query },
       result: { errors: graphQLErrors },
     })
@@ -1880,13 +1880,13 @@ describe('QueryManager', () => {
         // don't use .catch() for this or it will catch the above error
         error => {
           const apolloError = error as ApolloError;
-          expect(apolloError.graphQLErrors).toBe(graphQLErrors);
+          expect(apolloError.graphQLErrors).toEqual(graphQLErrors);
           expect(!apolloError.networkError).toBeTruthy();
         },
-      );
+      ).then(resolve, reject);
   });
 
-  it('should not empty the store when a non-polling query fails due to a network error', done => {
+  itAsync('should not empty the store when a non-polling query fails due to a network error', (resolve, reject) => {
     const query = gql`
       query {
         author {
@@ -1902,6 +1902,7 @@ describe('QueryManager', () => {
       },
     };
     const queryManager = mockQueryManager(
+      reject,
       {
         request: { query },
         result: { data },
@@ -1919,26 +1920,24 @@ describe('QueryManager', () => {
         queryManager
           .query<any>({ query, fetchPolicy: 'network-only' })
           .then(() => {
-            done.fail(
+            reject(
               new Error('Returned a result when it was not supposed to.'),
             );
           })
           .catch(() => {
             // make that the error thrown doesn't empty the state
             expect(
-              (queryManager.dataStore.getCache() as InMemoryCache).extract()[
-                '$ROOT_QUERY.author'
-              ] as Object,
-            ).toEqual(data['author']);
-            done();
+              queryManager.cache.extract().ROOT_QUERY.author,
+            ).toEqual(data.author);
+            resolve();
           });
       })
       .catch(() => {
-        done.fail(new Error('Threw an error on the first query.'));
+        reject(new Error('Threw an error on the first query.'));
       });
   });
 
-  it('should be able to unsubscribe from a polling query subscription', () => {
+  itAsync('should be able to unsubscribe from a polling query subscription', (resolve, reject) => {
     const query = gql`
       query {
         author {
@@ -1954,7 +1953,7 @@ describe('QueryManager', () => {
       },
     };
 
-    const observable = mockQueryManager({
+    const observable = mockQueryManager(reject, {
       request: { query },
       result: { data },
     }).watchQuery({ query, pollInterval: 20 });
@@ -1969,10 +1968,11 @@ describe('QueryManager', () => {
         subscription.unsubscribe();
       },
     );
-    return promise;
+
+    return promise.then(resolve, reject);
   });
 
-  it('should not empty the store when a polling query fails due to a network error', () => {
+  itAsync('should not empty the store when a polling query fails due to a network error', (resolve, reject) => {
     const query = gql`
       query {
         author {
@@ -1988,6 +1988,7 @@ describe('QueryManager', () => {
       },
     };
     const queryManager = mockQueryManager(
+      reject,
       {
         request: { query },
         result: { data },
@@ -2009,23 +2010,21 @@ describe('QueryManager', () => {
         errorCallbacks: [
           () => {
             expect(
-              (queryManager.dataStore.getCache() as InMemoryCache).extract()[
-                '$ROOT_QUERY.author'
-              ] as Object,
+              queryManager.cache.extract().ROOT_QUERY.author,
             ).toEqual(data.author);
           },
         ],
       },
       result => {
         expect(stripSymbols(result.data)).toEqual(data);
-        expect((queryManager.dataStore.getCache() as InMemoryCache).extract()[
-          '$ROOT_QUERY.author'
-        ] as Object).toEqual(data.author);
+        expect(
+          queryManager.cache.extract().ROOT_QUERY.author
+        ).toEqual(data.author);
       },
-    );
+    ).then(resolve, reject);
   });
 
-  it('should not fire next on an observer if there is no change in the result', () => {
+  itAsync('should not fire next on an observer if there is no change in the result', (resolve, reject) => {
     const query = gql`
       query {
         author {
@@ -2042,6 +2041,7 @@ describe('QueryManager', () => {
       },
     };
     const queryManager = mockQueryManager(
+      reject,
       {
         request: { query },
         result: { data },
@@ -2062,10 +2062,10 @@ describe('QueryManager', () => {
       queryManager.query<any>({ query }).then(result => {
         expect(stripSymbols(result.data)).toEqual(data);
       }),
-    ]);
+    ]).then(resolve, reject);
   });
 
-  it('should store metadata with watched queries', () => {
+  itAsync('should store metadata with watched queries', (resolve, reject) => {
     const query = gql`
       query {
         author {
@@ -2081,7 +2081,8 @@ describe('QueryManager', () => {
         lastName: 'Smith',
       },
     };
-    const queryManager = mockQueryManager({
+
+    const queryManager = mockQueryManager(reject, {
       request: { query },
       result: { data },
     });
@@ -2090,15 +2091,16 @@ describe('QueryManager', () => {
       query,
       metadata: { foo: 'bar' },
     });
+
     return observableToPromise({ observable }, result => {
       expect(stripSymbols(result.data)).toEqual(data);
       expect(queryManager.queryStore.get(observable.queryId).metadata).toEqual({
         foo: 'bar',
       });
-    });
+    }).then(resolve, reject);
   });
 
-  it('should return stale data when we orphan a real-id node in the store with a real-id node', () => {
+  itAsync('should return stale data when we orphan a real-id node in the store with a real-id node', (resolve, reject) => {
     const query1 = gql`
       query {
         author {
@@ -2145,16 +2147,13 @@ describe('QueryManager', () => {
     };
     const reducerConfig = { dataIdFromObject };
     const queryManager = createQueryManager({
-      link: mockSingleLink(
-        {
-          request: { query: query1 },
-          result: { data: data1 },
-        },
-        {
-          request: { query: query2 },
-          result: { data: data2 },
-        },
-      ),
+      link: mockSingleLink({
+        request: { query: query1 },
+        result: { data: data1 },
+      }, {
+        request: { query: query2 },
+        result: { data: data2 },
+      }).setOnError(reject),
       config: reducerConfig,
     });
 
@@ -2199,10 +2198,10 @@ describe('QueryManager', () => {
           });
         },
       ),
-    ]);
+    ]).then(resolve, reject);
   });
 
-  it('should return partial data when configured when we orphan a real-id node in the store with a real-id node', () => {
+  itAsync('should return partial data when configured when we orphan a real-id node in the store with a real-id node', (resolve, reject) => {
     const query1 = gql`
       query {
         author {
@@ -2249,16 +2248,13 @@ describe('QueryManager', () => {
     };
 
     const queryManager = createQueryManager({
-      link: mockSingleLink(
-        {
-          request: { query: query1 },
-          result: { data: data1 },
-        },
-        {
-          request: { query: query2 },
-          result: { data: data2 },
-        },
-      ),
+      link: mockSingleLink({
+        request: { query: query1 },
+        result: { data: data1 },
+      }, {
+        request: { query: query2 },
+        result: { data: data2 },
+      }).setOnError(reject),
     });
 
     const observable1 = queryManager.watchQuery<any>({
@@ -2313,10 +2309,10 @@ describe('QueryManager', () => {
           });
         },
       ),
-    ]);
+    ]).then(resolve, reject);
   });
 
-  it('should error if we replace a real id node in the store with a generated id node', () => {
+  itAsync('should error if we replace a real id node in the store with a generated id node', (resolve, reject) => {
     const queryWithId = gql`
       query {
         author {
@@ -2349,16 +2345,13 @@ describe('QueryManager', () => {
     };
     const reducerConfig = { dataIdFromObject };
     const queryManager = createQueryManager({
-      link: mockSingleLink(
-        {
-          request: { query: queryWithId },
-          result: { data: dataWithId },
-        },
-        {
-          request: { query: queryWithoutId },
-          result: { data: dataWithoutId },
-        },
-      ),
+      link: mockSingleLink({
+        request: { query: queryWithId },
+        result: { data: dataWithId },
+      }, {
+        request: { query: queryWithoutId },
+        result: { data: dataWithoutId },
+      }).setOnError(reject),
       config: reducerConfig,
     });
 
@@ -2379,10 +2372,10 @@ describe('QueryManager', () => {
         errorCallbacks: [error => expect(error.message).toMatch('Store error')],
         wait: 60,
       }),
-    ]);
+    ]).then(resolve, reject);
   });
 
-  it('should not error when merging a generated id store node  with a real id node', () => {
+  itAsync('should not error when replacing unidentified data with a normalized ID', (resolve, reject) => {
     const queryWithoutId = gql`
       query {
         author {
@@ -2395,6 +2388,7 @@ describe('QueryManager', () => {
         }
       }
     `;
+
     const queryWithId = gql`
       query {
         author {
@@ -2406,6 +2400,7 @@ describe('QueryManager', () => {
         }
       }
     `;
+
     const dataWithoutId = {
       author: {
         name: {
@@ -2416,6 +2411,7 @@ describe('QueryManager', () => {
         __typename: 'Author',
       },
     };
+
     const dataWithId = {
       author: {
         name: {
@@ -2425,51 +2421,39 @@ describe('QueryManager', () => {
         __typename: 'Author',
       },
     };
-    const mergedDataWithoutId = {
-      author: {
-        name: {
-          firstName: 'Jane',
-          lastName: 'Smith',
-        },
-        age: '124',
-        __typename: 'Author',
-      },
-    };
+
     const queryManager = createQueryManager({
-      link: mockSingleLink(
-        {
-          request: { query: queryWithoutId },
-          result: { data: dataWithoutId },
-        },
-        {
-          request: { query: queryWithId },
-          result: { data: dataWithId },
-        },
-      ),
+      link: mockSingleLink({
+        request: { query: queryWithoutId },
+        result: { data: dataWithoutId },
+      }, {
+        request: { query: queryWithId },
+        result: { data: dataWithId },
+      }).setOnError(reject),
     });
 
     const observableWithId = queryManager.watchQuery<any>({
       query: queryWithId,
     });
+
     const observableWithoutId = queryManager.watchQuery<any>({
       query: queryWithoutId,
     });
 
-    // I'm not sure the waiting 60 here really is required, but the test used to do it
     return Promise.all([
       observableToPromise(
-        { observable: observableWithoutId, wait: 120 },
+        { observable: observableWithoutId },
+        result => expect(stripSymbols(result.data)).toEqual(dataWithoutId),
         result => expect(stripSymbols(result.data)).toEqual(dataWithoutId),
-        result =>
-          expect(stripSymbols(result.data)).toEqual(mergedDataWithoutId),
       ),
-      observableToPromise({ observable: observableWithId, wait: 120 }, result =>
-        expect(stripSymbols(result.data)).toEqual(dataWithId),
+      observableToPromise(
+        { observable: observableWithId },
+        result => expect(stripSymbols(result.data)).toEqual(dataWithId),
       ),
-    ]);
+    ]).then(resolve, reject);
   });
 
-  it('exposes errors on a refetch as a rejection', done => {
+  itAsync('exposes errors on a refetch as a rejection', async (resolve, reject) => {
     const request = {
       query: gql`
         {
@@ -2495,7 +2479,12 @@ describe('QueryManager', () => {
       ],
     };
 
-    const queryManager = mockRefetch({ request, firstResult, secondResult });
+    const queryManager = mockRefetch({
+      reject,
+      request,
+      firstResult,
+      secondResult,
+    });
 
     const handle = queryManager.watchQuery<any>(request);
 
@@ -2515,17 +2504,15 @@ describe('QueryManager', () => {
     handle
       .refetch()
       .then(() => {
-        done.fail(new Error('Error on refetch should reject promise'));
+        reject(new Error('Error on refetch should reject promise'));
       })
       .catch(error => {
         checkError(error);
-        done();
-      });
-
-    // We have an unhandled error warning from the `subscribe` above, which has no `error` cb
+      })
+      .then(resolve, reject);
   });
 
-  it('does not return incomplete data when two queries for the same item are executed', () => {
+  itAsync('does not return incomplete data when two queries for the same item are executed', (resolve, reject) => {
     const queryA = gql`
       query queryA {
         person(id: "abc") {
@@ -2565,9 +2552,9 @@ describe('QueryManager', () => {
     const queryManager = new QueryManager<NormalizedCacheObject>({
       link: mockSingleLink(
         { request: { query: queryA }, result: { data: dataA } },
-        { request: { query: queryB }, result: { data: dataB }, delay: 20 },
-      ),
-      store: new DataStore(new InMemoryCache({})),
+        { request: { query: queryB }, result: { data: dataB }, delay: 20 }
+      ).setOnError(reject),
+      cache: new InMemoryCache({}),
       ssrMode: true,
     });
 
@@ -2603,11 +2590,11 @@ describe('QueryManager', () => {
           partial: false,
         });
       }),
-    ]);
+    ]).then(resolve, reject);
   });
 
   describe('polling queries', () => {
-    it('allows you to poll queries', () => {
+    itAsync('allows you to poll queries', (resolve, reject) => {
       const query = gql`
         query fetchLuke($id: String) {
           people_one(id: $id) {
@@ -2633,6 +2620,7 @@ describe('QueryManager', () => {
       };
 
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query, variables },
           result: { data: data1 },
@@ -2653,10 +2641,10 @@ describe('QueryManager', () => {
         { observable },
         result => expect(stripSymbols(result.data)).toEqual(data1),
         result => expect(stripSymbols(result.data)).toEqual(data2),
-      );
+      ).then(resolve, reject);
     });
 
-    it('does not poll during SSR', done => {
+    itAsync('does not poll during SSR', (resolve, reject) => {
       const query = gql`
         query fetchLuke($id: String) {
           people_one(id: $id) {
@@ -2682,21 +2670,17 @@ describe('QueryManager', () => {
       };
 
       const queryManager = new QueryManager<NormalizedCacheObject>({
-        link: mockSingleLink(
-          {
-            request: { query, variables },
-            result: { data: data1 },
-          },
-          {
-            request: { query, variables },
-            result: { data: data2 },
-          },
-          {
-            request: { query, variables },
-            result: { data: data2 },
-          },
-        ),
-        store: new DataStore(new InMemoryCache({ addTypename: false })),
+        link: mockSingleLink({
+          request: { query, variables },
+          result: { data: data1 },
+        }, {
+          request: { query, variables },
+          result: { data: data2 },
+        }, {
+          request: { query, variables },
+          result: { data: data2 },
+        }).setOnError(reject),
+        cache: new InMemoryCache({ addTypename: false }),
         ssrMode: true,
       });
 
@@ -2708,7 +2692,6 @@ describe('QueryManager', () => {
       });
 
       let count = 1;
-      let doneCalled = false;
       const subHandle = observable.subscribe({
         next: (result: any) => {
           switch (count) {
@@ -2716,22 +2699,19 @@ describe('QueryManager', () => {
               expect(stripSymbols(result.data)).toEqual(data1);
               setTimeout(() => {
                 subHandle.unsubscribe();
-                if (!doneCalled) {
-                  done();
-                }
+                resolve();
               }, 15);
               count++;
               break;
             case 2:
             default:
-              doneCalled = true;
-              done.fail(new Error('Only expected one result, not multiple'));
+              reject(new Error('Only expected one result, not multiple'));
           }
         },
       });
     });
 
-    it('should let you handle multiple polled queries and unsubscribe from one of them', done => {
+    itAsync('should let you handle multiple polled queries and unsubscribe from one of them', (resolve, reject) => {
       const query1 = gql`
         query {
           author {
@@ -2782,6 +2762,7 @@ describe('QueryManager', () => {
         },
       };
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query: query1 },
           result: { data: data11 },
@@ -2843,11 +2824,11 @@ describe('QueryManager', () => {
         subscription1.unsubscribe();
         subscription2.unsubscribe();
 
-        done();
+        resolve();
       }, 400);
     });
 
-    it('allows you to unsubscribe from polled queries', () => {
+    itAsync('allows you to unsubscribe from polled queries', (resolve, reject) => {
       const query = gql`
         query fetchLuke($id: String) {
           people_one(id: $id) {
@@ -2873,6 +2854,7 @@ describe('QueryManager', () => {
       };
 
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query, variables },
           result: { data: data1 },
@@ -2903,10 +2885,10 @@ describe('QueryManager', () => {
         },
       );
 
-      return promise;
+      return promise.then(resolve, reject);
     });
 
-    it('allows you to unsubscribe from polled query errors', done => {
+    itAsync('allows you to unsubscribe from polled query errors', (resolve, reject) => {
       const query = gql`
         query fetchLuke($id: String) {
           people_one(id: $id) {
@@ -2932,6 +2914,7 @@ describe('QueryManager', () => {
       };
 
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query, variables },
           result: { data: data1 },
@@ -2955,7 +2938,7 @@ describe('QueryManager', () => {
 
       let isFinished;
       process.once('unhandledRejection', () => {
-        if (!isFinished) done.fail('unhandledRejection from network');
+        if (!isFinished) reject('unhandledRejection from network');
       });
 
       const { promise, subscription } = observableToPromiseAndSubscription(
@@ -2975,12 +2958,12 @@ describe('QueryManager', () => {
       promise.then(() => {
         setTimeout(() => {
           isFinished = true;
-          done();
+          resolve();
         }, 4);
       });
     });
 
-    it('exposes a way to start a polling query', () => {
+    itAsync('exposes a way to start a polling query', (resolve, reject) => {
       const query = gql`
         query fetchLuke($id: String) {
           people_one(id: $id) {
@@ -3006,6 +2989,7 @@ describe('QueryManager', () => {
       };
 
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query, variables },
           result: { data: data1 },
@@ -3027,10 +3011,10 @@ describe('QueryManager', () => {
         { observable },
         result => expect(stripSymbols(result.data)).toEqual(data1),
         result => expect(stripSymbols(result.data)).toEqual(data2),
-      );
+      ).then(resolve, reject);
     });
 
-    it('exposes a way to stop a polling query', () => {
+    itAsync('exposes a way to stop a polling query', (resolve, reject) => {
       const query = gql`
         query fetchLeia($id: String) {
           people_one(id: $id) {
@@ -3056,6 +3040,7 @@ describe('QueryManager', () => {
       };
 
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query, variables },
           result: { data: data1 },
@@ -3074,10 +3059,10 @@ describe('QueryManager', () => {
       return observableToPromise({ observable, wait: 60 }, result => {
         expect(stripSymbols(result.data)).toEqual(data1);
         observable.stopPolling();
-      });
+      }).then(resolve, reject);
     });
 
-    it('stopped polling queries still get updates', () => {
+    itAsync('stopped polling queries still get updates', (resolve, reject) => {
       const query = gql`
         query fetchLeia($id: String) {
           people_one(id: $id) {
@@ -3103,6 +3088,7 @@ describe('QueryManager', () => {
       };
 
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query, variables },
           result: { data: data1 },
@@ -3135,11 +3121,11 @@ describe('QueryManager', () => {
         new Promise((_, reject) => {
           timeout = (error: Error) => reject(error);
         }),
-      ]);
+      ]).then(resolve, reject);
     });
   });
   describe('store resets', () => {
-    it('returns a promise resolving when all queries have been refetched', () => {
+    itAsync('returns a promise resolving when all queries have been refetched', (resolve, reject) => {
       const query = gql`
         query {
           author {
@@ -3187,24 +3173,19 @@ describe('QueryManager', () => {
       };
 
       const queryManager = createQueryManager({
-        link: mockSingleLink(
-          {
-            request: { query },
-            result: { data },
-          },
-          {
-            request: { query: query2 },
-            result: { data: data2 },
-          },
-          {
-            request: { query },
-            result: { data: dataChanged },
-          },
-          {
-            request: { query: query2 },
-            result: { data: data2Changed },
-          },
-        ),
+        link: mockSingleLink({
+          request: { query },
+          result: { data },
+        }, {
+          request: { query: query2 },
+          result: { data: data2 },
+        }, {
+          request: { query },
+          result: { data: dataChanged },
+        }, {
+          request: { query: query2 },
+          result: { data: data2Changed },
+        }).setOnError(reject),
       });
 
       const observable = queryManager.watchQuery<any>({ query });
@@ -3230,19 +3211,23 @@ describe('QueryManager', () => {
           expect(result2.partial).toBe(false);
           expect(stripSymbols(result2.data)).toEqual(data2Changed);
         });
-      });
+      }).then(resolve, reject);
     });
 
-    it('should change the store state to an empty state', () => {
-      const queryManager = createQueryManager({});
+    itAsync('should change the store state to an empty state', (resolve, reject) => {
+      const queryManager = createQueryManager({
+        link: mockSingleLink().setOnError(reject),
+      });
 
       queryManager.resetStore();
 
       expect(
-        (queryManager.dataStore.getCache() as InMemoryCache).extract(),
+        queryManager.cache.extract(),
       ).toEqual({});
       expect(queryManager.queryStore.getStore()).toEqual({});
       expect(queryManager.mutationStore.getStore()).toEqual({});
+
+      resolve();
     });
 
     xit('should only refetch once when we store reset', () => {
@@ -3303,7 +3288,7 @@ describe('QueryManager', () => {
       );
     });
 
-    it('should not refetch torn-down queries', done => {
+    itAsync('should not refetch torn-down queries', (resolve, reject) => {
       let queryManager: QueryManager<NormalizedCacheObject>;
       let observable: ObservableQuery<any>;
       const query = gql`
@@ -3346,7 +3331,7 @@ describe('QueryManager', () => {
         setTimeout(() => {
           expect(timesFired).toBe(1);
 
-          done();
+          resolve();
         }, 50);
       });
     });
@@ -3415,7 +3400,7 @@ describe('QueryManager', () => {
       );
     });
 
-    it('should not error on a stopped query()', done => {
+    itAsync('should not error on a stopped query()', (resolve, reject) => {
       let queryManager: QueryManager<NormalizedCacheObject>;
       const query = gql`
         query {
@@ -3445,13 +3430,13 @@ describe('QueryManager', () => {
       const queryId = '1';
       queryManager
         .fetchQuery(queryId, { query })
-        .catch(e => done.fail('Exception thrown for stopped query'));
+        .catch(e => reject('Exception thrown for stopped query'));
 
       queryManager.removeQuery(queryId);
-      queryManager.resetStore().then(() => done());
+      queryManager.resetStore().then(resolve, reject);
     });
 
-    it('should throw an error on an inflight fetch query if the store is reset', done => {
+    itAsync('should throw an error on an inflight fetch query if the store is reset', (resolve, reject) => {
       const query = gql`
         query {
           author {
@@ -3466,7 +3451,7 @@ describe('QueryManager', () => {
           lastName: 'Smith',
         },
       };
-      const queryManager = mockQueryManager({
+      const queryManager = mockQueryManager(reject, {
         request: { query },
         result: { data },
         delay: 10000, //i.e. forever
@@ -3474,16 +3459,18 @@ describe('QueryManager', () => {
       queryManager
         .fetchQuery('made up id', { query })
         .then(() => {
-          done.fail(new Error('Returned a result.'));
+          reject(new Error('Returned a result.'));
         })
         .catch(error => {
           expect(error.message).toMatch('Store reset');
-          done();
+          resolve();
         });
-      queryManager.resetStore();
+      // Need to delay the reset at least until the fetchRequest method
+      // has had a chance to enter this request into fetchQueryRejectFns.
+      setTimeout(() => queryManager.resetStore(), 100);
     });
 
-    it('should call refetch on a mocked Observable if the store is reset', done => {
+    itAsync('should call refetch on a mocked Observable if the store is reset', (resolve, reject) => {
       const query = gql`
         query {
           author {
@@ -3492,11 +3479,11 @@ describe('QueryManager', () => {
           }
         }
       `;
-      const queryManager = mockQueryManager();
+      const queryManager = mockQueryManager(reject);
 
       const mockObservableQuery: ObservableQuery<any> = ({
         refetch(_: any): Promise<ExecutionResult> {
-          done();
+          resolve();
           return null as never;
         },
         options: {
@@ -3511,7 +3498,7 @@ describe('QueryManager', () => {
       queryManager.resetStore();
     });
 
-    it('should not call refetch on a cache-only Observable if the store is reset', done => {
+    itAsync('should not call refetch on a cache-only Observable if the store is reset', (resolve, reject) => {
       const query = gql`
         query {
           author {
@@ -3520,7 +3507,9 @@ describe('QueryManager', () => {
           }
         }
       `;
-      const queryManager = createQueryManager({});
+      const queryManager = createQueryManager({
+        link: mockSingleLink().setOnError(reject),
+      });
       const options = assign({}) as WatchQueryOptions;
       options.fetchPolicy = 'cache-only';
       options.query = query;
@@ -3541,11 +3530,11 @@ describe('QueryManager', () => {
       queryManager.resetStore();
       setTimeout(() => {
         expect(refetchCount).toEqual(0);
-        done();
+        resolve();
       }, 50);
     });
 
-    it('should not call refetch on a standby Observable if the store is reset', done => {
+    itAsync('should not call refetch on a standby Observable if the store is reset', (resolve, reject) => {
       const query = gql`
         query {
           author {
@@ -3554,7 +3543,9 @@ describe('QueryManager', () => {
           }
         }
       `;
-      const queryManager = createQueryManager({});
+      const queryManager = createQueryManager({
+        link: mockSingleLink().setOnError(reject),
+      });
       const options = assign({}) as WatchQueryOptions;
       options.fetchPolicy = 'standby';
       options.query = query;
@@ -3575,11 +3566,11 @@ describe('QueryManager', () => {
       queryManager.resetStore();
       setTimeout(() => {
         expect(refetchCount).toEqual(0);
-        done();
+        resolve();
       }, 50);
     });
 
-    it('should throw an error on an inflight query() if the store is reset', done => {
+    itAsync('should throw an error on an inflight query() if the store is reset', (resolve, reject) => {
       let queryManager: QueryManager<NormalizedCacheObject>;
       const query = gql`
         query {
@@ -3610,15 +3601,15 @@ describe('QueryManager', () => {
       queryManager
         .query<any>({ query })
         .then(() => {
-          done.fail(new Error('query() gave results on a store reset'));
+          reject(new Error('query() gave results on a store reset'));
         })
         .catch(() => {
-          done();
+          resolve();
         });
     });
   });
   describe('refetching observed queries', () => {
-    it('returns a promise resolving when all queries have been refetched', () => {
+    itAsync('returns a promise resolving when all queries have been refetched', (resolve, reject) => {
       const query = gql`
         query {
           author {
@@ -3666,24 +3657,19 @@ describe('QueryManager', () => {
       };
 
       const queryManager = createQueryManager({
-        link: mockSingleLink(
-          {
-            request: { query },
-            result: { data },
-          },
-          {
-            request: { query: query2 },
-            result: { data: data2 },
-          },
-          {
-            request: { query },
-            result: { data: dataChanged },
-          },
-          {
-            request: { query: query2 },
-            result: { data: data2Changed },
-          },
-        ),
+        link: mockSingleLink({
+          request: { query },
+          result: { data },
+        }, {
+          request: { query: query2 },
+          result: { data: data2 },
+        }, {
+          request: { query },
+          result: { data: dataChanged },
+        }, {
+          request: { query: query2 },
+          result: { data: data2Changed },
+        }).setOnError(reject),
       });
 
       const observable = queryManager.watchQuery<any>({ query });
@@ -3709,10 +3695,10 @@ describe('QueryManager', () => {
           expect(result2.partial).toBe(false);
           expect(stripSymbols(result2.data)).toEqual(data2Changed);
         });
-      });
+      }).then(resolve, reject);
     });
 
-    it('should only refetch once when we refetch observable queries', done => {
+    itAsync('should only refetch once when we refetch observable queries', (resolve, reject) => {
       let queryManager: QueryManager<NormalizedCacheObject>;
       const query = gql`
         query {
@@ -3766,14 +3752,14 @@ describe('QueryManager', () => {
           // only refetch once and make sure data has changed
           expect(stripSymbols(result.data)).toEqual(data2);
           expect(timesFired).toBe(2);
-          done();
+          resolve();
         },
       ).catch(e => {
-        done.fail(e);
+        reject(e);
       });
     });
 
-    it('should not refetch torn-down queries', done => {
+    itAsync('should not refetch torn-down queries', (resolve, reject) => {
       let queryManager: QueryManager<NormalizedCacheObject>;
       let observable: ObservableQuery<any>;
       const query = gql`
@@ -3816,7 +3802,7 @@ describe('QueryManager', () => {
         setTimeout(() => {
           expect(timesFired).toBe(1);
 
-          done();
+          resolve();
         }, 50);
       });
     });
@@ -3871,7 +3857,7 @@ describe('QueryManager', () => {
       );
     });
 
-    it('should NOT throw an error on an inflight fetch query if the observable queries are refetched', done => {
+    itAsync('should NOT throw an error on an inflight fetch query if the observable queries are refetched', (resolve, reject) => {
       const query = gql`
         query {
           author {
@@ -3886,23 +3872,21 @@ describe('QueryManager', () => {
           lastName: 'Smith',
         },
       };
-      const queryManager = mockQueryManager({
+      const queryManager = mockQueryManager(reject, {
         request: { query },
         result: { data },
         delay: 100,
       });
       queryManager
         .fetchQuery('made up id', { query })
-        .then(() => {
-          done();
-        })
+        .then(resolve)
         .catch(error => {
-          done.fail(new Error('Should not return an error'));
+          reject(new Error('Should not return an error'));
         });
       queryManager.reFetchObservableQueries();
     });
 
-    it('should call refetch on a mocked Observable if the observed queries are refetched', done => {
+    itAsync('should call refetch on a mocked Observable if the observed queries are refetched', (resolve, reject) => {
       const query = gql`
         query {
           author {
@@ -3911,11 +3895,11 @@ describe('QueryManager', () => {
           }
         }
       `;
-      const queryManager = mockQueryManager();
+      const queryManager = mockQueryManager(reject);
 
       const mockObservableQuery: ObservableQuery<any> = ({
         refetch(_: any): Promise<ExecutionResult> {
-          done();
+          resolve();
           return null as never;
         },
         options: {
@@ -3930,7 +3914,7 @@ describe('QueryManager', () => {
       queryManager.reFetchObservableQueries();
     });
 
-    it('should not call refetch on a cache-only Observable if the observed queries are refetched', done => {
+    itAsync('should not call refetch on a cache-only Observable if the observed queries are refetched', (resolve, reject) => {
       const query = gql`
         query {
           author {
@@ -3939,7 +3923,9 @@ describe('QueryManager', () => {
           }
         }
       `;
-      const queryManager = createQueryManager({});
+      const queryManager = createQueryManager({
+        link: mockSingleLink().setOnError(reject),
+      });
       const options = assign({}) as WatchQueryOptions;
       options.fetchPolicy = 'cache-only';
       options.query = query;
@@ -3960,11 +3946,11 @@ describe('QueryManager', () => {
       queryManager.reFetchObservableQueries();
       setTimeout(() => {
         expect(refetchCount).toEqual(0);
-        done();
+        resolve();
       }, 50);
     });
 
-    it('should not call refetch on a standby Observable if the observed queries are refetched', done => {
+    itAsync('should not call refetch on a standby Observable if the observed queries are refetched', (resolve, reject) => {
       const query = gql`
         query {
           author {
@@ -3973,7 +3959,9 @@ describe('QueryManager', () => {
           }
         }
       `;
-      const queryManager = createQueryManager({});
+      const queryManager = createQueryManager({
+        link: mockSingleLink().setOnError(reject),
+      });
       const options = assign({}) as WatchQueryOptions;
       options.fetchPolicy = 'standby';
       options.query = query;
@@ -3994,11 +3982,11 @@ describe('QueryManager', () => {
       queryManager.reFetchObservableQueries();
       setTimeout(() => {
         expect(refetchCount).toEqual(0);
-        done();
+        resolve();
       }, 50);
     });
 
-    it('should refetch on a standby Observable if the observed queries are refetched and the includeStandby parameter is set to true', done => {
+    itAsync('should refetch on a standby Observable if the observed queries are refetched and the includeStandby parameter is set to true', (resolve, reject) => {
       const query = gql`
         query {
           author {
@@ -4007,7 +3995,9 @@ describe('QueryManager', () => {
           }
         }
       `;
-      const queryManager = createQueryManager({});
+      const queryManager = createQueryManager({
+        link: mockSingleLink().setOnError(reject),
+      });
       const options = assign({}) as WatchQueryOptions;
       options.fetchPolicy = 'standby';
       options.query = query;
@@ -4029,11 +4019,11 @@ describe('QueryManager', () => {
       queryManager.reFetchObservableQueries(includeStandBy);
       setTimeout(() => {
         expect(refetchCount).toEqual(1);
-        done();
+        resolve();
       }, 50);
     });
 
-    it('should NOT throw an error on an inflight query() if the observed queries are refetched', done => {
+    itAsync('should NOT throw an error on an inflight query() if the observed queries are refetched', (resolve, reject) => {
       let queryManager: QueryManager<NormalizedCacheObject>;
       const query = gql`
         query {
@@ -4064,10 +4054,10 @@ describe('QueryManager', () => {
       queryManager
         .query<any>({ query })
         .then(() => {
-          done();
+          resolve();
         })
         .catch(e => {
-          done.fail(
+          reject(
             new Error(
               'query() should not throw error when refetching observed queriest',
             ),
@@ -4075,8 +4065,9 @@ describe('QueryManager', () => {
         });
     });
   });
+
   describe('loading state', () => {
-    it('should be passed as false if we are not watching a query', () => {
+    itAsync('should be passed as false if we are not watching a query', (resolve, reject) => {
       const query = gql`
         query {
           fortuneCookie
@@ -4085,7 +4076,7 @@ describe('QueryManager', () => {
       const data = {
         fortuneCookie: 'Buy it',
       };
-      return mockQueryManager({
+      return mockQueryManager(reject, {
         request: { query },
         result: { data },
       })
@@ -4093,10 +4084,11 @@ describe('QueryManager', () => {
         .then(result => {
           expect(!result.loading).toBeTruthy();
           expect(stripSymbols(result.data)).toEqual(data);
-        });
+        })
+        .then(resolve, reject);
     });
 
-    it('should be passed to the observer as true if we are returning partial data', () => {
+    itAsync('should be passed to the observer as true if we are returning partial data', (resolve, reject) => {
       const fortuneCookie =
         'You must stick to your goal but rethink your approach';
       const primeQuery = gql`
@@ -4118,6 +4110,7 @@ describe('QueryManager', () => {
       const fullData = { fortuneCookie, author };
 
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query },
           result: { data: fullData },
@@ -4148,12 +4141,13 @@ describe('QueryManager', () => {
               expect(result.data).toEqual(fullData);
             },
           );
-        });
+        })
+        .then(resolve, reject);
     });
 
-    it('should be passed to the observer as false if we are returning all the data', done => {
+    itAsync('should be passed to the observer as false if we are returning all the data', (resolve, reject) => {
       assertWithObserver({
-        done,
+        reject,
         query: gql`
           query {
             author {
@@ -4173,13 +4167,13 @@ describe('QueryManager', () => {
         observer: {
           next(result) {
             expect(!result.loading).toBeTruthy();
-            done();
+            resolve();
           },
         },
       });
     });
 
-    it('will update on `resetStore`', done => {
+    itAsync('will update on `resetStore`', (resolve, reject) => {
       const testQuery = gql`
         query {
           author {
@@ -4201,6 +4195,7 @@ describe('QueryManager', () => {
         },
       };
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query: testQuery },
           result: { data: data1 },
@@ -4230,17 +4225,17 @@ describe('QueryManager', () => {
               case 1:
                 expect(result.loading).toBe(false);
                 expect(stripSymbols(result.data)).toEqual(data2);
-                done();
+                resolve();
                 break;
               default:
-                done.fail(new Error('`next` was called to many times.'));
+                reject(new Error('`next` was called to many times.'));
             }
           },
-          error: error => done.fail(error),
+          error: error => reject(error),
         });
     });
 
-    it('will be true when partial data may be returned', done => {
+    itAsync('will be true when partial data may be returned', (resolve, reject) => {
       const query1 = gql`{
         a { x1 y1 z1 }
       }`;
@@ -4256,6 +4251,7 @@ describe('QueryManager', () => {
         b: { x2: 3, y2: 2, z2: 1 },
       };
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query: query1 },
           result: { data: data1 },
@@ -4286,16 +4282,15 @@ describe('QueryManager', () => {
                   case 1:
                     expect(result2.loading).toBe(false);
                     expect(result2.data).toEqual(data2);
-                    done();
+                    resolve();
                     break;
                   default:
-                    done(new Error('`next` was called to many times.'));
+                    reject(new Error('`next` was called to many times.'));
                 }
               },
-              error: error => done(error),
+              error: reject,
             });
-        })
-        .catch(done);
+        }).then(resolve, reject);
     });
   });
 
@@ -4304,7 +4299,7 @@ describe('QueryManager', () => {
     let warned: any;
     let timesWarned = 0;
 
-    beforeEach(done => {
+    beforeEach(() => {
       // clear warnings
       warned = null;
       timesWarned = 0;
@@ -4313,10 +4308,9 @@ describe('QueryManager', () => {
         warned = args;
         timesWarned++;
       };
-      done();
     });
 
-    it('should refetch the right query when a result is successfully returned', () => {
+    itAsync('should refetch the right query when a result is successfully returned', (resolve, reject) => {
       const mutation = gql`
         mutation changeAuthorName {
           changeAuthorName(newName: "Jack Smith") {
@@ -4353,6 +4347,7 @@ describe('QueryManager', () => {
       };
       const variables = { id: '1234' };
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query, variables },
           result: { data },
@@ -4383,10 +4378,10 @@ describe('QueryManager', () => {
           );
           expect(stripSymbols(result.data)).toEqual(secondReqData);
         },
-      );
+      ).then(resolve, reject);
     });
 
-    it('should not warn and continue when an unknown query name is asked to refetch', () => {
+    itAsync('should not warn and continue when an unknown query name is asked to refetch', (resolve, reject) => {
       const mutation = gql`
         mutation changeAuthorName {
           changeAuthorName(newName: "Jack Smith") {
@@ -4422,6 +4417,7 @@ describe('QueryManager', () => {
         },
       };
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query },
           result: { data },
@@ -4452,10 +4448,10 @@ describe('QueryManager', () => {
           expect(stripSymbols(result.data)).toEqual(secondReqData);
           expect(timesWarned).toBe(0);
         },
-      );
+      ).then(resolve, reject);
     });
 
-    it('should ignore without warning a query name that is asked to refetch with no active subscriptions', () => {
+    itAsync('should ignore without warning a query name that is asked to refetch with no active subscriptions', (resolve, reject) => {
       const mutation = gql`
         mutation changeAuthorName {
           changeAuthorName(newName: "Jack Smith") {
@@ -4491,6 +4487,7 @@ describe('QueryManager', () => {
         },
       };
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query },
           result: { data },
@@ -4516,10 +4513,11 @@ describe('QueryManager', () => {
             refetchQueries: ['getAuthors'],
           });
         })
-        .then(() => expect(timesWarned).toBe(0));
+        .then(() => expect(timesWarned).toBe(0))
+        .then(resolve, reject);
     });
 
-    it('also works with a query document and variables', done => {
+    itAsync('also works with a query document and variables', (resolve, reject) => {
       const mutation = gql`
         mutation changeAuthorName($id: ID!) {
           changeAuthorName(newName: "Jack Smith", id: $id) {
@@ -4558,6 +4556,7 @@ describe('QueryManager', () => {
       const variables = { id: '1234' };
       const mutationVariables = { id: '2345' };
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query, variables },
           result: { data },
@@ -4592,7 +4591,7 @@ describe('QueryManager', () => {
               expect(stripSymbols(observable.getCurrentResult().data)).toEqual(
                 secondReqData,
               );
-              done();
+              resolve();
             }, 1);
 
             expect(resultData).toEqual(secondReqData);
@@ -4603,7 +4602,7 @@ describe('QueryManager', () => {
       });
     });
 
-    it('also works with a conditional function that returns false', () => {
+    itAsync('also works with a conditional function that returns false', (resolve, reject) => {
       const mutation = gql`
         mutation changeAuthorName {
           changeAuthorName(newName: "Jack Smith") {
@@ -4639,6 +4638,7 @@ describe('QueryManager', () => {
         },
       };
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query },
           result: { data },
@@ -4661,9 +4661,10 @@ describe('QueryManager', () => {
       return observableToPromise({ observable }, result => {
         expect(stripSymbols(result.data)).toEqual(data);
         queryManager.mutate({ mutation, refetchQueries: conditional });
-      });
+      }).then(resolve, reject);
     });
-    it('also works with a conditional function that returns an array of refetches', () => {
+
+    itAsync('also works with a conditional function that returns an array of refetches', (resolve, reject) => {
       const mutation = gql`
         mutation changeAuthorName {
           changeAuthorName(newName: "Jack Smith") {
@@ -4699,6 +4700,7 @@ describe('QueryManager', () => {
         },
       };
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query },
           result: { data },
@@ -4725,10 +4727,10 @@ describe('QueryManager', () => {
           queryManager.mutate({ mutation, refetchQueries: conditional });
         },
         result => expect(stripSymbols(result.data)).toEqual(secondReqData),
-      );
+      ).then(resolve, reject);
     });
 
-    it('should refetch using the original query context (if any)', () => {
+    itAsync('should refetch using the original query context (if any)', (resolve, reject) => {
       const mutation = gql`
         mutation changeAuthorName {
           changeAuthorName(newName: "Jack Smith") {
@@ -4765,6 +4767,7 @@ describe('QueryManager', () => {
       };
       const variables = { id: '1234' };
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query, variables },
           result: { data },
@@ -4804,10 +4807,10 @@ describe('QueryManager', () => {
           expect(context.headers).not.toBeUndefined();
           expect(context.headers.someHeader).toEqual(headers.someHeader);
         },
-      );
+      ).then(resolve, reject);
     });
 
-    it('should refetch using the specified context, if provided', () => {
+    itAsync('should refetch using the specified context, if provided', (resolve, reject) => {
       const mutation = gql`
         mutation changeAuthorName {
           changeAuthorName(newName: "Jack Smith") {
@@ -4844,6 +4847,7 @@ describe('QueryManager', () => {
       };
       const variables = { id: '1234' };
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query, variables },
           result: { data },
@@ -4889,18 +4893,16 @@ describe('QueryManager', () => {
           expect(context.headers).not.toBeUndefined();
           expect(context.headers.someHeader).toEqual(headers.someHeader);
         },
-      );
+      ).then(resolve, reject);
     });
 
-    afterEach(done => {
-      // restore standard method
+    afterEach(() => {
       console.warn = oldWarn;
-      done();
     });
   });
 
   describe('awaitRefetchQueries', () => {
-    function awaitRefetchTest({ awaitRefetchQueries }) {
+    const awaitRefetchTest = ({ awaitRefetchQueries }) => new Promise((resolve, reject) => {
       const query = gql`
         query getAuthors($id: ID!) {
           author(id: $id) {
@@ -4943,6 +4945,7 @@ describe('QueryManager', () => {
       const variables = { id: '1234' };
 
       const queryManager = mockQueryManager(
+        reject,
         {
           request: { query, variables },
           result: { data: queryData },
@@ -4990,29 +4993,30 @@ describe('QueryManager', () => {
           );
           expect(stripSymbols(result.data)).toEqual(secondReqData);
         },
-      );
-    }
+      ).then(resolve, reject);
+    });
 
     it(
       'should not wait for `refetchQueries` to complete before resolving ' +
-        'the mutation, when `awaitRefetchQueries` is falsy',
-      () => {
-        awaitRefetchTest({ awaitRefetchQueries: undefined });
-        awaitRefetchTest({ awaitRefetchQueries: false });
-      },
+        'the mutation, when `awaitRefetchQueries` is undefined',
+      () => awaitRefetchTest({ awaitRefetchQueries: void 0 })
+    );
+
+    it(
+      'should not wait for `refetchQueries` to complete before resolving ' +
+        'the mutation, when `awaitRefetchQueries` is false',
+      () => awaitRefetchTest({ awaitRefetchQueries: false })
     );
 
     it(
       'should wait for `refetchQueries` to complete before resolving ' +
         'the mutation, when `awaitRefetchQueries` is `true`',
-      () => {
-        awaitRefetchTest({ awaitRefetchQueries: true });
-      },
+      () => awaitRefetchTest({ awaitRefetchQueries: true })
     );
   });
 
   describe('store watchers', () => {
-    it('does not fill up the store on resolved queries', () => {
+    itAsync('does not fill up the store on resolved queries', (resolve, reject) => {
       const query1 = gql`
         query One {
           one
@@ -5038,13 +5042,13 @@ describe('QueryManager', () => {
         { request: { query: query1 }, result: { data: { one: 1 } } },
         { request: { query: query2 }, result: { data: { two: 2 } } },
         { request: { query: query3 }, result: { data: { three: 3 } } },
-        { request: { query: query4 }, result: { data: { four: 4 } } },
-      );
+        { request: { query: query4 }, result: { data: { four: 4 } } }
+      ).setOnError(reject);
       const cache = new InMemoryCache();
 
       const queryManager = new QueryManager<NormalizedCacheObject>({
         link,
-        store: new DataStore(cache),
+        cache,
       });
 
       return queryManager
@@ -5065,15 +5069,15 @@ describe('QueryManager', () => {
         })
         .then(() => {
           expect(cache.watches.size).toBe(0);
-        });
+        })
+        .then(resolve, reject);
     });
   });
 
   describe('`no-cache` handling', () => {
-    it(
-      'should return a query result (if one exists) when a `no-cache` ' +
-        'fetch policy is used',
-      done => {
+    itAsync(
+      'should return a query result (if one exists) when a `no-cache` fetch policy is used',
+      (resolve, reject) => {
         const query = gql`
           query {
             author {
@@ -5094,7 +5098,7 @@ describe('QueryManager', () => {
           link: mockSingleLink({
             request: { query },
             result: { data },
-          }),
+          }).setOnError(reject),
         });
 
         const observable = queryManager.watchQuery<any>({
@@ -5105,14 +5109,14 @@ describe('QueryManager', () => {
           expect(stripSymbols(result.data)).toEqual(data);
           const currentResult = queryManager.getCurrentQueryResult(observable);
           expect(currentResult.data).toEqual(data);
-          done();
+          resolve();
         });
       },
     );
   });
 
   describe('client awareness', () => {
-    it('should pass client awareness settings into the link chain via context', done => {
+    itAsync('should pass client awareness settings into the link chain via context', (resolve, reject) => {
       const query = gql`
         query {
           author {
@@ -5132,7 +5136,7 @@ describe('QueryManager', () => {
       const link = mockSingleLink({
         request: { query },
         result: { data },
-      });
+      }).setOnError(reject);
 
       const clientAwareness = {
         name: 'Test',
@@ -5153,7 +5157,7 @@ describe('QueryManager', () => {
         const context = link.operation.getContext();
         expect(context.clientAwareness).toBeDefined();
         expect(context.clientAwareness).toEqual(clientAwareness);
-        done();
+        resolve();
       });
     });
   });
diff --git a/packages/apollo-client/src/core/__tests__/QueryManager/links.ts b/src/core/__tests__/QueryManager/links.ts
similarity index 80%
rename from packages/apollo-client/src/core/__tests__/QueryManager/links.ts
rename to src/core/__tests__/QueryManager/links.ts
--- a/packages/apollo-client/src/core/__tests__/QueryManager/links.ts
+++ b/src/core/__tests__/QueryManager/links.ts
@@ -1,36 +1,17 @@
 // externals
-import Rx from 'rxjs';
-import { assign } from 'lodash';
 import gql from 'graphql-tag';
-import { DocumentNode, ExecutionResult } from 'graphql';
-import { ApolloLink, Operation, Observable } from 'apollo-link';
-import { InMemoryCache, ApolloReducerConfig } from 'apollo-cache-inmemory';
-import { stripSymbols } from 'apollo-utilities';
+
+import { Observable } from '../../../utilities/observables/Observable';
+import { ApolloLink } from '../../../link/core/ApolloLink';
+import { InMemoryCache } from '../../../cache/inmemory/inMemoryCache';
+import { stripSymbols } from '../../../utilities/testing/stripSymbols';
 
 // mocks
-import mockQueryManager from '../../../__mocks__/mockQueryManager';
-import mockWatchQuery from '../../../__mocks__/mockWatchQuery';
-import {
-  mockSingleLink,
-  MockSubscriptionLink,
-} from '../../../__mocks__/mockLinks';
+import { MockSubscriptionLink } from '../../../utilities/testing/mocking/mockSubscriptionLink';
 
 // core
-import { ApolloQueryResult } from '../../types';
-import { NetworkStatus } from '../../networkStatus';
-import { ObservableQuery } from '../../ObservableQuery';
-import { WatchQueryOptions } from '../../watchQueryOptions';
 import { QueryManager } from '../../QueryManager';
-
-import { ApolloError } from '../../../errors/ApolloError';
-import { DataStore } from '../../../data/store';
-import { Observer } from '../../../util/Observable';
-
-// testing utils
-import wrap from '../../../util/wrap';
-import observableToPromise, {
-  observableToPromiseAndSubscription,
-} from '../../../util/observableToPromise';
+import { Reference } from '../../../utilities/graphql/storeUtils';
 
 describe('Link interactions', () => {
   it('includes the cache on the context for eviction links', done => {
@@ -71,7 +52,7 @@ describe('Link interactions', () => {
     const mockLink = new MockSubscriptionLink();
     const link = ApolloLink.from([evictionLink, mockLink]);
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link,
     });
 
@@ -114,7 +95,7 @@ describe('Link interactions', () => {
 
     const link = new MockSubscriptionLink();
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link,
     });
 
@@ -185,7 +166,7 @@ describe('Link interactions', () => {
 
     const link = new MockSubscriptionLink();
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link,
     });
 
@@ -264,7 +245,7 @@ describe('Link interactions', () => {
     const mockLink = new MockSubscriptionLink();
     const link = ApolloLink.from([evictionLink, mockLink]);
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link,
     });
 
@@ -302,7 +283,7 @@ describe('Link interactions', () => {
     const mockLink = new MockSubscriptionLink();
     const link = ApolloLink.from([evictionLink, mockLink]);
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link,
     });
 
@@ -345,23 +326,22 @@ describe('Link interactions', () => {
 
     const queryManager = new QueryManager({
       link,
-      store: new DataStore(
-        new InMemoryCache({
-          cacheResolvers: {
-            Query: {
-              book: (_, { id }, context) => {
-                expect(context.getCacheKey).toBeDefined();
-                const cacheKey = context.getCacheKey({
-                  id,
-                  __typename: 'Book',
-                });
-                expect(cacheKey.id).toEqual(`Book:${id}`);
-                return cacheKey;
+      cache: new InMemoryCache({
+        typePolicies: {
+          Query: {
+            fields: {
+              book(_, { args, toReference, readField }) {
+                const ref = toReference({ __typename: "Book", id: args.id });
+                expect(ref).toEqual({ __ref: `Book:${args.id}` });
+                const found = readField<Reference[]>("books").find(
+                  book => book.__ref === ref.__ref);
+                expect(found).toBeTruthy();
+                return found;
               },
             },
           },
-        }),
-      ),
+        },
+      }),
     });
 
     await queryManager.query({ query });
diff --git a/packages/apollo-client/src/core/__tests__/QueryManager/live.ts b/src/core/__tests__/QueryManager/live.ts
similarity index 87%
rename from packages/apollo-client/src/core/__tests__/QueryManager/live.ts
rename to src/core/__tests__/QueryManager/live.ts
--- a/packages/apollo-client/src/core/__tests__/QueryManager/live.ts
+++ b/src/core/__tests__/QueryManager/live.ts
@@ -6,20 +6,12 @@
 
 // externals
 import gql from 'graphql-tag';
-import { DocumentNode, ExecutionResult } from 'graphql';
-import { ApolloLink, Operation, Observable } from 'apollo-link';
-import { InMemoryCache, ApolloReducerConfig } from 'apollo-cache-inmemory';
-import { stripSymbols } from 'apollo-utilities';
-
-import { MockSubscriptionLink } from '../../../__mocks__/mockLinks';
+import { InMemoryCache } from '../../../cache/inmemory/inMemoryCache';
+import { stripSymbols } from '../../../utilities/testing/stripSymbols';
+import { MockSubscriptionLink } from '../../../utilities/testing/mocking/mockSubscriptionLink';
 
 // core
-import { ApolloQueryResult } from '../../types';
-import { NetworkStatus } from '../../networkStatus';
-import { ObservableQuery } from '../../ObservableQuery';
-import { WatchQueryOptions } from '../../watchQueryOptions';
 import { QueryManager } from '../../QueryManager';
-import { DataStore } from '../../../data/store';
 
 describe('Live queries', () => {
   it('handles mutliple results for live queries', done => {
@@ -49,7 +41,7 @@ describe('Live queries', () => {
     };
     const link = new MockSubscriptionLink();
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link,
     });
 
@@ -115,7 +107,7 @@ describe('Live queries', () => {
     let count = 0;
     const link = new MockSubscriptionLink();
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link,
     });
 
@@ -199,7 +191,7 @@ describe('Live queries', () => {
     link.onUnsubscribe(() => cleanedupTimes++);
 
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link,
     });
 
diff --git a/packages/apollo-client/src/core/__tests__/QueryManager/multiple-results.ts b/src/core/__tests__/QueryManager/multiple-results.ts
similarity index 85%
rename from packages/apollo-client/src/core/__tests__/QueryManager/multiple-results.ts
rename to src/core/__tests__/QueryManager/multiple-results.ts
--- a/packages/apollo-client/src/core/__tests__/QueryManager/multiple-results.ts
+++ b/src/core/__tests__/QueryManager/multiple-results.ts
@@ -1,37 +1,16 @@
 // externals
-import Rx from 'rxjs';
-import { assign } from 'lodash';
 import gql from 'graphql-tag';
-import { DocumentNode, ExecutionResult } from 'graphql';
-import { ApolloLink, Operation, Observable } from 'apollo-link';
-import { InMemoryCache, ApolloReducerConfig } from 'apollo-cache-inmemory';
-import { stripSymbols } from 'apollo-utilities';
+import { InMemoryCache } from '../../../cache/inmemory/inMemoryCache';
+import { stripSymbols } from '../../../utilities/testing/stripSymbols';
 
 // mocks
-import mockQueryManager from '../../../__mocks__/mockQueryManager';
-import mockWatchQuery from '../../../__mocks__/mockWatchQuery';
 import {
-  mockSingleLink,
-  MockSubscriptionLink,
-} from '../../../__mocks__/mockLinks';
+  MockSubscriptionLink
+} from '../../../utilities/testing/mocking/mockSubscriptionLink';
 
 // core
-import { ApolloQueryResult } from '../../types';
-import { NetworkStatus } from '../../networkStatus';
-import { ObservableQuery } from '../../ObservableQuery';
-import { WatchQueryOptions } from '../../watchQueryOptions';
 import { QueryManager } from '../../QueryManager';
 
-import { ApolloError } from '../../../errors/ApolloError';
-import { DataStore } from '../../../data/store';
-import { Observer } from '../../../util/Observable';
-
-// testing utils
-import wrap from '../../../util/wrap';
-import observableToPromise, {
-  observableToPromiseAndSubscription,
-} from '../../../util/observableToPromise';
-
 describe('mutiple results', () => {
   it('allows multiple query results from link', done => {
     const query = gql`
@@ -61,7 +40,7 @@ describe('mutiple results', () => {
     };
     const link = new MockSubscriptionLink();
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link,
     });
 
@@ -118,7 +97,7 @@ describe('mutiple results', () => {
     };
     const link = new MockSubscriptionLink();
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link,
     });
 
@@ -187,7 +166,7 @@ describe('mutiple results', () => {
     };
     const link = new MockSubscriptionLink();
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link,
     });
 
@@ -258,7 +237,7 @@ describe('mutiple results', () => {
     };
     const link = new MockSubscriptionLink();
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link,
     });
 
@@ -333,7 +312,7 @@ describe('mutiple results', () => {
     };
     const link = new MockSubscriptionLink();
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link,
     });
 
diff --git a/packages/apollo-client/src/core/__tests__/QueryManager/recycler.ts b/src/core/__tests__/QueryManager/recycler.ts
similarity index 80%
rename from packages/apollo-client/src/core/__tests__/QueryManager/recycler.ts
rename to src/core/__tests__/QueryManager/recycler.ts
--- a/packages/apollo-client/src/core/__tests__/QueryManager/recycler.ts
+++ b/src/core/__tests__/QueryManager/recycler.ts
@@ -6,20 +6,14 @@
 
 // externals
 import gql from 'graphql-tag';
-import { DocumentNode, ExecutionResult } from 'graphql';
-import { ApolloLink, Operation, Observable } from 'apollo-link';
-import { InMemoryCache, ApolloReducerConfig } from 'apollo-cache-inmemory';
-import { stripSymbols } from 'apollo-utilities';
-
-import { MockSubscriptionLink } from '../../../__mocks__/mockLinks';
+import { InMemoryCache } from '../../../cache/inmemory/inMemoryCache';
+import { stripSymbols } from '../../../utilities/testing/stripSymbols';
+import {
+  MockSubscriptionLink
+} from '../../../utilities/testing/mocking/mockSubscriptionLink';
 
 // core
-import { ApolloQueryResult } from '../../types';
-import { NetworkStatus } from '../../networkStatus';
-import { ObservableQuery } from '../../ObservableQuery';
-import { WatchQueryOptions } from '../../watchQueryOptions';
 import { QueryManager } from '../../QueryManager';
-import { DataStore } from '../../../data/store';
 
 describe('Subscription lifecycles', () => {
   it('cleans up and reuses data like QueryRecycler wants', done => {
@@ -43,7 +37,7 @@ describe('Subscription lifecycles', () => {
 
     const link = new MockSubscriptionLink();
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link,
     });
 
diff --git a/packages/apollo-client/src/core/__tests__/fetchPolicies.ts b/src/core/__tests__/fetchPolicies.ts
similarity index 69%
rename from packages/apollo-client/src/core/__tests__/fetchPolicies.ts
rename to src/core/__tests__/fetchPolicies.ts
--- a/packages/apollo-client/src/core/__tests__/fetchPolicies.ts
+++ b/src/core/__tests__/fetchPolicies.ts
@@ -1,26 +1,12 @@
-import { cloneDeep, assign } from 'lodash';
-import { GraphQLError, ExecutionResult, DocumentNode } from 'graphql';
 import gql from 'graphql-tag';
-import { print } from 'graphql/language/printer';
-import { ApolloLink, Observable } from 'apollo-link';
-import {
-  InMemoryCache,
-  IntrospectionFragmentMatcher,
-  FragmentMatcherInterface,
-} from 'apollo-cache-inmemory';
-import { stripSymbols } from 'apollo-utilities';
 
-import { QueryManager } from '../QueryManager';
-import { WatchQueryOptions } from '../watchQueryOptions';
-
-import { ApolloError } from '../../errors/ApolloError';
-
-import ApolloClient, { printAST } from '../..';
-
-import subscribeAndCount from '../../util/subscribeAndCount';
-import { withWarning } from '../../util/wrap';
-
-import { mockSingleLink } from '../../__mocks__/mockLinks';
+import { ApolloLink } from '../../link/core/ApolloLink';
+import { InMemoryCache } from '../../cache/inmemory/inMemoryCache';
+import { stripSymbols } from '../../utilities/testing/stripSymbols';
+import { itAsync } from '../../utilities/testing/itAsync';
+import { ApolloClient } from '../..';
+import subscribeAndCount from '../../utilities/testing/subscribeAndCount';
+import { mockSingleLink } from '../../utilities/testing/mocking/mockLink';
 import { NetworkStatus } from '../networkStatus';
 
 const query = gql`
@@ -68,51 +54,41 @@ const mutationResult = {
 
 const merged = { author: { ...result.author, firstName: 'James' } };
 
-const createLink = () =>
-  mockSingleLink(
-    {
-      request: { query },
-      result: { data: result },
-    },
-    {
-      request: { query },
-      result: { data: result },
-    },
-  );
-
-const createFailureLink = () =>
-  mockSingleLink(
-    {
-      request: { query },
-      error: new Error('query failed'),
-    },
-    {
-      request: { query },
-      result: { data: result },
-    },
-  );
-
-const createMutationLink = () =>
+const createLink = (reject: (reason: any) => any) =>
+  mockSingleLink({
+    request: { query },
+    result: { data: result },
+  }, {
+    request: { query },
+    result: { data: result },
+  }).setOnError(reject);
+
+const createFailureLink = (reject: (reason: any) => any) =>
+  mockSingleLink({
+    request: { query },
+    error: new Error('query failed'),
+  }, {
+    request: { query },
+    result: { data: result },
+  }).setOnError(reject);
+
+const createMutationLink = (reject: (reason: any) => any) =>
   // fetch the data
-  mockSingleLink(
-    {
-      request: { query },
-      result: { data: result },
-    },
-    // update the data
-    {
-      request: { query: mutation, variables },
-      result: { data: mutationResult },
-    },
-    // get the new results
-    {
-      request: { query },
-      result: { data: merged },
-    },
-  );
+  mockSingleLink({
+    request: { query },
+    result: { data: result },
+  }, // update the data
+  {
+    request: { query: mutation, variables },
+    result: { data: mutationResult },
+  }, // get the new results
+  {
+    request: { query },
+    result: { data: merged },
+  }).setOnError(reject);
 
 describe('network-only', () => {
-  it('requests from the network even if already in cache', () => {
+  itAsync('requests from the network even if already in cache', (resolve, reject) => {
     let called = 0;
     const inspector = new ApolloLink((operation, forward) => {
       called++;
@@ -123,20 +99,21 @@ describe('network-only', () => {
     });
 
     const client = new ApolloClient({
-      link: inspector.concat(createLink()),
+      link: inspector.concat(createLink(reject)),
       cache: new InMemoryCache({ addTypename: false }),
     });
 
-    return client.query({ query }).then(() =>
-      client
+    return client.query({ query }).then(
+      () => client
         .query({ fetchPolicy: 'network-only', query })
         .then(actualResult => {
           expect(stripSymbols(actualResult.data)).toEqual(result);
           expect(called).toBe(4);
         }),
-    );
+    ).then(resolve, reject);
   });
-  it('saves data to the cache on success', () => {
+
+  itAsync('saves data to the cache on success', (resolve, reject) => {
     let called = 0;
     const inspector = new ApolloLink((operation, forward) => {
       called++;
@@ -147,18 +124,19 @@ describe('network-only', () => {
     });
 
     const client = new ApolloClient({
-      link: inspector.concat(createLink()),
+      link: inspector.concat(createLink(reject)),
       cache: new InMemoryCache({ addTypename: false }),
     });
 
-    return client.query({ query, fetchPolicy: 'network-only' }).then(() =>
-      client.query({ query }).then(actualResult => {
+    return client.query({ query, fetchPolicy: 'network-only' }).then(
+      () => client.query({ query }).then(actualResult => {
         expect(stripSymbols(actualResult.data)).toEqual(result);
         expect(called).toBe(2);
       }),
-    );
+    ).then(resolve, reject);
   });
-  it('does not save data to the cache on failure', () => {
+
+  itAsync('does not save data to the cache on failure', (resolve, reject) => {
     let called = 0;
     const inspector = new ApolloLink((operation, forward) => {
       called++;
@@ -169,7 +147,7 @@ describe('network-only', () => {
     });
 
     const client = new ApolloClient({
-      link: inspector.concat(createFailureLink()),
+      link: inspector.concat(createFailureLink(reject)),
       cache: new InMemoryCache({ addTypename: false }),
     });
 
@@ -180,17 +158,16 @@ describe('network-only', () => {
         expect(e.message).toMatch('query failed');
         didFail = true;
       })
-      .then(() =>
-        client.query({ query }).then(actualResult => {
-          expect(stripSymbols(actualResult.data)).toEqual(result);
-          // the first error doesn't call .map on the inspector
-          expect(called).toBe(3);
-          expect(didFail).toBe(true);
-        }),
-      );
+      .then(() => client.query({ query }).then(actualResult => {
+        expect(stripSymbols(actualResult.data)).toEqual(result);
+        // the first error doesn't call .map on the inspector
+        expect(called).toBe(3);
+        expect(didFail).toBe(true);
+      }))
+      .then(resolve, reject);
   });
 
-  it('updates the cache on a mutation', () => {
+  itAsync('updates the cache on a mutation', (resolve, reject) => {
     let called = 0;
     const inspector = new ApolloLink((operation, forward) => {
       called++;
@@ -201,7 +178,7 @@ describe('network-only', () => {
     });
 
     const client = new ApolloClient({
-      link: inspector.concat(createMutationLink()),
+      link: inspector.concat(createMutationLink(reject)),
       cache: new InMemoryCache({ addTypename: false }),
     });
 
@@ -216,11 +193,13 @@ describe('network-only', () => {
         return client.query({ query }).then(actualResult => {
           expect(stripSymbols(actualResult.data)).toEqual(merged);
         });
-      });
+      })
+      .then(resolve, reject);
   });
 });
+
 describe('no-cache', () => {
-  it('requests from the network when not in cache', () => {
+  itAsync('requests from the network when not in cache', (resolve, reject) => {
     let called = 0;
     const inspector = new ApolloLink((operation, forward) => {
       called++;
@@ -231,7 +210,7 @@ describe('no-cache', () => {
     });
 
     const client = new ApolloClient({
-      link: inspector.concat(createLink()),
+      link: inspector.concat(createLink(reject)),
       cache: new InMemoryCache({ addTypename: false }),
     });
 
@@ -240,9 +219,11 @@ describe('no-cache', () => {
       .then(actualResult => {
         expect(actualResult.data).toEqual(result);
         expect(called).toBe(2);
-      });
+      })
+      .then(resolve, reject);
   });
-  it('requests from the network even if already in cache', () => {
+
+  itAsync('requests from the network even if already in cache', (resolve, reject) => {
     let called = 0;
     const inspector = new ApolloLink((operation, forward) => {
       called++;
@@ -253,18 +234,19 @@ describe('no-cache', () => {
     });
 
     const client = new ApolloClient({
-      link: inspector.concat(createLink()),
+      link: inspector.concat(createLink(reject)),
       cache: new InMemoryCache({ addTypename: false }),
     });
 
-    return client.query({ query }).then(() =>
-      client.query({ fetchPolicy: 'no-cache', query }).then(actualResult => {
+    return client.query({ query }).then(
+      () => client.query({ fetchPolicy: 'no-cache', query }).then(actualResult => {
         expect(actualResult.data).toEqual(result);
         expect(called).toBe(4);
       }),
-    );
+    ).then(resolve, reject);
   });
-  it('does not save the data to the cache on success', () => {
+
+  itAsync('does not save the data to the cache on success', (resolve, reject) => {
     let called = 0;
     const inspector = new ApolloLink((operation, forward) => {
       called++;
@@ -275,20 +257,20 @@ describe('no-cache', () => {
     });
 
     const client = new ApolloClient({
-      link: inspector.concat(createLink()),
+      link: inspector.concat(createLink(reject)),
       cache: new InMemoryCache({ addTypename: false }),
     });
 
-    return client.query({ query, fetchPolicy: 'no-cache' }).then(() =>
-      client.query({ query }).then(actualResult => {
+    return client.query({ query, fetchPolicy: 'no-cache' }).then(
+      () => client.query({ query }).then(actualResult => {
         expect(stripSymbols(actualResult.data)).toEqual(result);
         // the second query couldn't read anything from the cache
         expect(called).toBe(4);
       }),
-    );
+    ).then(resolve, reject);
   });
 
-  it('does not save data to the cache on failure', () => {
+  itAsync('does not save data to the cache on failure', (resolve, reject) => {
     let called = 0;
     const inspector = new ApolloLink((operation, forward) => {
       called++;
@@ -299,7 +281,7 @@ describe('no-cache', () => {
     });
 
     const client = new ApolloClient({
-      link: inspector.concat(createFailureLink()),
+      link: inspector.concat(createFailureLink(reject)),
       cache: new InMemoryCache({ addTypename: false }),
     });
 
@@ -310,16 +292,16 @@ describe('no-cache', () => {
         expect(e.message).toMatch('query failed');
         didFail = true;
       })
-      .then(() =>
-        client.query({ query }).then(actualResult => {
-          expect(stripSymbols(actualResult.data)).toEqual(result);
-          // the first error doesn't call .map on the inspector
-          expect(called).toBe(3);
-          expect(didFail).toBe(true);
-        }),
-      );
+      .then(() => client.query({ query }).then(actualResult => {
+        expect(stripSymbols(actualResult.data)).toEqual(result);
+        // the first error doesn't call .map on the inspector
+        expect(called).toBe(3);
+        expect(didFail).toBe(true);
+      }))
+      .then(resolve, reject);
   });
-  it('does not update the cache on a mutation', () => {
+
+  itAsync('does not update the cache on a mutation', (resolve, reject) => {
     let called = 0;
     const inspector = new ApolloLink((operation, forward) => {
       called++;
@@ -330,7 +312,7 @@ describe('no-cache', () => {
     });
 
     const client = new ApolloClient({
-      link: inspector.concat(createMutationLink()),
+      link: inspector.concat(createMutationLink(reject)),
       cache: new InMemoryCache({ addTypename: false }),
     });
 
@@ -343,12 +325,13 @@ describe('no-cache', () => {
         return client.query({ query }).then(actualResult => {
           expect(stripSymbols(actualResult.data)).toEqual(result);
         });
-      });
+      })
+      .then(resolve, reject);
   });
 });
 
 describe('cache-and-network', function() {
-  it('gives appropriate networkStatus for refetched queries', done => {
+  itAsync('gives appropriate networkStatus for refetched queries', (resolve, reject) => {
     const client = new ApolloClient({
       link: ApolloLink.empty(),
       cache: new InMemoryCache(),
@@ -389,7 +372,7 @@ describe('cache-and-network', function() {
       };
     }
 
-    subscribeAndCount(done, observable, (count, result) => {
+    subscribeAndCount(reject, observable, (count, result) => {
       if (count === 1) {
         expect(result).toEqual({
           data: void 0,
@@ -449,7 +432,7 @@ describe('cache-and-network', function() {
           networkStatus: NetworkStatus.ready,
           stale: false,
         });
-        done();
+        resolve();
       }
     });
   });
diff --git a/packages/apollo-client/src/core/__tests__/scheduler.ts b/src/core/__tests__/scheduler.ts
similarity index 79%
rename from packages/apollo-client/src/core/__tests__/scheduler.ts
rename to src/core/__tests__/scheduler.ts
--- a/packages/apollo-client/src/core/__tests__/scheduler.ts
+++ b/src/core/__tests__/scheduler.ts
@@ -1,13 +1,14 @@
-import { InMemoryCache } from 'apollo-cache-inmemory';
 import gql from 'graphql-tag';
-import { stripSymbols } from 'apollo-utilities';
+
+import { InMemoryCache } from '../../cache/inmemory/inMemoryCache';
+import { stripSymbols } from '../../utilities/testing/stripSymbols';
+import { itAsync } from '../../utilities/testing/itAsync';
 
 import { QueryManager } from '../QueryManager';
 import { WatchQueryOptions } from '../../core/watchQueryOptions';
-import { mockSingleLink } from '../../__mocks__/mockLinks';
+import { mockSingleLink } from '../../utilities/testing/mocking/mockLink';
 import { NetworkStatus } from '../../core/networkStatus';
 
-import { DataStore } from '../../data/store';
 import { ObservableQuery } from '../../core/ObservableQuery';
 
 // Used only for unit testing.
@@ -36,10 +37,10 @@ function eachPollingQuery(
 }
 
 describe('QueryScheduler', () => {
-  it('should throw an error if we try to start polling a non-polling query', () => {
+  itAsync('should throw an error if we try to start polling a non-polling query', (resolve, reject) => {
     const queryManager = new QueryManager({
-      link: mockSingleLink(),
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      link: mockSingleLink().setOnError(reject),
+      cache: new InMemoryCache({ addTypename: false }),
     });
 
     const query = gql`
@@ -50,15 +51,15 @@ describe('QueryScheduler', () => {
         }
       }
     `;
-    const queryOptions: WatchQueryOptions = {
-      query,
-    };
+
     expect(() => {
-      queryManager.startPollingQuery(queryOptions, null as never);
+      queryManager.startPollingQuery({ query }, null as never);
     }).toThrow();
+
+    resolve();
   });
 
-  it('should correctly start polling queries', done => {
+  itAsync('should correctly start polling queries', (resolve, reject) => {
     const query = gql`
       query {
         author {
@@ -82,10 +83,9 @@ describe('QueryScheduler', () => {
     const link = mockSingleLink({
       request: queryOptions,
       result: { data },
-    });
+    }).setOnError(reject);
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
-
+      cache: new InMemoryCache({ addTypename: false }),
       link: link,
     });
     let timesFired = 0;
@@ -95,11 +95,11 @@ describe('QueryScheduler', () => {
     setTimeout(() => {
       expect(timesFired).toBeGreaterThanOrEqual(0);
       queryManager.stop();
-      done();
+      resolve();
     }, 120);
   });
 
-  it('should correctly stop polling queries', done => {
+  itAsync('should correctly stop polling queries', (resolve, reject) => {
     const query = gql`
       query {
         someAlias: author {
@@ -123,9 +123,9 @@ describe('QueryScheduler', () => {
         query: queryOptions.query,
       },
       result: { data },
-    });
+    }).setOnError(reject);
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link: link,
     });
     let timesFired = 0;
@@ -143,11 +143,11 @@ describe('QueryScheduler', () => {
     setTimeout(() => {
       expect(timesFired).toEqual(1);
       queryManager.stop();
-      done();
+      resolve();
     }, 170);
   });
 
-  it('should register a query and return an observable that can be unsubscribed', done => {
+  itAsync('should register a query and return an observable that can be unsubscribed', (resolve, reject) => {
     const myQuery = gql`
       query {
         someAuthorAlias: author {
@@ -169,10 +169,9 @@ describe('QueryScheduler', () => {
     const link = mockSingleLink({
       request: queryOptions,
       result: { data },
-    });
+    }).setOnError(reject);
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
-
+      cache: new InMemoryCache({ addTypename: false }),
       link,
     });
     let timesFired = 0;
@@ -188,11 +187,11 @@ describe('QueryScheduler', () => {
     setTimeout(() => {
       expect(timesFired).toEqual(1);
       queryManager.stop();
-      done();
+      resolve();
     }, 100);
   });
 
-  it('should register a query and return an observable that can adjust interval', done => {
+  itAsync('should register a query and return an observable that can adjust interval', (resolve, reject) => {
     const myQuery = gql`
       query {
         someAuthorAlias: author {
@@ -214,10 +213,10 @@ describe('QueryScheduler', () => {
     const link = mockSingleLink(
       { request: queryOptions, result: { data: data[0] } },
       { request: queryOptions, result: { data: data[1] } },
-      { request: queryOptions, result: { data: data[2] } },
-    );
+      { request: queryOptions, result: { data: data[2] } }
+    ).setOnError(reject);
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link,
     });
     let timesFired = 0;
@@ -241,11 +240,11 @@ describe('QueryScheduler', () => {
       expect(timesFired).toEqual(2);
       subscription.unsubscribe();
       queryManager.stop();
-      done();
+      resolve();
     }, 100);
   });
 
-  it('should handle network errors on polling queries correctly', done => {
+  itAsync('should handle network errors on polling queries correctly', (resolve, reject) => {
     const query = gql`
       query {
         author {
@@ -262,16 +261,16 @@ describe('QueryScheduler', () => {
     const link = mockSingleLink({
       request: queryOptions,
       error,
-    });
+    }).setOnError(reject);
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link,
     });
     let observableQuery = registerPollingQuery(queryManager, queryOptions);
     const subscription = observableQuery.subscribe({
       next() {
         queryManager.stop();
-        done.fail(
+        reject(
           new Error('Observer provided a result despite a network error.'),
         );
       },
@@ -283,12 +282,12 @@ describe('QueryScheduler', () => {
         });
         subscription.unsubscribe();
         queryManager.stop();
-        done();
+        resolve();
       },
     });
   });
 
-  it('should not fire another query if one with the same id is in flight', done => {
+  itAsync('should not fire another query if one with the same id is in flight', (resolve, reject) => {
     const query = gql`
       query B {
         fortuneCookie
@@ -305,9 +304,9 @@ describe('QueryScheduler', () => {
       request: queryOptions,
       result: { data },
       delay: 20000,
-    });
+    }).setOnError(reject);
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache()),
+      cache: new InMemoryCache(),
       link,
     });
     const observer = registerPollingQuery(queryManager, queryOptions);
@@ -315,11 +314,11 @@ describe('QueryScheduler', () => {
     setTimeout(() => {
       subscription.unsubscribe();
       queryManager.stop();
-      done();
+      resolve();
     }, 100);
   });
 
-  it('should add a query to an interval correctly', () => {
+  itAsync('should add a query to an interval correctly', (resolve, reject) => {
     const query = gql`
       query {
         fortuneCookie
@@ -335,9 +334,9 @@ describe('QueryScheduler', () => {
     const link = mockSingleLink({
       request: queryOptions,
       result: { data },
-    });
+    }).setOnError(reject);
     const queryManager = new QueryManager<any>({
-      store: new DataStore(new InMemoryCache()),
+      cache: new InMemoryCache(),
       link,
     });
     const queryId = 'fake-id';
@@ -351,9 +350,11 @@ describe('QueryScheduler', () => {
     });
     expect(count).toEqual(1);
     queryManager.stop();
+
+    resolve();
   });
 
-  it('should add multiple queries to an interval correctly', () => {
+  itAsync('should add multiple queries to an interval correctly', (resolve, reject) => {
     const query1 = gql`
       query {
         fortuneCookie
@@ -386,17 +387,14 @@ describe('QueryScheduler', () => {
       pollInterval: interval,
     };
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
-      link: mockSingleLink(
-        {
-          request: { query: query1 },
-          result: { data: data1 },
-        },
-        {
-          request: { query: query2 },
-          result: { data: data2 },
-        },
-      ),
+      cache: new InMemoryCache({ addTypename: false }),
+      link: mockSingleLink({
+        request: { query: query1 },
+        result: { data: data1 },
+      }, {
+        request: { query: query2 },
+        result: { data: data2 },
+      }).setOnError(reject),
     });
     const observable1 = registerPollingQuery(queryManager, queryOptions1);
     observable1.subscribe({
@@ -419,9 +417,11 @@ describe('QueryScheduler', () => {
     });
     expect(count).toEqual(2);
     queryManager.stop();
+
+    resolve();
   });
 
-  it('should remove queries from the interval list correctly', done => {
+  itAsync('should remove queries from the interval list correctly', (resolve, reject) => {
     const query = gql`
       query {
         author {
@@ -437,11 +437,11 @@ describe('QueryScheduler', () => {
       },
     };
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link: mockSingleLink({
         request: { query },
         result: { data },
-      }),
+      }).setOnError(reject),
     });
     let timesFired = 0;
     const observable = registerPollingQuery(queryManager, {
@@ -463,11 +463,11 @@ describe('QueryScheduler', () => {
     setTimeout(() => {
       expect(timesFired).toEqual(1);
       queryManager.stop();
-      done();
+      resolve();
     }, 100);
   });
 
-  it('should correctly start new polling query after removing old one', done => {
+  itAsync('should correctly start new polling query after removing old one', (resolve, reject) => {
     const query = gql`
       query {
         someAlias: author {
@@ -490,14 +490,9 @@ describe('QueryScheduler', () => {
       request: queryOptions,
       result: { data },
     };
-    const link = mockSingleLink(
-      networkResult,
-      networkResult,
-      networkResult,
-      networkResult,
-    );
+    const link = mockSingleLink(networkResult, networkResult, networkResult, networkResult).setOnError(reject);
     const queryManager = new QueryManager({
-      store: new DataStore(new InMemoryCache({ addTypename: false })),
+      cache: new InMemoryCache({ addTypename: false }),
       link: link,
     });
     let timesFired = 0;
@@ -515,7 +510,7 @@ describe('QueryScheduler', () => {
       setTimeout(() => {
         expect(timesFired).toBeGreaterThanOrEqual(1);
         queryManager.stop();
-        done();
+        resolve();
       }, 80);
     }, 200);
   });
diff --git a/packages/apollo-client/src/data/__tests__/queries.ts b/src/data/__tests__/queries.ts
similarity index 99%
rename from packages/apollo-client/src/data/__tests__/queries.ts
rename to src/data/__tests__/queries.ts
--- a/packages/apollo-client/src/data/__tests__/queries.ts
+++ b/src/data/__tests__/queries.ts
@@ -1,6 +1,7 @@
+import { DocumentNode } from 'graphql';
+
 import { QueryStore } from '../queries';
 import { NetworkStatus } from '../../core/networkStatus';
-import { DocumentNode } from 'graphql';
 
 describe('QueryStore', () => {
   const queryId = 'abc123';
diff --git a/packages/apollo-client/src/errors/__tests__/ApolloError.ts b/src/errors/__tests__/ApolloError.ts
similarity index 100%
rename from packages/apollo-client/src/errors/__tests__/ApolloError.ts
rename to src/errors/__tests__/ApolloError.ts
diff --git a/src/link/core/__tests__/ApolloLink.ts b/src/link/core/__tests__/ApolloLink.ts
new file mode 100644
--- /dev/null
+++ b/src/link/core/__tests__/ApolloLink.ts
@@ -0,0 +1,1052 @@
+import gql from 'graphql-tag';
+import { print } from 'graphql/language/printer';
+
+import { Observable } from '../../../utilities/observables/Observable';
+import { FetchResult, Operation, NextLink, GraphQLRequest } from '../types';
+import { ApolloLink } from '../ApolloLink';
+
+export class SetContextLink extends ApolloLink {
+  constructor(
+    private setContext: (
+      context: Record<string, any>,
+    ) => Record<string, any> = c => c,
+  ) {
+    super();
+  }
+
+  public request(
+    operation: Operation,
+    forward: NextLink,
+  ): Observable<FetchResult> {
+    operation.setContext(this.setContext(operation.getContext()));
+    return forward(operation);
+  }
+}
+
+export const sampleQuery = gql`
+  query SampleQuery {
+    stub {
+      id
+    }
+  }
+`;
+
+function checkCalls<T>(calls: any[] = [], results: Array<T>) {
+  expect(calls.length).toBe(results.length);
+  calls.map((call, i) => expect(call.data).toEqual(results[i]));
+}
+
+interface TestResultType {
+  link: ApolloLink;
+  results?: any[];
+  query?: string;
+  done?: () => void;
+  context?: any;
+  variables?: any;
+}
+
+export function testLinkResults(params: TestResultType) {
+  const { link, context, variables } = params;
+  const results = params.results || [];
+  const query = params.query || sampleQuery;
+  const done = params.done || (() => void 0);
+
+  const spy = jest.fn();
+  ApolloLink.execute(link, { query, context, variables }).subscribe({
+    next: spy,
+    error: error => {
+      expect(error).toEqual(results.pop());
+      checkCalls(spy.mock.calls[0], results);
+      if (done) {
+        done();
+      }
+    },
+    complete: () => {
+      checkCalls(spy.mock.calls[0], results);
+      if (done) {
+        done();
+      }
+    },
+  });
+}
+
+export const setContext = () => ({ add: 1 });
+
+describe('ApolloClient', () => {
+  describe('context', () => {
+    it('should merge context when using a function', done => {
+      const returnOne = new SetContextLink(setContext);
+      const mock = new ApolloLink((op, forward) => {
+        op.setContext(({ add }) => ({ add: add + 2 }));
+        op.setContext(() => ({ substract: 1 }));
+
+        return forward(op);
+      });
+      const link = returnOne.concat(mock).concat(op => {
+        expect(op.getContext()).toEqual({
+          add: 3,
+          substract: 1,
+        });
+        return Observable.of({ data: op.getContext().add });
+      });
+
+      testLinkResults({
+        link,
+        results: [3],
+        done,
+      });
+    });
+
+    it('should merge context when not using a function', done => {
+      const returnOne = new SetContextLink(setContext);
+      const mock = new ApolloLink((op, forward) => {
+        op.setContext({ add: 3 });
+        op.setContext({ substract: 1 });
+
+        return forward(op);
+      });
+      const link = returnOne.concat(mock).concat(op => {
+        expect(op.getContext()).toEqual({
+          add: 3,
+          substract: 1,
+        });
+        return Observable.of({ data: op.getContext().add });
+      });
+
+      testLinkResults({
+        link,
+        results: [3],
+        done,
+      });
+    });
+  });
+
+  describe('concat', () => {
+    it('should concat a function', done => {
+      const returnOne = new SetContextLink(setContext);
+      const link = returnOne.concat((operation, forward) => {
+        return Observable.of({ data: { count: operation.getContext().add } });
+      });
+
+      testLinkResults({
+        link,
+        results: [{ count: 1 }],
+        done,
+      });
+    });
+
+    it('should concat a Link', done => {
+      const returnOne = new SetContextLink(setContext);
+      const mock = new ApolloLink(op =>
+        Observable.of({ data: op.getContext().add }),
+      );
+      const link = returnOne.concat(mock);
+
+      testLinkResults({
+        link,
+        results: [1],
+        done,
+      });
+    });
+
+    it("should pass error to observable's error", done => {
+      const error = new Error('thrown');
+      const returnOne = new SetContextLink(setContext);
+      const mock = new ApolloLink(
+        op =>
+          new Observable(observer => {
+            observer.next({ data: op.getContext().add });
+            observer.error(error);
+          }),
+      );
+      const link = returnOne.concat(mock);
+
+      testLinkResults({
+        link,
+        results: [1, error],
+        done,
+      });
+    });
+
+    it('should concat a Link and function', done => {
+      const returnOne = new SetContextLink(setContext);
+      const mock = new ApolloLink((op, forward) => {
+        op.setContext(({ add }) => ({ add: add + 2 }));
+        return forward(op);
+      });
+      const link = returnOne.concat(mock).concat(op => {
+        return Observable.of({ data: op.getContext().add });
+      });
+
+      testLinkResults({
+        link,
+        results: [3],
+        done,
+      });
+    });
+
+    it('should concat a function and Link', done => {
+      const returnOne = new SetContextLink(setContext);
+      const mock = new ApolloLink((op, forward) =>
+        Observable.of({ data: op.getContext().add }),
+      );
+
+      const link = returnOne
+        .concat((operation, forward) => {
+          operation.setContext({
+            add: operation.getContext().add + 2,
+          });
+          return forward(operation);
+        })
+        .concat(mock);
+      testLinkResults({
+        link,
+        results: [3],
+        done,
+      });
+    });
+
+    it('should concat two functions', done => {
+      const returnOne = new SetContextLink(setContext);
+      const link = returnOne
+        .concat((operation, forward) => {
+          operation.setContext({
+            add: operation.getContext().add + 2,
+          });
+          return forward(operation);
+        })
+        .concat((op, forward) => Observable.of({ data: op.getContext().add }));
+      testLinkResults({
+        link,
+        results: [3],
+        done,
+      });
+    });
+
+    it('should concat two Links', done => {
+      const returnOne = new SetContextLink(setContext);
+      const mock1 = new ApolloLink((operation, forward) => {
+        operation.setContext({
+          add: operation.getContext().add + 2,
+        });
+        return forward(operation);
+      });
+      const mock2 = new ApolloLink((op, forward) =>
+        Observable.of({ data: op.getContext().add }),
+      );
+
+      const link = returnOne.concat(mock1).concat(mock2);
+      testLinkResults({
+        link,
+        results: [3],
+        done,
+      });
+    });
+
+    it("should return an link that can be concat'd multiple times", done => {
+      const returnOne = new SetContextLink(setContext);
+      const mock1 = new ApolloLink((operation, forward) => {
+        operation.setContext({
+          add: operation.getContext().add + 2,
+        });
+        return forward(operation);
+      });
+      const mock2 = new ApolloLink((op, forward) =>
+        Observable.of({ data: op.getContext().add + 2 }),
+      );
+      const mock3 = new ApolloLink((op, forward) =>
+        Observable.of({ data: op.getContext().add + 3 }),
+      );
+      const link = returnOne.concat(mock1);
+
+      testLinkResults({
+        link: link.concat(mock2),
+        results: [5],
+      });
+      testLinkResults({
+        link: link.concat(mock3),
+        results: [6],
+        done,
+      });
+    });
+  });
+
+  describe('empty', () => {
+    it('should returns an immediately completed Observable', done => {
+      testLinkResults({
+        link: ApolloLink.empty(),
+        done,
+      });
+    });
+  });
+
+  describe('execute', () => {
+    it('transforms an opearation with context into something serlizable', done => {
+      const query = gql`
+        {
+          id
+        }
+      `;
+      const link = new ApolloLink(operation => {
+        const str = JSON.stringify({
+          ...operation,
+          query: print(operation.query),
+        });
+
+        expect(str).toBe(
+          JSON.stringify({
+            variables: { id: 1 },
+            extensions: { cache: true },
+            operationName: null,
+            query: print(operation.query),
+          }),
+        );
+        return Observable.of();
+      });
+      const noop = () => {};
+      ApolloLink.execute(link, {
+        query,
+        variables: { id: 1 },
+        extensions: { cache: true },
+      }).subscribe(noop, noop, done);
+    });
+
+    describe('execute', () => {
+      let _warn: (message?: any, ...originalParams: any[]) => void;
+
+      beforeEach(() => {
+        _warn = console.warn;
+        console.warn = jest.fn(warning => {
+          expect(warning).toBe(`query should either be a string or GraphQL AST`);
+        });
+      });
+
+      afterEach(() => {
+        console.warn = _warn;
+      });
+
+      it('should return an empty observable when a link returns null', done => {
+        const link = new ApolloLink();
+        link.request = () => null;
+        testLinkResults({
+          link,
+          results: [],
+          done,
+        });
+      });
+
+      it('should return an empty observable when a link is empty', done => {
+        testLinkResults({
+          link: ApolloLink.empty(),
+          results: [],
+          done,
+        });
+      });
+
+      it("should return an empty observable when a concat'd link returns null", done => {
+        const link = new ApolloLink((operation, forward) => {
+          return forward(operation);
+        }).concat(() => null);
+        testLinkResults({
+          link,
+          results: [],
+          done,
+        });
+      });
+
+      it('should return an empty observable when a split link returns null', done => {
+        let context = { test: true };
+        const link = new SetContextLink(() => context).split(
+          op => op.getContext().test,
+          () => Observable.of(),
+          () => null,
+        );
+        testLinkResults({
+          link,
+          results: [],
+        });
+        context.test = false;
+        testLinkResults({
+          link,
+          results: [],
+          done,
+        });
+      });
+
+      it('should set a default context, variable, query and operationName on a copy of operation', done => {
+        const operation = {
+          query: gql`
+            {
+              id
+            }
+          `,
+        };
+        const link = new ApolloLink(op => {
+          expect(operation['operationName']).toBeUndefined();
+          expect(operation['variables']).toBeUndefined();
+          expect(operation['context']).toBeUndefined();
+          expect(operation['extensions']).toBeUndefined();
+          expect(op['operationName']).toBeDefined();
+          expect(op['variables']).toBeDefined();
+          expect(op['context']).toBeUndefined();
+          expect(op['extensions']).toBeDefined();
+          return Observable.of();
+        });
+
+        ApolloLink.execute(link, operation).subscribe({
+          complete: done,
+        });
+      });
+    })
+  });
+
+  describe('from', () => {
+    const uniqueOperation: GraphQLRequest = {
+      query: sampleQuery,
+      context: { name: 'uniqueName' },
+      operationName: 'SampleQuery',
+      extensions: {},
+    };
+
+    it('should create an observable that completes when passed an empty array', done => {
+      const observable = ApolloLink.execute(ApolloLink.from([]), {
+        query: sampleQuery,
+      });
+      observable.subscribe(() => expect(false), () => expect(false), done);
+    });
+
+    it('can create chain of one', () => {
+      expect(() => ApolloLink.from([new ApolloLink()])).not.toThrow();
+    });
+
+    it('can create chain of two', () => {
+      expect(() =>
+        ApolloLink.from([
+          new ApolloLink((operation, forward) => forward(operation)),
+          new ApolloLink(),
+        ]),
+      ).not.toThrow();
+    });
+
+    it('should receive result of one link', done => {
+      const data: FetchResult = {
+        data: {
+          hello: 'world',
+        },
+      };
+      const chain = ApolloLink.from([new ApolloLink(() => Observable.of(data))]);
+      // Smoke tests execute as a static method
+      const observable = ApolloLink.execute(chain, uniqueOperation);
+      observable.subscribe({
+        next: actualData => {
+          expect(data).toEqual(actualData);
+        },
+        error: () => {
+          throw new Error();
+        },
+        complete: () => done(),
+      });
+    });
+
+    it('should accept AST query and pass AST to link', () => {
+      const astOperation = {
+        ...uniqueOperation,
+        query: sampleQuery,
+      };
+
+      const stub = jest.fn();
+
+      const chain = ApolloLink.from([new ApolloLink(stub)]);
+      ApolloLink.execute(chain, astOperation);
+
+      expect(stub).toBeCalledWith({
+        query: sampleQuery,
+        operationName: 'SampleQuery',
+        variables: {},
+        extensions: {},
+      });
+    });
+
+    it('should pass operation from one link to next with modifications', done => {
+      const chain = ApolloLink.from([
+        new ApolloLink((op, forward) =>
+          forward({
+            ...op,
+            query: sampleQuery,
+          }),
+        ),
+        new ApolloLink(op => {
+          expect({
+            extensions: {},
+            operationName: 'SampleQuery',
+            query: sampleQuery,
+            variables: {},
+          }).toEqual(op);
+          return done();
+        }),
+      ]);
+      ApolloLink.execute(chain, uniqueOperation);
+    });
+
+    it('should pass result of one link to another with forward', done => {
+      const data: FetchResult = {
+        data: {
+          hello: 'world',
+        },
+      };
+
+      const chain = ApolloLink.from([
+        new ApolloLink((op, forward) => {
+          const observable = forward(op);
+
+          observable.subscribe({
+            next: actualData => {
+              expect(data).toEqual(actualData);
+            },
+            error: () => {
+              throw new Error();
+            },
+            complete: done,
+          });
+
+          return observable;
+        }),
+        new ApolloLink(() => Observable.of(data)),
+      ]);
+      ApolloLink.execute(chain, uniqueOperation);
+    });
+
+    it('should receive final result of two link chain', done => {
+      const data: FetchResult = {
+        data: {
+          hello: 'world',
+        },
+      };
+
+      const chain = ApolloLink.from([
+        new ApolloLink((op, forward) => {
+          const observable = forward(op);
+
+          return new Observable(observer => {
+            observable.subscribe({
+              next: actualData => {
+                expect(data).toEqual(actualData);
+                observer.next({
+                  data: {
+                    ...actualData.data,
+                    modification: 'unique',
+                  },
+                });
+              },
+              error: error => observer.error(error),
+              complete: () => observer.complete(),
+            });
+          });
+        }),
+        new ApolloLink(() => Observable.of(data)),
+      ]);
+
+      const result = ApolloLink.execute(chain, uniqueOperation);
+
+      result.subscribe({
+        next: modifiedData => {
+          expect({
+            data: {
+              ...data.data,
+              modification: 'unique',
+            },
+          }).toEqual(modifiedData);
+        },
+        error: () => {
+          throw new Error();
+        },
+        complete: done,
+      });
+    });
+
+    it('should chain together a function with links', done => {
+      const add1 = new ApolloLink((operation: Operation, forward: NextLink) => {
+        operation.setContext(({ num }) => ({ num: num + 1 }));
+        return forward(operation);
+      });
+      const add1Link = new ApolloLink((operation, forward) => {
+        operation.setContext(({ num }) => ({ num: num + 1 }));
+        return forward(operation);
+      });
+
+      const link = ApolloLink.from([
+        add1,
+        add1,
+        add1Link,
+        add1,
+        add1Link,
+        new ApolloLink(operation =>
+          Observable.of({ data: operation.getContext() }),
+        ),
+      ]);
+      testLinkResults({
+        link,
+        results: [{ num: 5 }],
+        context: { num: 0 },
+        done,
+      });
+    });
+  });
+
+  describe('split', () => {
+    it('should split two functions', done => {
+      const context = { add: 1 };
+      const returnOne = new SetContextLink(() => context);
+      const link1 = returnOne.concat((operation, forward) =>
+        Observable.of({ data: operation.getContext().add + 1 }),
+      );
+      const link2 = returnOne.concat((operation, forward) =>
+        Observable.of({ data: operation.getContext().add + 2 }),
+      );
+      const link = returnOne.split(
+        operation => operation.getContext().add === 1,
+        link1,
+        link2,
+      );
+
+      testLinkResults({
+        link,
+        results: [2],
+      });
+
+      context.add = 2;
+
+      testLinkResults({
+        link,
+        results: [4],
+        done,
+      });
+    });
+
+    it('should split two Links', done => {
+      const context = { add: 1 };
+      const returnOne = new SetContextLink(() => context);
+      const link1 = returnOne.concat(
+        new ApolloLink((operation, forward) =>
+          Observable.of({ data: operation.getContext().add + 1 }),
+        ),
+      );
+      const link2 = returnOne.concat(
+        new ApolloLink((operation, forward) =>
+          Observable.of({ data: operation.getContext().add + 2 }),
+        ),
+      );
+      const link = returnOne.split(
+        operation => operation.getContext().add === 1,
+        link1,
+        link2,
+      );
+
+      testLinkResults({
+        link,
+        results: [2],
+      });
+
+      context.add = 2;
+
+      testLinkResults({
+        link,
+        results: [4],
+        done,
+      });
+    });
+
+    it('should split a link and a function', done => {
+      const context = { add: 1 };
+      const returnOne = new SetContextLink(() => context);
+      const link1 = returnOne.concat((operation, forward) =>
+        Observable.of({ data: operation.getContext().add + 1 }),
+      );
+      const link2 = returnOne.concat(
+        new ApolloLink((operation, forward) =>
+          Observable.of({ data: operation.getContext().add + 2 }),
+        ),
+      );
+      const link = returnOne.split(
+        operation => operation.getContext().add === 1,
+        link1,
+        link2,
+      );
+
+      testLinkResults({
+        link,
+        results: [2],
+      });
+
+      context.add = 2;
+
+      testLinkResults({
+        link,
+        results: [4],
+        done,
+      });
+    });
+
+    it('should allow concat after split to be join', done => {
+      const context = { test: true, add: 1 };
+      const start = new SetContextLink(() => ({ ...context }));
+      const link = start
+        .split(
+          operation => operation.getContext().test,
+          (operation, forward) => {
+            operation.setContext(({ add }) => ({ add: add + 1 }));
+            return forward(operation);
+          },
+          (operation, forward) => {
+            operation.setContext(({ add }) => ({ add: add + 2 }));
+            return forward(operation);
+          },
+        )
+        .concat(operation =>
+          Observable.of({ data: operation.getContext().add }),
+        );
+
+      testLinkResults({
+        link,
+        context,
+        results: [2],
+      });
+
+      context.test = false;
+
+      testLinkResults({
+        link,
+        context,
+        results: [3],
+        done,
+      });
+    });
+
+    it('should allow default right to be empty or passthrough when forward available', done => {
+      let context = { test: true };
+      const start = new SetContextLink(() => context);
+      const link = start.split(
+        operation => operation.getContext().test,
+        operation =>
+          Observable.of({
+            data: {
+              count: 1,
+            },
+          }),
+      );
+      const concat = link.concat(operation =>
+        Observable.of({
+          data: {
+            count: 2,
+          },
+        }),
+      );
+
+      testLinkResults({
+        link,
+        results: [{ count: 1 }],
+      });
+
+      context.test = false;
+
+      testLinkResults({
+        link,
+        results: [],
+      });
+
+      testLinkResults({
+        link: concat,
+        results: [{ count: 2 }],
+        done,
+      });
+    });
+
+    it('should create filter when single link passed in', done => {
+      const link = ApolloLink.split(
+        operation => operation.getContext().test,
+        (operation, forward) => Observable.of({ data: { count: 1 } }),
+      );
+
+      let context = { test: true };
+
+      testLinkResults({
+        link,
+        results: [{ count: 1 }],
+        context,
+      });
+
+      context.test = false;
+
+      testLinkResults({
+        link,
+        results: [],
+        context,
+        done,
+      });
+    });
+
+    it('should split two functions', done => {
+      const link = ApolloLink.split(
+        operation => operation.getContext().test,
+        (operation, forward) => Observable.of({ data: { count: 1 } }),
+        (operation, forward) => Observable.of({ data: { count: 2 } }),
+      );
+
+      let context = { test: true };
+
+      testLinkResults({
+        link,
+        results: [{ count: 1 }],
+        context,
+      });
+
+      context.test = false;
+
+      testLinkResults({
+        link,
+        results: [{ count: 2 }],
+        context,
+        done,
+      });
+    });
+
+    it('should split two Links', done => {
+      const link = ApolloLink.split(
+        operation => operation.getContext().test,
+        (operation, forward) => Observable.of({ data: { count: 1 } }),
+        new ApolloLink((operation, forward) =>
+          Observable.of({ data: { count: 2 } }),
+        ),
+      );
+
+      let context = { test: true };
+
+      testLinkResults({
+        link,
+        results: [{ count: 1 }],
+        context,
+      });
+
+      context.test = false;
+
+      testLinkResults({
+        link,
+        results: [{ count: 2 }],
+        context,
+        done,
+      });
+    });
+
+    it('should split a link and a function', done => {
+      const link = ApolloLink.split(
+        operation => operation.getContext().test,
+        (operation, forward) => Observable.of({ data: { count: 1 } }),
+        new ApolloLink((operation, forward) =>
+          Observable.of({ data: { count: 2 } }),
+        ),
+      );
+
+      let context = { test: true };
+
+      testLinkResults({
+        link,
+        results: [{ count: 1 }],
+        context,
+      });
+
+      context.test = false;
+
+      testLinkResults({
+        link,
+        results: [{ count: 2 }],
+        context,
+        done,
+      });
+    });
+
+    it('should allow concat after split to be join', done => {
+      const context = { test: true };
+      const link = ApolloLink.split(
+        operation => operation.getContext().test,
+        (operation, forward) =>
+          forward(operation).map(data => ({
+            data: { count: data.data.count + 1 },
+          })),
+      ).concat(() => Observable.of({ data: { count: 1 } }));
+
+      testLinkResults({
+        link,
+        context,
+        results: [{ count: 2 }],
+      });
+
+      context.test = false;
+
+      testLinkResults({
+        link,
+        context,
+        results: [{ count: 1 }],
+        done,
+      });
+    });
+
+    it('should allow default right to be passthrough', done => {
+      const context = { test: true };
+      const link = ApolloLink.split(
+        operation => operation.getContext().test,
+        operation => Observable.of({ data: { count: 2 } }),
+      ).concat(operation => Observable.of({ data: { count: 1 } }));
+
+      testLinkResults({
+        link,
+        context,
+        results: [{ count: 2 }],
+      });
+
+      context.test = false;
+
+      testLinkResults({
+        link,
+        context,
+        results: [{ count: 1 }],
+        done,
+      });
+    });
+  });
+
+  describe('Terminating links', () => {
+    const _warn = console.warn;
+    const warningStub = jest.fn(warning => {
+      expect(warning.message).toBe(
+        `You are calling concat on a terminating link, which will have no effect`,
+      );
+    });
+    const data = {
+      stub: 'data',
+    };
+
+    beforeAll(() => {
+      console.warn = warningStub;
+    });
+
+    beforeEach(() => {
+      warningStub.mockClear();
+    });
+
+    afterAll(() => {
+      console.warn = _warn;
+    });
+
+    describe('split', () => {
+      it('should not warn if attempting to split a terminating and non-terminating Link', () => {
+        const split = ApolloLink.split(
+          () => true,
+          operation => Observable.of({ data }),
+          (operation, forward) => forward(operation),
+        );
+        split.concat((operation, forward) => forward(operation));
+        expect(warningStub).not.toBeCalled();
+      });
+
+      it('should warn if attempting to concat to split two terminating links', () => {
+        const split = ApolloLink.split(
+          () => true,
+          operation => Observable.of({ data }),
+          operation => Observable.of({ data }),
+        );
+        expect(split.concat((operation, forward) => forward(operation))).toEqual(
+          split,
+        );
+        expect(warningStub).toHaveBeenCalledTimes(1);
+      });
+
+      it('should warn if attempting to split to split two terminating links', () => {
+        const split = ApolloLink.split(
+          () => true,
+          operation => Observable.of({ data }),
+          operation => Observable.of({ data }),
+        );
+        expect(
+          split.split(
+            () => true,
+            (operation, forward) => forward(operation),
+            (operation, forward) => forward(operation),
+          ),
+        ).toEqual(split);
+        expect(warningStub).toHaveBeenCalledTimes(1);
+      });
+    });
+
+    describe('from', () => {
+      it('should not warn if attempting to form a terminating then non-terminating Link', () => {
+        ApolloLink.from([
+          (operation, forward) => forward(operation),
+          operation => Observable.of({ data }),
+        ]);
+        expect(warningStub).not.toBeCalled();
+      });
+
+      it('should warn if attempting to add link after termination', () => {
+        ApolloLink.from([
+          (operation, forward) => forward(operation),
+          operation => Observable.of({ data }),
+          (operation, forward) => forward(operation),
+        ]);
+        expect(warningStub).toHaveBeenCalledTimes(1);
+      });
+
+      it('should warn if attempting to add link after termination', () => {
+        ApolloLink.from([
+          new ApolloLink((operation, forward) => forward(operation)),
+          new ApolloLink(operation => Observable.of({ data })),
+          new ApolloLink((operation, forward) => forward(operation)),
+        ]);
+        expect(warningStub).toHaveBeenCalledTimes(1);
+      });
+    });
+
+    describe('concat', () => {
+      it('should warn if attempting to concat to a terminating Link from function', () => {
+        const link = new ApolloLink(operation => Observable.of({ data }));
+        expect(ApolloLink.concat(link, (operation, forward) => forward(operation))).toEqual(
+          link,
+        );
+        expect(warningStub).toHaveBeenCalledTimes(1);
+        expect(warningStub.mock.calls[0][0].link).toEqual(link);
+      });
+
+      it('should warn if attempting to concat to a terminating Link', () => {
+        const link = new ApolloLink(operation => Observable.of());
+        expect(link.concat((operation, forward) => forward(operation))).toEqual(
+          link,
+        );
+        expect(warningStub).toHaveBeenCalledTimes(1);
+        expect(warningStub.mock.calls[0][0].link).toEqual(link);
+      });
+
+      it('should not warn if attempting concat a terminating Link at end', () => {
+        const link = new ApolloLink((operation, forward) => forward(operation));
+        link.concat(operation => Observable.of());
+        expect(warningStub).not.toBeCalled();
+      });
+    });
+
+    describe('warning', () => {
+      it('should include link that terminates', () => {
+        const terminatingLink = new ApolloLink(operation =>
+          Observable.of({ data }),
+        );
+        ApolloLink.from([
+          new ApolloLink((operation, forward) => forward(operation)),
+          new ApolloLink((operation, forward) => forward(operation)),
+          terminatingLink,
+          new ApolloLink((operation, forward) => forward(operation)),
+          new ApolloLink((operation, forward) => forward(operation)),
+          new ApolloLink(operation => Observable.of({ data })),
+          new ApolloLink((operation, forward) => forward(operation)),
+        ]);
+        expect(warningStub).toHaveBeenCalledTimes(4);
+      });
+    });
+  });
+});
diff --git a/src/link/http/__tests__/HttpLink.ts b/src/link/http/__tests__/HttpLink.ts
new file mode 100644
--- /dev/null
+++ b/src/link/http/__tests__/HttpLink.ts
@@ -0,0 +1,1112 @@
+import gql from 'graphql-tag';
+import fetchMock from 'fetch-mock';
+import { print } from 'graphql';
+
+import { Observable } from '../../../utilities/observables/Observable';
+import { ApolloLink } from '../../core/ApolloLink';
+import { execute } from '../../core/execute';
+import { HttpLink } from '../HttpLink';
+import { createHttpLink } from '../createHttpLink';
+
+const sampleQuery = gql`
+  query SampleQuery {
+    stub {
+      id
+    }
+  }
+`;
+
+const sampleMutation = gql`
+  mutation SampleMutation {
+    stub {
+      id
+    }
+  }
+`;
+
+const makeCallback = (done, body) => {
+  return (...args) => {
+    try {
+      body(...args);
+      done();
+    } catch (error) {
+      done.fail(error);
+    }
+  };
+};
+
+const convertBatchedBody = body => {
+  const parsed = JSON.parse(body);
+  return parsed;
+};
+
+const makePromise =
+  res => new Promise((resolve) => setTimeout(() => resolve(res)));
+
+describe('HttpLink', () => {
+  describe('General', () => {
+    const data = { data: { hello: 'world' } };
+    const data2 = { data: { hello: 'everyone' } };
+    const mockError = { throws: new TypeError('mock me') };
+    let subscriber;
+
+    beforeEach(() => {
+      fetchMock.restore();
+      fetchMock.post('begin:/data2', makePromise(data2));
+      fetchMock.post('begin:/data', makePromise(data));
+      fetchMock.post('begin:/error', mockError);
+      fetchMock.post('begin:/apollo', makePromise(data));
+
+      fetchMock.get('begin:/data', makePromise(data));
+      fetchMock.get('begin:/data2', makePromise(data2));
+
+      const next = jest.fn();
+      const error = jest.fn();
+      const complete = jest.fn();
+
+      subscriber = {
+        next,
+        error,
+        complete,
+      };
+    });
+
+    afterEach(() => {
+      fetchMock.restore();
+    });
+
+    it('does not need any constructor arguments', () => {
+      expect(() => new HttpLink()).not.toThrow();
+    });
+
+    it('constructor creates link that can call next and then complete', done => {
+      const next = jest.fn();
+      const link = new HttpLink({ uri: '/data' });
+      const observable = execute(link, {
+        query: sampleQuery,
+      });
+      observable.subscribe({
+        next,
+        error: error => expect(false),
+        complete: () => {
+          expect(next).toHaveBeenCalledTimes(1);
+          done();
+        },
+      });
+    });
+
+    it('supports using a GET request', done => {
+      const variables = { params: 'stub' };
+      const extensions = { myExtension: 'foo' };
+
+      const link = createHttpLink({
+        uri: '/data',
+        fetchOptions: { method: 'GET' },
+        includeExtensions: true,
+      });
+
+      execute(link, { query: sampleQuery, variables, extensions }).subscribe({
+        next: makeCallback(done, result => {
+          const [uri, options] = fetchMock.lastCall();
+          const { method, body } = options;
+          expect(body).toBeUndefined();
+          expect(method).toBe('GET');
+          expect(uri).toBe(
+            '/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D%0A&operationName=SampleQuery&variables=%7B%22params%22%3A%22stub%22%7D&extensions=%7B%22myExtension%22%3A%22foo%22%7D',
+          );
+        }),
+        error: error => done.fail(error),
+      });
+    });
+
+    it('supports using a GET request with search', done => {
+      const variables = { params: 'stub' };
+
+      const link = createHttpLink({
+        uri: '/data?foo=bar',
+        fetchOptions: { method: 'GET' },
+      });
+
+      execute(link, { query: sampleQuery, variables }).subscribe({
+        next: makeCallback(done, result => {
+          const [uri, options] = fetchMock.lastCall();
+          const { method, body } = options;
+          expect(body).toBeUndefined();
+          expect(method).toBe('GET');
+          expect(uri).toBe(
+            '/data?foo=bar&query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D%0A&operationName=SampleQuery&variables=%7B%22params%22%3A%22stub%22%7D',
+          );
+        }),
+        error: error => done.fail(error),
+      });
+    });
+
+    it('supports using a GET request on the context', done => {
+      const variables = { params: 'stub' };
+      const link = createHttpLink({
+        uri: '/data',
+      });
+
+      execute(link, {
+        query: sampleQuery,
+        variables,
+        context: {
+          fetchOptions: { method: 'GET' },
+        },
+      }).subscribe(
+        makeCallback(done, result => {
+          const [uri, options] = fetchMock.lastCall();
+          const { method, body } = options;
+          expect(body).toBeUndefined();
+          expect(method).toBe('GET');
+          expect(uri).toBe(
+            '/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D%0A&operationName=SampleQuery&variables=%7B%22params%22%3A%22stub%22%7D',
+          );
+        }),
+      );
+    });
+
+    it('uses GET with useGETForQueries', done => {
+      const variables = { params: 'stub' };
+      const link = createHttpLink({
+        uri: '/data',
+        useGETForQueries: true,
+      });
+
+      execute(link, {
+        query: sampleQuery,
+        variables,
+      }).subscribe(
+        makeCallback(done, result => {
+          const [uri, options] = fetchMock.lastCall();
+          const { method, body } = options;
+          expect(body).toBeUndefined();
+          expect(method).toBe('GET');
+          expect(uri).toBe(
+            '/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D%0A&operationName=SampleQuery&variables=%7B%22params%22%3A%22stub%22%7D',
+          );
+        }),
+      );
+    });
+
+    it('uses POST for mutations with useGETForQueries', done => {
+      const variables = { params: 'stub' };
+      const link = createHttpLink({
+        uri: '/data',
+        useGETForQueries: true,
+      });
+
+      execute(link, {
+        query: sampleMutation,
+        variables,
+      }).subscribe(
+        makeCallback(done, result => {
+          const [uri, options] = fetchMock.lastCall();
+          const { method, body } = options;
+          expect(body).toBeDefined();
+          expect(method).toBe('POST');
+          expect(uri).toBe('/data');
+        }),
+      );
+    });
+
+    it('should add client awareness settings to request headers', done => {
+      const variables = { params: 'stub' };
+      const link = createHttpLink({
+        uri: '/data',
+      });
+
+      const clientAwareness = {
+        name: 'Some Client Name',
+        version: '1.0.1',
+      };
+
+      execute(link, {
+        query: sampleQuery,
+        variables,
+        context: {
+          clientAwareness,
+        },
+      }).subscribe(
+        makeCallback(done, result => {
+          const [uri, options] = fetchMock.lastCall();
+          const { headers } = options;
+          expect(headers['apollographql-client-name']).toBeDefined();
+          expect(headers['apollographql-client-name']).toEqual(
+            clientAwareness.name,
+          );
+          expect(headers['apollographql-client-version']).toBeDefined();
+          expect(headers['apollographql-client-version']).toEqual(
+            clientAwareness.version,
+          );
+        }),
+      );
+    });
+
+    it('should not add empty client awareness settings to request headers', done => {
+      const variables = { params: 'stub' };
+      const link = createHttpLink({
+        uri: '/data',
+      });
+
+      const hasOwn = Object.prototype.hasOwnProperty;
+      const clientAwareness = {};
+      execute(link, {
+        query: sampleQuery,
+        variables,
+        context: {
+          clientAwareness,
+        },
+      }).subscribe(
+        makeCallback(done, result => {
+          const [uri, options] = fetchMock.lastCall();
+          const { headers } = options;
+          expect(hasOwn.call(headers, 'apollographql-client-name')).toBe(false);
+          expect(hasOwn.call(headers, 'apollographql-client-version')).toBe(
+            false,
+          );
+        }),
+      );
+    });
+
+    it("throws for GET if the variables can't be stringified", done => {
+      const link = createHttpLink({
+        uri: '/data',
+        useGETForQueries: true,
+      });
+
+      let b;
+      const a = { b };
+      b = { a };
+      a.b = b;
+      const variables = {
+        a,
+        b,
+      };
+      execute(link, { query: sampleQuery, variables }).subscribe(
+        result => {
+          done.fail('next should have been thrown from the link');
+        },
+        makeCallback(done, e => {
+          expect(e.message).toMatch(/Variables map is not serializable/);
+          expect(e.parseError.message).toMatch(
+            /Converting circular structure to JSON/,
+          );
+        }),
+      );
+    });
+
+    it("throws for GET if the extensions can't be stringified", done => {
+      const link = createHttpLink({
+        uri: '/data',
+        useGETForQueries: true,
+        includeExtensions: true,
+      });
+
+      let b;
+      const a = { b };
+      b = { a };
+      a.b = b;
+      const extensions = {
+        a,
+        b,
+      };
+      execute(link, { query: sampleQuery, extensions }).subscribe(
+        result => {
+          done.fail('next should have been thrown from the link');
+        },
+        makeCallback(done, e => {
+          expect(e.message).toMatch(/Extensions map is not serializable/);
+          expect(e.parseError.message).toMatch(
+            /Converting circular structure to JSON/,
+          );
+        }),
+      );
+    });
+
+    it('raises warning if called with concat', () => {
+      const link = createHttpLink();
+      const _warn = console.warn;
+      console.warn = warning => expect(warning['message']).toBeDefined();
+      expect(link.concat((operation, forward) => forward(operation))).toEqual(
+        link,
+      );
+      console.warn = _warn;
+    });
+
+    it('does not need any constructor arguments', () => {
+      expect(() => createHttpLink()).not.toThrow();
+    });
+
+    it('calls next and then complete', done => {
+      const next = jest.fn();
+      const link = createHttpLink({ uri: 'data' });
+      const observable = execute(link, {
+        query: sampleQuery,
+      });
+      observable.subscribe({
+        next,
+        error: error => done.fail(error),
+        complete: makeCallback(done, () => {
+          expect(next).toHaveBeenCalledTimes(1);
+        }),
+      });
+    });
+
+    it('calls error when fetch fails', done => {
+      const link = createHttpLink({ uri: 'error' });
+      const observable = execute(link, {
+        query: sampleQuery,
+      });
+      observable.subscribe(
+        result => done.fail('next should not have been called'),
+        makeCallback(done, error => {
+          expect(error).toEqual(mockError.throws);
+        }),
+        () => done.fail('complete should not have been called'),
+      );
+    });
+
+    it('calls error when fetch fails', done => {
+      const link = createHttpLink({ uri: 'error' });
+      const observable = execute(link, {
+        query: sampleMutation,
+      });
+      observable.subscribe(
+        result => done.fail('next should not have been called'),
+        makeCallback(done, error => {
+          expect(error).toEqual(mockError.throws);
+        }),
+        () => done.fail('complete should not have been called'),
+      );
+    });
+
+    it('unsubscribes without calling subscriber', done => {
+      const link = createHttpLink({ uri: 'data' });
+      const observable = execute(link, {
+        query: sampleQuery,
+      });
+      const subscription = observable.subscribe(
+        result => done.fail('next should not have been called'),
+        error => done.fail(error),
+        () => done.fail('complete should not have been called'),
+      );
+      subscription.unsubscribe();
+      expect(subscription.closed).toBe(true);
+      setTimeout(done, 50);
+    });
+
+    const verifyRequest = (
+      link: ApolloLink,
+      after: () => void,
+      includeExtensions: boolean,
+      done: any,
+    ) => {
+      const next = jest.fn();
+      const context = { info: 'stub' };
+      const variables = { params: 'stub' };
+
+      const observable = execute(link, {
+        query: sampleMutation,
+        context,
+        variables,
+      });
+      observable.subscribe({
+        next,
+        error: error => done.fail(error),
+        complete: () => {
+          try {
+            let body = convertBatchedBody(fetchMock.lastCall()[1].body);
+            expect(body.query).toBe(print(sampleMutation));
+            expect(body.variables).toEqual(variables);
+            expect(body.context).not.toBeDefined();
+            if (includeExtensions) {
+              expect(body.extensions).toBeDefined();
+            } else {
+              expect(body.extensions).not.toBeDefined();
+            }
+            expect(next).toHaveBeenCalledTimes(1);
+
+            after();
+          } catch (e) {
+            done.fail(e);
+          }
+        },
+      });
+    };
+
+    it('passes all arguments to multiple fetch body including extensions', done => {
+      debugger;
+      const link = createHttpLink({ uri: 'data', includeExtensions: true });
+      verifyRequest(
+        link,
+        () => verifyRequest(link, done, true, done),
+        true,
+        done,
+      );
+    });
+
+    it('passes all arguments to multiple fetch body excluding extensions', done => {
+      const link = createHttpLink({ uri: 'data' });
+      verifyRequest(
+        link,
+        () => verifyRequest(link, done, false, done),
+        false,
+        done,
+      );
+    });
+
+    it('calls multiple subscribers', done => {
+      const link = createHttpLink({ uri: 'data' });
+      const context = { info: 'stub' };
+      const variables = { params: 'stub' };
+
+      const observable = execute(link, {
+        query: sampleMutation,
+        context,
+        variables,
+      });
+      observable.subscribe(subscriber);
+      observable.subscribe(subscriber);
+
+      setTimeout(() => {
+        expect(subscriber.next).toHaveBeenCalledTimes(2);
+        expect(subscriber.complete).toHaveBeenCalledTimes(2);
+        expect(subscriber.error).not.toHaveBeenCalled();
+        done();
+      }, 50);
+    });
+
+    it('calls remaining subscribers after unsubscribe', done => {
+      const link = createHttpLink({ uri: 'data' });
+      const context = { info: 'stub' };
+      const variables = { params: 'stub' };
+
+      const observable = execute(link, {
+        query: sampleMutation,
+        context,
+        variables,
+      });
+
+      observable.subscribe(subscriber);
+
+      setTimeout(() => {
+        const subscription = observable.subscribe(subscriber);
+        subscription.unsubscribe();
+      }, 10);
+
+      setTimeout(
+        makeCallback(done, () => {
+          expect(subscriber.next).toHaveBeenCalledTimes(1);
+          expect(subscriber.complete).toHaveBeenCalledTimes(1);
+          expect(subscriber.error).not.toHaveBeenCalled();
+          done();
+        }),
+        50,
+      );
+    });
+
+    it('allows for dynamic endpoint setting', done => {
+      const variables = { params: 'stub' };
+      const link = createHttpLink({ uri: 'data' });
+
+      execute(link, {
+        query: sampleQuery,
+        variables,
+        context: { uri: 'data2' },
+      }).subscribe(result => {
+        expect(result).toEqual(data2);
+        done();
+      });
+    });
+
+    it('adds headers to the request from the context', done => {
+      const variables = { params: 'stub' };
+      const middleware = new ApolloLink((operation, forward) => {
+        operation.setContext({
+          headers: { authorization: '1234' },
+        });
+        return forward(operation).map(result => {
+          const { headers } = operation.getContext();
+          try {
+            expect(headers).toBeDefined();
+          } catch (e) {
+            done.fail(e);
+          }
+          return result;
+        });
+      });
+      const link = middleware.concat(createHttpLink({ uri: 'data' }));
+
+      execute(link, { query: sampleQuery, variables }).subscribe(
+        makeCallback(done, result => {
+          const headers = fetchMock.lastCall()[1].headers;
+          expect(headers.authorization).toBe('1234');
+          expect(headers['content-type']).toBe('application/json');
+          expect(headers.accept).toBe('*/*');
+        }),
+      );
+    });
+
+    it('adds headers to the request from the setup', done => {
+      const variables = { params: 'stub' };
+      const link = createHttpLink({
+        uri: 'data',
+        headers: { authorization: '1234' },
+      });
+
+      execute(link, { query: sampleQuery, variables }).subscribe(
+        makeCallback(done, result => {
+          const headers = fetchMock.lastCall()[1].headers;
+          expect(headers.authorization).toBe('1234');
+          expect(headers['content-type']).toBe('application/json');
+          expect(headers.accept).toBe('*/*');
+        }),
+      );
+    });
+
+    it('prioritizes context headers over setup headers', done => {
+      const variables = { params: 'stub' };
+      const middleware = new ApolloLink((operation, forward) => {
+        operation.setContext({
+          headers: { authorization: '1234' },
+        });
+        return forward(operation);
+      });
+      const link = middleware.concat(
+        createHttpLink({ uri: 'data', headers: { authorization: 'no user' } }),
+      );
+
+      execute(link, { query: sampleQuery, variables }).subscribe(
+        makeCallback(done, result => {
+          const headers = fetchMock.lastCall()[1].headers;
+          expect(headers.authorization).toBe('1234');
+          expect(headers['content-type']).toBe('application/json');
+          expect(headers.accept).toBe('*/*');
+        }),
+      );
+    });
+
+    it('adds headers to the request from the context on an operation', done => {
+      const variables = { params: 'stub' };
+      const link = createHttpLink({ uri: 'data' });
+
+      const context = {
+        headers: { authorization: '1234' },
+      };
+      execute(link, {
+        query: sampleQuery,
+        variables,
+        context,
+      }).subscribe(
+        makeCallback(done, result => {
+          const headers = fetchMock.lastCall()[1].headers;
+          expect(headers.authorization).toBe('1234');
+          expect(headers['content-type']).toBe('application/json');
+          expect(headers.accept).toBe('*/*');
+        }),
+      );
+    });
+
+    it('adds creds to the request from the context', done => {
+      const variables = { params: 'stub' };
+      const middleware = new ApolloLink((operation, forward) => {
+        operation.setContext({
+          credentials: 'same-team-yo',
+        });
+        return forward(operation);
+      });
+      const link = middleware.concat(createHttpLink({ uri: 'data' }));
+
+      execute(link, { query: sampleQuery, variables }).subscribe(
+        makeCallback(done, result => {
+          const creds = fetchMock.lastCall()[1].credentials;
+          expect(creds).toBe('same-team-yo');
+        }),
+      );
+    });
+
+    it('adds creds to the request from the setup', done => {
+      const variables = { params: 'stub' };
+      const link = createHttpLink({ uri: 'data', credentials: 'same-team-yo' });
+
+      execute(link, { query: sampleQuery, variables }).subscribe(
+        makeCallback(done, result => {
+          const creds = fetchMock.lastCall()[1].credentials;
+          expect(creds).toBe('same-team-yo');
+        }),
+      );
+    });
+
+    it('prioritizes creds from the context over the setup', done => {
+      const variables = { params: 'stub' };
+      const middleware = new ApolloLink((operation, forward) => {
+        operation.setContext({
+          credentials: 'same-team-yo',
+        });
+        return forward(operation);
+      });
+      const link = middleware.concat(
+        createHttpLink({ uri: 'data', credentials: 'error' }),
+      );
+
+      execute(link, { query: sampleQuery, variables }).subscribe(
+        makeCallback(done, result => {
+          const creds = fetchMock.lastCall()[1].credentials;
+          expect(creds).toBe('same-team-yo');
+        }),
+      );
+    });
+
+    it('adds uri to the request from the context', done => {
+      const variables = { params: 'stub' };
+      const middleware = new ApolloLink((operation, forward) => {
+        operation.setContext({
+          uri: 'data',
+        });
+        return forward(operation);
+      });
+      const link = middleware.concat(createHttpLink());
+
+      execute(link, { query: sampleQuery, variables }).subscribe(
+        makeCallback(done, result => {
+          const uri = fetchMock.lastUrl();
+          expect(uri).toBe('/data');
+        }),
+      );
+    });
+
+    it('adds uri to the request from the setup', done => {
+      const variables = { params: 'stub' };
+      const link = createHttpLink({ uri: 'data' });
+
+      execute(link, { query: sampleQuery, variables }).subscribe(
+        makeCallback(done, result => {
+          const uri = fetchMock.lastUrl();
+          expect(uri).toBe('/data');
+        }),
+      );
+    });
+
+    it('prioritizes context uri over setup uri', done => {
+      const variables = { params: 'stub' };
+      const middleware = new ApolloLink((operation, forward) => {
+        operation.setContext({
+          uri: 'apollo',
+        });
+        return forward(operation);
+      });
+      const link = middleware.concat(
+        createHttpLink({ uri: 'data', credentials: 'error' }),
+      );
+
+      execute(link, { query: sampleQuery, variables }).subscribe(
+        makeCallback(done, result => {
+          const uri = fetchMock.lastUrl();
+
+          expect(uri).toBe('/apollo');
+        }),
+      );
+    });
+
+    it('allows uri to be a function', done => {
+      const variables = { params: 'stub' };
+      const customFetch = (uri, options) => {
+        const { operationName } = convertBatchedBody(options.body);
+        try {
+          expect(operationName).toBe('SampleQuery');
+        } catch (e) {
+          done.fail(e);
+        }
+        return fetch('dataFunc', options);
+      };
+
+      const link = createHttpLink({ fetch: customFetch });
+
+      execute(link, { query: sampleQuery, variables }).subscribe(
+        makeCallback(done, result => {
+          const uri = fetchMock.lastUrl();
+          expect(fetchMock.lastUrl()).toBe('/dataFunc');
+        }),
+      );
+    });
+
+    it('adds fetchOptions to the request from the setup', done => {
+      const variables = { params: 'stub' };
+      const link = createHttpLink({
+        uri: 'data',
+        fetchOptions: { someOption: 'foo', mode: 'no-cors' },
+      });
+
+      execute(link, { query: sampleQuery, variables }).subscribe(
+        makeCallback(done, result => {
+          const { someOption, mode, headers } = fetchMock.lastCall()[1];
+          expect(someOption).toBe('foo');
+          expect(mode).toBe('no-cors');
+          expect(headers['content-type']).toBe('application/json');
+        }),
+      );
+    });
+
+    it('adds fetchOptions to the request from the context', done => {
+      const variables = { params: 'stub' };
+      const middleware = new ApolloLink((operation, forward) => {
+        operation.setContext({
+          fetchOptions: {
+            someOption: 'foo',
+          },
+        });
+        return forward(operation);
+      });
+      const link = middleware.concat(createHttpLink({ uri: 'data' }));
+
+      execute(link, { query: sampleQuery, variables }).subscribe(
+        makeCallback(done, result => {
+          const { someOption } = fetchMock.lastCall()[1];
+          expect(someOption).toBe('foo');
+          done();
+        }),
+      );
+    });
+
+    it('prioritizes context over setup', done => {
+      const variables = { params: 'stub' };
+      const middleware = new ApolloLink((operation, forward) => {
+        operation.setContext({
+          fetchOptions: {
+            someOption: 'foo',
+          },
+        });
+        return forward(operation);
+      });
+      const link = middleware.concat(
+        createHttpLink({ uri: 'data', fetchOptions: { someOption: 'bar' } }),
+      );
+
+      execute(link, { query: sampleQuery, variables }).subscribe(
+        makeCallback(done, result => {
+          const { someOption } = fetchMock.lastCall()[1];
+          expect(someOption).toBe('foo');
+        }),
+      );
+    });
+
+    it('allows for not sending the query with the request', done => {
+      const variables = { params: 'stub' };
+      const middleware = new ApolloLink((operation, forward) => {
+        operation.setContext({
+          http: {
+            includeQuery: false,
+            includeExtensions: true,
+          },
+        });
+        operation.extensions.persistedQuery = { hash: '1234' };
+        return forward(operation);
+      });
+      const link = middleware.concat(createHttpLink({ uri: 'data' }));
+
+      execute(link, { query: sampleQuery, variables }).subscribe(
+        makeCallback(done, result => {
+          let body = convertBatchedBody(fetchMock.lastCall()[1].body);
+
+          expect(body.query).not.toBeDefined();
+          expect(body.extensions).toEqual({ persistedQuery: { hash: '1234' } });
+          done();
+        }),
+      );
+    });
+
+    it('sets the raw response on context', done => {
+      const middleware = new ApolloLink((operation, forward) => {
+        return new Observable(ob => {
+          const op = forward(operation);
+          const sub = op.subscribe({
+            next: ob.next.bind(ob),
+            error: ob.error.bind(ob),
+            complete: makeCallback(done, e => {
+              expect(operation.getContext().response.headers.toBeDefined);
+              ob.complete();
+            }),
+          });
+
+          return () => {
+            sub.unsubscribe();
+          };
+        });
+      });
+
+      const link = middleware.concat(createHttpLink({ uri: 'data', fetch }));
+
+      execute(link, { query: sampleQuery }).subscribe(
+        result => {
+          done();
+        },
+        () => {},
+      );
+    });
+  });
+
+  describe('Dev warnings', () => {
+    let oldFetch;
+    beforeEach(() => {
+      oldFetch = window.fetch;
+      delete window.fetch;
+    });
+
+    afterEach(() => {
+      window.fetch = oldFetch;
+    });
+
+    it('warns if fetch is undeclared', done => {
+      try {
+        const link = createHttpLink({ uri: 'data' });
+        done.fail("warning wasn't called");
+      } catch (e) {
+        makeCallback(done, () =>
+          expect(e.message).toMatch(/has not been found globally/),
+        )();
+      }
+    });
+
+    it('warns if fetch is undefined', done => {
+      window.fetch = undefined;
+      try {
+        const link = createHttpLink({ uri: 'data' });
+        done.fail("warning wasn't called");
+      } catch (e) {
+        makeCallback(done, () =>
+          expect(e.message).toMatch(/has not been found globally/),
+        )();
+      }
+    });
+
+    it('does not warn if fetch is undeclared but a fetch is passed', () => {
+      expect(() => {
+        const link = createHttpLink({ uri: 'data', fetch: () => {} });
+      }).not.toThrow();
+    });
+  });
+
+  describe('Error handling', () => {
+    let responseBody;
+    const text = jest.fn(() => {
+      const responseBodyText = '{}';
+      responseBody = JSON.parse(responseBodyText);
+      return Promise.resolve(responseBodyText);
+    });
+    const textWithData = jest.fn(() => {
+      responseBody = {
+        data: { stub: { id: 1 } },
+        errors: [{ message: 'dangit' }],
+      };
+
+      return Promise.resolve(JSON.stringify(responseBody));
+    });
+
+    const textWithErrors = jest.fn(() => {
+      responseBody = {
+        errors: [{ message: 'dangit' }],
+      };
+
+      return Promise.resolve(JSON.stringify(responseBody));
+    });
+    const fetch = jest.fn((uri, options) => {
+      return Promise.resolve({ text });
+    });
+    beforeEach(() => {
+      fetch.mockReset();
+    });
+    it('makes it easy to do stuff on a 401', done => {
+      const middleware = new ApolloLink((operation, forward) => {
+        return new Observable(ob => {
+          fetch.mockReturnValueOnce(Promise.resolve({ status: 401, text }));
+          const op = forward(operation);
+          const sub = op.subscribe({
+            next: ob.next.bind(ob),
+            error: makeCallback(done, e => {
+              expect(e.message).toMatch(/Received status code 401/);
+              expect(e.statusCode).toEqual(401);
+              ob.error(e);
+            }),
+            complete: ob.complete.bind(ob),
+          });
+
+          return () => {
+            sub.unsubscribe();
+          };
+        });
+      });
+
+      const link = middleware.concat(createHttpLink({ uri: 'data', fetch }));
+
+      execute(link, { query: sampleQuery }).subscribe(
+        result => {
+          done.fail('next should have been thrown from the network');
+        },
+        () => {},
+      );
+    });
+
+    it('throws an error if response code is > 300', done => {
+      fetch.mockReturnValueOnce(Promise.resolve({ status: 400, text }));
+      const link = createHttpLink({ uri: 'data', fetch });
+
+      execute(link, { query: sampleQuery }).subscribe(
+        result => {
+          done.fail('next should have been thrown from the network');
+        },
+        makeCallback(done, e => {
+          expect(e.message).toMatch(/Received status code 400/);
+          expect(e.statusCode).toBe(400);
+          expect(e.result).toEqual(responseBody);
+        }),
+      );
+    });
+    it('throws an error if response code is > 300 and returns data', done => {
+      fetch.mockReturnValueOnce(
+        Promise.resolve({ status: 400, text: textWithData }),
+      );
+
+      const link = createHttpLink({ uri: 'data', fetch });
+
+      let called = false;
+
+      execute(link, { query: sampleQuery }).subscribe(
+        result => {
+          called = true;
+          expect(result).toEqual(responseBody);
+        },
+        e => {
+          expect(called).toBe(true);
+          expect(e.message).toMatch(/Received status code 400/);
+          expect(e.statusCode).toBe(400);
+          expect(e.result).toEqual(responseBody);
+          done();
+        },
+      );
+    });
+    it('throws an error if only errors are returned', done => {
+      fetch.mockReturnValueOnce(
+        Promise.resolve({ status: 400, text: textWithErrors }),
+      );
+
+      const link = createHttpLink({ uri: 'data', fetch });
+
+      let called = false;
+
+      execute(link, { query: sampleQuery }).subscribe(
+        result => {
+          done.fail('should not have called result because we have no data');
+        },
+        e => {
+          expect(e.message).toMatch(/Received status code 400/);
+          expect(e.statusCode).toBe(400);
+          expect(e.result).toEqual(responseBody);
+          done();
+        },
+      );
+    });
+    it('throws an error if empty response from the server ', done => {
+      fetch.mockReturnValueOnce(Promise.resolve({ text }));
+      text.mockReturnValueOnce(Promise.resolve('{ "body": "boo" }'));
+      const link = createHttpLink({ uri: 'data', fetch });
+
+      execute(link, { query: sampleQuery }).subscribe(
+        result => {
+          done.fail('next should have been thrown from the network');
+        },
+        makeCallback(done, e => {
+          expect(e.message).toMatch(
+            /Server response was missing for query 'SampleQuery'/,
+          );
+        }),
+      );
+    });
+    it("throws if the body can't be stringified", done => {
+      fetch.mockReturnValueOnce(Promise.resolve({ data: {}, text }));
+      const link = createHttpLink({ uri: 'data', fetch });
+
+      let b;
+      const a = { b };
+      b = { a };
+      a.b = b;
+      const variables = {
+        a,
+        b,
+      };
+      execute(link, { query: sampleQuery, variables }).subscribe(
+        result => {
+          done.fail('next should have been thrown from the link');
+        },
+        makeCallback(done, e => {
+          expect(e.message).toMatch(/Payload is not serializable/);
+          expect(e.parseError.message).toMatch(
+            /Converting circular structure to JSON/,
+          );
+        }),
+      );
+    });
+    it('supports being cancelled and does not throw', done => {
+      let called;
+      class AbortController {
+        signal: {};
+        abort = () => {
+          called = true;
+        };
+      }
+
+      global.AbortController = AbortController;
+
+      fetch.mockReturnValueOnce(Promise.resolve({ text }));
+      text.mockReturnValueOnce(
+        Promise.resolve('{ "data": { "hello": "world" } }'),
+      );
+
+      const link = createHttpLink({ uri: 'data', fetch });
+
+      const sub = execute(link, { query: sampleQuery }).subscribe({
+        next: result => {
+          done.fail('result should not have been called');
+        },
+        error: e => {
+          done.fail(e);
+        },
+        complete: () => {
+          done.fail('complete should not have been called');
+        },
+      });
+      sub.unsubscribe();
+
+      setTimeout(
+        makeCallback(done, () => {
+          delete global.AbortController;
+          expect(called).toBe(true);
+          fetch.mockReset();
+          text.mockReset();
+        }),
+        150,
+      );
+    });
+
+    const body = '{';
+    const unparsableJson = jest.fn(() => Promise.resolve(body));
+    it('throws an error if response is unparsable', done => {
+      fetch.mockReturnValueOnce(
+        Promise.resolve({ status: 400, text: unparsableJson }),
+      );
+      const link = createHttpLink({ uri: 'data', fetch });
+
+      execute(link, { query: sampleQuery }).subscribe(
+        result => {
+          done.fail('next should have been thrown from the network');
+        },
+        makeCallback(done, e => {
+          expect(e.message).toMatch(/JSON/);
+          expect(e.statusCode).toBe(400);
+          expect(e.response).toBeDefined();
+          expect(e.bodyText).toBe(body);
+        }),
+      );
+    });
+  });
+});
diff --git a/src/link/http/__tests__/checkFetcher.ts b/src/link/http/__tests__/checkFetcher.ts
new file mode 100644
--- /dev/null
+++ b/src/link/http/__tests__/checkFetcher.ts
@@ -0,0 +1,23 @@
+import { checkFetcher } from '../checkFetcher';
+
+describe('checkFetcher', () => {
+  let oldFetch;
+  beforeEach(() => {
+    oldFetch = window.fetch;
+    delete window.fetch;
+  });
+
+  afterEach(() => {
+    window.fetch = oldFetch;
+  });
+
+  it('throws if no fetch is present', () => {
+    expect(() => checkFetcher(undefined)).toThrow(
+      /has not been found globally/,
+    );
+  });
+
+  it('does not throws if no fetch is present but a fetch is passed', () => {
+    expect(() => checkFetcher(() => {})).not.toThrow();
+  });
+});
diff --git a/src/link/http/__tests__/parseAndCheckHttpResponse.ts b/src/link/http/__tests__/parseAndCheckHttpResponse.ts
new file mode 100644
--- /dev/null
+++ b/src/link/http/__tests__/parseAndCheckHttpResponse.ts
@@ -0,0 +1,91 @@
+import gql from 'graphql-tag';
+import fetchMock from 'fetch-mock';
+
+import { createOperation } from '../../utils/createOperation';
+import { parseAndCheckHttpResponse } from '../parseAndCheckHttpResponse';
+
+const query = gql`
+  query SampleQuery {
+    stub {
+      id
+    }
+  }
+`;
+
+describe('parseAndCheckResponse', () => {
+  beforeEach(() => {
+    fetchMock.restore();
+  });
+
+  const operations = [createOperation({}, { query })];
+
+  it('throws a parse error with a status code on unparsable response', done => {
+    const status = 400;
+    fetchMock.mock('begin:/error', status);
+    fetch('error')
+      .then(parseAndCheckHttpResponse(operations))
+      .then(done.fail)
+      .catch(e => {
+        expect(e.statusCode).toBe(status);
+        expect(e.name).toBe('ServerParseError');
+        expect(e).toHaveProperty('response');
+        expect(e).toHaveProperty('bodyText');
+        done();
+      })
+      .catch(done.fail);
+  });
+
+  it('throws a network error with a status code and result', done => {
+    const status = 403;
+    const body = { data: 'fail' }; //does not contain data or errors
+    fetchMock.mock('begin:/error', {
+      body,
+      status,
+    });
+    fetch('error')
+      .then(parseAndCheckHttpResponse(operations))
+      .then(done.fail)
+      .catch(e => {
+        expect(e.statusCode).toBe(status);
+        expect(e.name).toBe('ServerError');
+        expect(e).toHaveProperty('response');
+        expect(e).toHaveProperty('result');
+        done();
+      })
+      .catch(done.fail);
+  });
+
+  it('throws a server error on incorrect data', done => {
+    const data = { hello: 'world' }; //does not contain data or erros
+    fetchMock.mock('begin:/incorrect', data);
+    fetch('incorrect')
+      .then(parseAndCheckHttpResponse(operations))
+      .then(done.fail)
+      .catch(e => {
+        expect(e.statusCode).toBe(200);
+        expect(e.name).toBe('ServerError');
+        expect(e).toHaveProperty('response');
+        expect(e.result).toEqual(data);
+        done();
+      })
+      .catch(done.fail);
+  });
+
+  it('is able to return a correct GraphQL result', done => {
+    const errors = ['', '' + new Error('hi')];
+    const data = { data: { hello: 'world' }, errors };
+
+    fetchMock.mock('begin:/data', {
+      body: data,
+    });
+    fetch('data')
+      .then(parseAndCheckHttpResponse(operations))
+      .then(({ data, errors: e }) => {
+        expect(data).toEqual({ hello: 'world' });
+        expect(e.length).toEqual(errors.length);
+        expect(e).toEqual(errors);
+        done();
+      })
+      .catch(done.fail);
+  });
+});
diff --git a/src/link/http/__tests__/selectHttpOptionsAndBody.ts b/src/link/http/__tests__/selectHttpOptionsAndBody.ts
new file mode 100644
--- /dev/null
+++ b/src/link/http/__tests__/selectHttpOptionsAndBody.ts
@@ -0,0 +1,91 @@
+import gql from 'graphql-tag';
+
+import { createOperation } from '../../utils/createOperation';
+import {
+  selectHttpOptionsAndBody,
+  fallbackHttpConfig,
+} from '../selectHttpOptionsAndBody';
+
+const query = gql`
+  query SampleQuery {
+    stub {
+      id
+    }
+  }
+`;
+
+describe('selectHttpOptionsAndBody', () => {
+  it('includeQuery allows the query to be ignored', () => {
+    const { options, body } = selectHttpOptionsAndBody(
+      createOperation({}, { query }),
+      { http: { includeQuery: false } },
+    );
+    expect(body).not.toHaveProperty('query');
+  });
+
+  it('includeExtensions allows the extensions to be added', () => {
+    const extensions = { yo: 'what up' };
+    const { options, body } = selectHttpOptionsAndBody(
+      createOperation({}, { query, extensions }),
+      { http: { includeExtensions: true } },
+    );
+    expect(body).toHaveProperty('extensions');
+    expect((body as any).extensions).toEqual(extensions);
+  });
+
+  it('the fallbackConfig is used if no other configs are specified', () => {
+    const defaultHeaders = {
+      accept: '*/*',
+      'content-type': 'application/json',
+    };
+
+    const defaultOptions = {
+      method: 'POST',
+    };
+
+    const extensions = { yo: 'what up' };
+    const { options, body } = selectHttpOptionsAndBody(
+      createOperation({}, { query, extensions }),
+      fallbackHttpConfig,
+    );
+
+    expect(body).toHaveProperty('query');
+    expect(body).not.toHaveProperty('extensions');
+
+    expect(options.headers).toEqual(defaultHeaders);
+    expect(options.method).toEqual(defaultOptions.method);
+  });
+
+  it('allows headers, credentials, and setting of method to function correctly', () => {
+    const headers = {
+      accept: 'application/json',
+      'content-type': 'application/graphql',
+    };
+
+    const credentials = {
+      'X-Secret': 'djmashko',
+    };
+
+    const opts = {
+      opt: 'hi',
+    };
+
+    const config = { headers, credentials, options: opts };
+
+    const extensions = { yo: 'what up' };
+
+    const { options, body } = selectHttpOptionsAndBody(
+      createOperation({}, { query, extensions }),
+      fallbackHttpConfig,
+      config,
+    );
+
+    expect(body).toHaveProperty('query');
+    expect(body).not.toHaveProperty('extensions');
+
+    expect(options.headers).toEqual(headers);
+    expect(options.credentials).toEqual(credentials);
+    expect(options.opt).toEqual('hi');
+    expect(options.method).toEqual('POST'); //from default
+  });
+});
diff --git a/src/link/http/__tests__/selectURI.ts b/src/link/http/__tests__/selectURI.ts
new file mode 100644
--- /dev/null
+++ b/src/link/http/__tests__/selectURI.ts
@@ -0,0 +1,32 @@
+import gql from 'graphql-tag';
+
+import { createOperation } from '../../utils/createOperation';
+import { selectURI } from '../selectURI';
+
+const query = gql`
+  query SampleQuery {
+    stub {
+      id
+    }
+  }
+`;
+
+describe('selectURI', () => {
+  it('returns a passed in string', () => {
+    const uri = '/somewhere';
+    const operation = createOperation({ uri }, { query });
+    expect(selectURI(operation)).toEqual(uri);
+  });
+
+  it('returns a fallback of /graphql', () => {
+    const uri = '/graphql';
+    const operation = createOperation({}, { query });
+    expect(selectURI(operation)).toEqual(uri);
+  });
+
+  it('returns the result of a UriFunction', () => {
+    const uri = '/somewhere';
+    const operation = createOperation({}, { query });
+    expect(selectURI(operation, () => uri)).toEqual(uri);
+  });
+});
diff --git a/src/link/http/__tests__/serializeFetchParameter.ts b/src/link/http/__tests__/serializeFetchParameter.ts
new file mode 100644
--- /dev/null
+++ b/src/link/http/__tests__/serializeFetchParameter.ts
@@ -0,0 +1,17 @@
+import { serializeFetchParameter } from '../serializeFetchParameter';
+
+describe('serializeFetchParameter', () => {
+  it('throws a parse error on an unparsable body', () => {
+    const b = {};
+    const a = { b };
+    (b as any).a = a;
+
+    expect(() => serializeFetchParameter(b, 'Label')).toThrow(/Label/);
+  });
+
+  it('returns a correctly parsed body', () => {
+    const body = { no: 'thing' };
+
+    expect(serializeFetchParameter(body, 'Label')).toEqual('{"no":"thing"}');
+  });
+});
diff --git a/src/link/utils/__tests__/fromError.ts b/src/link/utils/__tests__/fromError.ts
new file mode 100644
--- /dev/null
+++ b/src/link/utils/__tests__/fromError.ts
@@ -0,0 +1,12 @@
+import { toPromise } from '../toPromise';
+import { fromError, } from '../fromError';
+
+describe('fromError', () => {
+  it('acts as error call', () => {
+    const error = new Error('I always error');
+    const observable = fromError(error);
+    return toPromise(observable)
+      .then(expect.fail)
+      .catch(actualError => expect(error).toEqual(actualError));
+  });
+});
diff --git a/src/link/utils/__tests__/fromPromise.ts b/src/link/utils/__tests__/fromPromise.ts
new file mode 100644
--- /dev/null
+++ b/src/link/utils/__tests__/fromPromise.ts
@@ -0,0 +1,25 @@
+import { fromPromise } from '../fromPromise';
+import { toPromise, } from '../toPromise';
+
+describe('fromPromise', () => {
+  const data = {
+    data: {
+      hello: 'world',
+    },
+  };
+  const error = new Error('I always error');
+
+  it('return next call as Promise resolution', () => {
+    const observable = fromPromise(Promise.resolve(data));
+    return toPromise(observable).then(result =>
+      expect(data).toEqual(result),
+    );
+  });
+
+  it('return Promise rejection as error call', () => {
+    const observable = fromPromise(Promise.reject(error));
+    return toPromise(observable)
+      .then(expect.fail)
+      .catch(actualError => expect(error).toEqual(actualError));
+  });
+});
diff --git a/src/link/utils/__tests__/toPromise.ts b/src/link/utils/__tests__/toPromise.ts
new file mode 100644
--- /dev/null
+++ b/src/link/utils/__tests__/toPromise.ts
@@ -0,0 +1,46 @@
+import { Observable } from '../../../utilities/observables/Observable';
+import { toPromise } from '../toPromise';
+import { fromError } from '../fromError';
+
+describe('toPromise', () => {
+  const data = {
+    data: {
+      hello: 'world',
+    },
+  };
+  const error = new Error('I always error');
+
+  it('return next call as Promise resolution', () => {
+    return toPromise(Observable.of(data)).then(result =>
+      expect(data).toEqual(result),
+    );
+  });
+
+  it('return error call as Promise rejection', () => {
+    return toPromise(fromError(error))
+      .then(expect.fail)
+      .catch(actualError => expect(error).toEqual(actualError));
+  });
+
+  describe('warnings', () => {
+    const spy = jest.fn();
+    let _warn: (message?: any, ...originalParams: any[]) => void;
+
+    beforeEach(() => {
+      _warn = console.warn;
+      console.warn = spy;
+    });
+
+    afterEach(() => {
+      console.warn = _warn;
+    });
+
+    it('return error call as Promise rejection', done => {
+      toPromise(Observable.of(data, data)).then(result => {
+        expect(data).toEqual(result);
+        expect(spy).toHaveBeenCalled();
+        done();
+      });
+    });
+  });
+});
diff --git a/src/link/utils/__tests__/validateOperation.ts b/src/link/utils/__tests__/validateOperation.ts
new file mode 100644
--- /dev/null
+++ b/src/link/utils/__tests__/validateOperation.ts
@@ -0,0 +1,17 @@
+import { validateOperation, } from '../validateOperation';
+
+describe('validateOperation', () => {
+  it('should throw when invalid field in operation', () => {
+    expect(() => validateOperation(<any>{ qwerty: '' })).toThrow();
+  });
+
+  it('should not throw when valid fields in operation', () => {
+    expect(() =>
+      validateOperation({
+        query: '1234',
+        context: {},
+        variables: {},
+      }),
+    ).not.toThrow();
+  });
+});
diff --git a/src/react/context/__tests__/ApolloConsumer.test.tsx b/src/react/context/__tests__/ApolloConsumer.test.tsx
new file mode 100644
--- /dev/null
+++ b/src/react/context/__tests__/ApolloConsumer.test.tsx
@@ -0,0 +1,69 @@
+import { render, cleanup } from '@testing-library/react';
+
+import { ApolloLink } from '../../../link/core/ApolloLink';
+import { ApolloClient } from '../../../ApolloClient';
+import { InMemoryCache as Cache } from '../../../cache/inmemory/inMemoryCache';
+import { ApolloProvider } from '../ApolloProvider';
+import { ApolloConsumer } from '../ApolloConsumer';
+import { getApolloContext } from '../ApolloContext';
+import { requireReactLazily } from '../../react';
+
+const React = requireReactLazily();
+
+const client = new ApolloClient({
+  cache: new Cache(),
+  link: new ApolloLink((o, f) => (f ? f(o) : null))
+});
+
+describe('<ApolloConsumer /> component', () => {
+  afterEach(cleanup);
+
+  it('has a render prop', done => {
+    render(
+      <ApolloProvider client={client}>
+        <ApolloConsumer>
+          {clientRender => {
+            try {
+              expect(clientRender).toBe(client);
+              done();
+            } catch (e) {
+              done.fail(e);
+            }
+            return null;
+          }}
+        </ApolloConsumer>
+      </ApolloProvider>
+    );
+  });
+
+  it('renders the content in the children prop', () => {
+    const { getByText } = render(
+      <ApolloProvider client={client}>
+        <ApolloConsumer>{() => <div>Test</div>}</ApolloConsumer>
+      </ApolloProvider>
+    );
+
+    expect(getByText('Test')).toBeTruthy();
+  });
+
+  it('errors if there is no client in the context', () => {
+    // Prevent Error about missing context type from appearing in the console.
+    const errorLogger = console.error;
+    console.error = () => {};
+    expect(() => {
+      // We're wrapping the `ApolloConsumer` component in a
+      // `ApolloContext.Provider` component, to reset the context before
+      // testing.
+      const ApolloContext = getApolloContext();
+      render(
+        <ApolloContext.Provider value={{}}>
+          <ApolloConsumer>{() => null}</ApolloConsumer>
+        </ApolloContext.Provider>
+      );
+    }).toThrowError(
+      'Could not find "client" in the context of ApolloConsumer. Wrap the root component in an <ApolloProvider>'
+    );
+
+    console.error = errorLogger;
+  });
+});
diff --git a/src/react/context/__tests__/ApolloProvider.test.tsx b/src/react/context/__tests__/ApolloProvider.test.tsx
new file mode 100644
--- /dev/null
+++ b/src/react/context/__tests__/ApolloProvider.test.tsx
@@ -0,0 +1,149 @@
+import { render, cleanup } from '@testing-library/react';
+
+import { ApolloLink } from '../../../link/core/ApolloLink';
+import { ApolloClient } from '../../../ApolloClient';
+import { InMemoryCache as Cache } from '../../../cache/inmemory/inMemoryCache';
+import { ApolloProvider } from '../ApolloProvider';
+import { getApolloContext } from '../ApolloContext';
+import { requireReactLazily } from '../../react';
+
+const React = requireReactLazily();
+const { useContext } = React;
+
+describe('<ApolloProvider /> Component', () => {
+  afterEach(cleanup);
+
+  const client = new ApolloClient({
+    cache: new Cache(),
+    link: new ApolloLink((o, f) => (f ? f(o) : null))
+  });
+
+  class Child extends React.Component<any, { store: any; client: any }> {
+    static contextType = getApolloContext();
+
+    componentDidUpdate() {
+      if (this.props.data) this.props.data.refetch();
+    }
+
+    render() {
+      return null;
+    }
+  }
+
+  interface Props {
+    client: ApolloClient<any>;
+  }
+
+  class Container extends React.Component<Props, any> {
+    constructor(props: Props) {
+      super(props);
+      this.state = {};
+    }
+
+    componentDidMount() {
+      this.setState({
+        client: this.props.client
+      });
+    }
+
+    render() {
+      return (
+        <ApolloProvider client={this.state.client || this.props.client}>
+          <Child />
+        </ApolloProvider>
+      );
+    }
+  }
+
+  it('should render children components', () => {
+    const { getByText } = render(
+      <ApolloProvider client={client}>
+        <div className="unique">Test</div>
+      </ApolloProvider>
+    );
+
+    expect(getByText('Test')).toBeTruthy();
+  });
+
+  it('should support the 2.0', () => {
+    const { getByText } = render(
+      <ApolloProvider client={{} as ApolloClient<any>}>
+        <div className="unique">Test</div>
+      </ApolloProvider>
+    );
+
+    expect(getByText('Test')).toBeTruthy();
+  });
+
+  it('should require a client', () => {
+    const originalConsoleError = console.error;
+    console.error = () => {
+      /* noop */
+    };
+    expect(() => {
+      // Before testing `ApolloProvider`, we first fully reset the
+      // existing context using `ApolloContext.Provider` directly.
+      const ApolloContext = getApolloContext();
+      render(
+        <ApolloContext.Provider value={{}}>
+          <ApolloProvider client={undefined as any}>
+            <div className="unique" />
+          </ApolloProvider>
+        </ApolloContext.Provider>
+      );
+    }).toThrowError(
+      'ApolloProvider was not passed a client instance. Make ' +
+        'sure you pass in your client via the "client" prop.'
+    );
+    console.error = originalConsoleError;
+  });
+
+  it('should not require a store', () => {
+    const { getByText } = render(
+      <ApolloProvider client={client}>
+        <div className="unique">Test</div>
+      </ApolloProvider>
+    );
+    expect(getByText('Test')).toBeTruthy();
+  });
+
+  it('should add the client to the children context', () => {
+    const TestChild = () => {
+      const context = useContext(getApolloContext());
+      expect(context.client).toEqual(client);
+      return null;
+    };
+    render(
+      <ApolloProvider client={client}>
+        <TestChild />
+        <TestChild />
+      </ApolloProvider>
+    );
+  });
+
+  it('should update props when the client changes', () => {
+    let clientToCheck = client;
+
+    const TestChild = () => {
+      const context = useContext(getApolloContext());
+      expect(context.client).toEqual(clientToCheck);
+      return null;
+    };
+    const { rerender } = render(
+      <ApolloProvider client={clientToCheck}>
+        <TestChild />
+      </ApolloProvider>
+    );
+
+    const newClient = new ApolloClient({
+      cache: new Cache(),
+      link: new ApolloLink((o, f) => (f ? f(o) : null))
+    });
+    clientToCheck = newClient;
+    rerender(
+      <ApolloProvider client={clientToCheck}>
+        <TestChild />
+      </ApolloProvider>
+    );
+  });
+});
diff --git a/src/react/hooks/__tests__/useApolloClient.test.tsx b/src/react/hooks/__tests__/useApolloClient.test.tsx
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/__tests__/useApolloClient.test.tsx
@@ -0,0 +1,46 @@
+import { render, cleanup } from '@testing-library/react';
+import { InvariantError } from 'ts-invariant';
+
+import { ApolloLink } from '../../../link/core/ApolloLink';
+import { ApolloProvider } from '../../context/ApolloProvider';
+import { ApolloClient } from '../../../ApolloClient';
+import { InMemoryCache } from '../../../cache/inmemory/inMemoryCache';
+import { useApolloClient } from '../useApolloClient';
+import { resetApolloContext } from '../../context/ApolloContext';
+import { requireReactLazily } from '../../react';
+
+const React = requireReactLazily();
+
+describe('useApolloClient Hook', () => {
+  afterEach(() => {
+    cleanup();
+    resetApolloContext();
+  });
+
+  it('should return a client instance from the context if available', () => {
+    const client = new ApolloClient({
+      cache: new InMemoryCache(),
+      link: ApolloLink.empty()
+    });
+
+    function App() {
+      expect(useApolloClient()).toEqual(client);
+      return null;
+    }
+
+    render(
+      <ApolloProvider client={client}>
+        <App />
+      </ApolloProvider>
+    );
+  });
+
+  it("should error if a client instance can't be found in the context", () => {
+    function App() {
+      expect(() => useApolloClient()).toThrow(InvariantError);
+      return null;
+    }
+
+    render(<App />);
+  });
+});
diff --git a/src/react/hooks/__tests__/useLazyQuery.test.tsx b/src/react/hooks/__tests__/useLazyQuery.test.tsx
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/__tests__/useLazyQuery.test.tsx
@@ -0,0 +1,396 @@
+import { DocumentNode } from 'graphql';
+import gql from 'graphql-tag';
+import { render, wait } from '@testing-library/react';
+
+import { MockedProvider } from '../../../utilities/testing';
+import { ApolloClient } from '../../../ApolloClient';
+import { InMemoryCache } from '../../../cache/inmemory/inMemoryCache';
+import { ApolloProvider } from '../../context/ApolloProvider';
+import { useLazyQuery } from '../useLazyQuery';
+import { requireReactLazily } from '../../react';
+
+const React = requireReactLazily();
+
+describe('useLazyQuery Hook', () => {
+  const CAR_QUERY: DocumentNode = gql`
+    query {
+      cars {
+        make
+        model
+        vin
+      }
+    }
+  `;
+
+  const CAR_RESULT_DATA = {
+    cars: [
+      {
+        make: 'Audi',
+        model: 'RS8',
+        vin: 'DOLLADOLLABILL',
+        __typename: 'Car'
+      }
+    ]
+  };
+
+  const CAR_MOCKS = [
+    {
+      request: {
+        query: CAR_QUERY
+      },
+      result: { data: CAR_RESULT_DATA }
+    }
+  ];
+
+  it('should hold query execution until manually triggered', async () => {
+    let renderCount = 0;
+    const Component = () => {
+      const [execute, { loading, data }] = useLazyQuery(CAR_QUERY);
+      switch (renderCount) {
+        case 0:
+          expect(loading).toEqual(false);
+          setTimeout(() => {
+            execute();
+          });
+          break;
+        case 1:
+          expect(loading).toEqual(true);
+          break;
+        case 2:
+          expect(loading).toEqual(false);
+          expect(data).toEqual(CAR_RESULT_DATA);
+          break;
+        default: // Do nothing
+      }
+      renderCount += 1;
+      return null;
+    };
+
+    render(
+      <MockedProvider mocks={CAR_MOCKS}>
+        <Component />
+      </MockedProvider>
+    );
+
+    return wait(() => {
+      expect(renderCount).toBe(3);
+    });
+  });
+
+  it('should set `called` to false by default', () => {
+    const Component = () => {
+      const [, { loading, called }] = useLazyQuery(CAR_QUERY);
+      expect(loading).toBeFalsy();
+      expect(called).toBeFalsy();
+      return null;
+    };
+
+    render(
+      <MockedProvider mocks={CAR_MOCKS}>
+        <Component />
+      </MockedProvider>
+    );
+  });
+
+  it('should set `called` to true after calling the lazy execute function', async () => {
+    let renderCount = 0;
+    const Component = () => {
+      const [execute, { loading, called, data }] = useLazyQuery(CAR_QUERY);
+      switch (renderCount) {
+        case 0:
+          expect(loading).toBeFalsy();
+          expect(called).toBeFalsy();
+          setTimeout(() => {
+            execute();
+          });
+          break;
+        case 1:
+          expect(loading).toBeTruthy();
+          expect(called).toBeTruthy();
+          break;
+        case 2:
+          expect(loading).toEqual(false);
+          expect(called).toBeTruthy();
+          expect(data).toEqual(CAR_RESULT_DATA);
+          break;
+        default: // Do nothing
+      }
+      renderCount += 1;
+      return null;
+    };
+
+    render(
+      <MockedProvider mocks={CAR_MOCKS}>
+        <Component />
+      </MockedProvider>
+    );
+
+    return wait(() => {
+      expect(renderCount).toBe(3);
+    });
+  });
+
+  it('should override `skip` if lazy mode execution function is called', async () => {
+    let renderCount = 0;
+    const Component = () => {
+      const [execute, { loading, data }] = useLazyQuery(CAR_QUERY, {
+        skip: true
+      } as any);
+      switch (renderCount) {
+        case 0:
+          expect(loading).toBeFalsy();
+          setTimeout(() => {
+            execute();
+          });
+          break;
+        case 1:
+          expect(loading).toBeTruthy();
+          break;
+        case 2:
+          expect(loading).toEqual(false);
+          expect(data).toEqual(CAR_RESULT_DATA);
+          break;
+        default: // Do nothing
+      }
+      renderCount += 1;
+      return null;
+    };
+
+    render(
+      <MockedProvider mocks={CAR_MOCKS}>
+        <Component />
+      </MockedProvider>
+    );
+
+    return wait(() => {
+      expect(renderCount).toBe(3);
+    });
+  });
+
+  it(
+    'should use variables defined in hook options (if any), when running ' +
+      'the lazy execution function',
+    async () => {
+      const CAR_QUERY: DocumentNode = gql`
+        query AllCars($year: Int!) {
+          cars(year: $year) @client {
+            make
+            year
+          }
+        }
+      `;
+
+      const CAR_RESULT_DATA = [
+        {
+          make: 'Audi',
+          year: 2000,
+          __typename: 'Car'
+        },
+        {
+          make: 'Hyundai',
+          year: 2001,
+          __typename: 'Car'
+        }
+      ];
+
+      const client = new ApolloClient({
+        cache: new InMemoryCache(),
+        resolvers: {
+          Query: {
+            cars(_root, { year }) {
+              return CAR_RESULT_DATA.filter(car => car.year === year);
+            }
+          }
+        }
+      });
+
+      let renderCount = 0;
+      const Component = () => {
+        const [execute, { loading, data }] = useLazyQuery(CAR_QUERY, {
+          variables: { year: 2001 }
+        });
+        switch (renderCount) {
+          case 0:
+            expect(loading).toBeFalsy();
+            setTimeout(() => {
+              execute();
+            });
+            break;
+          case 1:
+            expect(loading).toBeTruthy();
+            break;
+          case 2:
+            expect(loading).toEqual(false);
+            expect(data.cars).toEqual([CAR_RESULT_DATA[1]]);
+            break;
+          default: // Do nothing
+        }
+        renderCount += 1;
+        return null;
+      };
+
+      render(
+        <ApolloProvider client={client}>
+          <Component />
+        </ApolloProvider>
+      );
+
+      return wait(() => {
+        expect(renderCount).toBe(3);
+      });
+    }
+  );
+
+  it(
+    'should use variables passed into lazy execution function, ' +
+      'overriding similar variables defined in Hook options',
+    async () => {
+      const CAR_QUERY: DocumentNode = gql`
+        query AllCars($year: Int!) {
+          cars(year: $year) @client {
+            make
+            year
+          }
+        }
+      `;
+
+      const CAR_RESULT_DATA = [
+        {
+          make: 'Audi',
+          year: 2000,
+          __typename: 'Car'
+        },
+        {
+          make: 'Hyundai',
+          year: 2001,
+          __typename: 'Car'
+        }
+      ];
+
+      const client = new ApolloClient({
+        cache: new InMemoryCache(),
+        resolvers: {
+          Query: {
+            cars(_root, { year }) {
+              return CAR_RESULT_DATA.filter(car => car.year === year);
+            }
+          }
+        }
+      });
+
+      let renderCount = 0;
+      const Component = () => {
+        const [execute, { loading, data }] = useLazyQuery(CAR_QUERY, {
+          variables: { year: 2001 }
+        });
+        switch (renderCount) {
+          case 0:
+            expect(loading).toBeFalsy();
+            setTimeout(() => {
+              execute({ variables: { year: 2000 } });
+            });
+            break;
+          case 1:
+            expect(loading).toBeTruthy();
+            break;
+          case 2:
+            expect(loading).toEqual(false);
+            expect(data.cars).toEqual([CAR_RESULT_DATA[0]]);
+            break;
+          default: // Do nothing
+        }
+        renderCount += 1;
+        return null;
+      };
+
+      render(
+        <ApolloProvider client={client}>
+          <Component />
+        </ApolloProvider>
+      );
+
+      return wait(() => {
+        expect(renderCount).toBe(3);
+      });
+    }
+  );
+
+  it(
+    'should fetch data each time the execution function is called, when ' +
+      'using a "network-only" fetch policy',
+    async () => {
+      const data1 = CAR_RESULT_DATA;
+
+      const data2 = {
+        cars: [
+          {
+            make: 'Audi',
+            model: 'SQ5',
+            vin: 'POWERANDTRUNKSPACE',
+            __typename: 'Car'
+          }
+        ]
+      };
+
+      const mocks = [
+        {
+          request: {
+            query: CAR_QUERY
+          },
+          result: { data: data1 }
+        },
+        {
+          request: {
+            query: CAR_QUERY
+          },
+          result: { data: data2 }
+        }
+      ];
+
+      let renderCount = 0;
+      const Component = () => {
+        const [execute, { loading, data }] = useLazyQuery(CAR_QUERY, {
+          fetchPolicy: 'network-only'
+        });
+        switch (renderCount) {
+          case 0:
+            expect(loading).toEqual(false);
+            setTimeout(() => {
+              execute();
+            });
+            break;
+          case 1:
+            expect(loading).toEqual(true);
+            break;
+          case 2:
+            expect(loading).toEqual(false);
+            expect(data).toEqual(data1);
+            setTimeout(() => {
+              execute();
+            });
+            break;
+          case 3:
+            expect(loading).toEqual(true);
+            break;
+          case 4:
+            expect(loading).toEqual(false);
+            expect(data).toEqual(data2);
+            break;
+          default: // Do nothing
+        }
+        renderCount += 1;
+        return null;
+      };
+
+      render(
+        <MockedProvider mocks={mocks}>
+          <Component />
+        </MockedProvider>
+      );
+
+      return wait(() => {
+        expect(renderCount).toBe(5);
+      });
+    }
+  );
+});
diff --git a/src/react/hooks/__tests__/useMutation.test.tsx b/src/react/hooks/__tests__/useMutation.test.tsx
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/__tests__/useMutation.test.tsx
@@ -0,0 +1,351 @@
+import { DocumentNode } from 'graphql';
+import gql from 'graphql-tag';
+import { render, cleanup, wait } from '@testing-library/react';
+
+import { MockedProvider, mockSingleLink } from '../../../utilities/testing';
+import { itAsync } from '../../../utilities/testing/itAsync';
+import { ApolloClient } from '../../../ApolloClient';
+import { InMemoryCache } from '../../../cache/inmemory/inMemoryCache';
+import { ApolloProvider } from '../../context/ApolloProvider';
+import { useMutation } from '../useMutation';
+import { requireReactLazily } from '../../react';
+
+const React = requireReactLazily();
+const { useEffect } = React;
+
+describe('useMutation Hook', () => {
+  interface Todo {
+    id: number;
+    description: string;
+    priority: string;
+  }
+
+  const CREATE_TODO_MUTATION: DocumentNode = gql`
+    mutation createTodo($description: String!) {
+      createTodo(description: $description) {
+        id
+        description
+        priority
+      }
+    }
+  `;
+
+  const CREATE_TODO_RESULT = {
+    createTodo: {
+      id: 1,
+      description: 'Get milk!',
+      priority: 'High',
+      __typename: 'Todo'
+    }
+  };
+
+  afterEach(cleanup);
+
+  describe('General use', () => {
+    it('should handle a simple mutation properly', async () => {
+      const variables = {
+        description: 'Get milk!'
+      };
+
+      const mocks = [
+        {
+          request: {
+            query: CREATE_TODO_MUTATION,
+            variables
+          },
+          result: { data: CREATE_TODO_RESULT }
+        }
+      ];
+
+      let renderCount = 0;
+      const Component = () => {
+        const [createTodo, { loading, data }] = useMutation(
+          CREATE_TODO_MUTATION
+        );
+        switch (renderCount) {
+          case 0:
+            expect(loading).toBeFalsy();
+            expect(data).toBeUndefined();
+            createTodo({ variables });
+            break;
+          case 1:
+            expect(loading).toBeTruthy();
+            expect(data).toBeUndefined();
+            break;
+          case 2:
+            expect(loading).toBeFalsy();
+            expect(data).toEqual(CREATE_TODO_RESULT);
+            break;
+          default:
+        }
+        renderCount += 1;
+        return null;
+      };
+
+      render(
+        <MockedProvider mocks={mocks}>
+          <Component />
+        </MockedProvider>
+      );
+
+      return wait(() => {
+        expect(renderCount).toBe(3);
+      });
+    });
+
+    it('should be able to call mutations as an effect', async () => {
+      const variables = {
+        description: 'Get milk!'
+      };
+
+      const mocks = [
+        {
+          request: {
+            query: CREATE_TODO_MUTATION,
+            variables
+          },
+          result: { data: CREATE_TODO_RESULT }
+        }
+      ];
+
+      let renderCount = 0;
+      const useCreateTodo = () => {
+        const [createTodo, { loading, data }] = useMutation(
+          CREATE_TODO_MUTATION
+        );
+
+        useEffect(() => {
+          createTodo({ variables });
+        }, [variables]);
+
+        switch (renderCount) {
+          case 0:
+            expect(loading).toBeFalsy();
+            expect(data).toBeUndefined();
+            break;
+          case 1:
+            expect(loading).toBeTruthy();
+            expect(data).toBeUndefined();
+            break;
+          case 2:
+            expect(loading).toBeFalsy();
+            expect(data).toEqual(CREATE_TODO_RESULT);
+            break;
+          default:
+        }
+        renderCount += 1;
+        return null;
+      };
+
+      const Component = () => {
+        useCreateTodo();
+        return null;
+      };
+
+      render(
+        <MockedProvider mocks={mocks}>
+          <Component />
+        </MockedProvider>
+      );
+
+      return wait(() => {
+        expect(renderCount).toBe(3);
+      });
+    });
+
+    it('should ensure the mutation callback function has a stable identity', async () => {
+      const variables = {
+        description: 'Get milk!'
+      };
+
+      const mocks = [
+        {
+          request: {
+            query: CREATE_TODO_MUTATION,
+            variables
+          },
+          result: { data: CREATE_TODO_RESULT }
+        }
+      ];
+
+      let mutationFn: any;
+      let renderCount = 0;
+      const Component = () => {
+        const [createTodo, { loading, data }] = useMutation(
+          CREATE_TODO_MUTATION
+        );
+        switch (renderCount) {
+          case 0:
+            mutationFn = createTodo;
+            expect(loading).toBeFalsy();
+            expect(data).toBeUndefined();
+            setTimeout(() => {
+              createTodo({ variables });
+            });
+            break;
+          case 1:
+            expect(mutationFn).toBe(createTodo);
+            expect(loading).toBeTruthy();
+            expect(data).toBeUndefined();
+            break;
+          case 2:
+            expect(loading).toBeFalsy();
+            expect(data).toEqual(CREATE_TODO_RESULT);
+            break;
+          default:
+        }
+        renderCount += 1;
+        return null;
+      };
+
+      render(
+        <MockedProvider mocks={mocks}>
+          <Component />
+        </MockedProvider>
+      );
+
+      return wait(() => {
+        expect(renderCount).toBe(3);
+      });
+    });
+
+    it('should resolve mutate function promise with mutation results', async () => {
+      const variables = {
+        description: 'Get milk!'
+      };
+
+      const mocks = [
+        {
+          request: {
+            query: CREATE_TODO_MUTATION,
+            variables
+          },
+          result: { data: CREATE_TODO_RESULT }
+        }
+      ];
+
+      const Component = () => {
+        const [createTodo] = useMutation<{ createTodo: Todo }>(
+          CREATE_TODO_MUTATION
+        );
+
+        async function doIt() {
+          const { data } = await createTodo({ variables });
+          expect(data).toEqual(CREATE_TODO_RESULT);
+          expect(data!.createTodo.description).toEqual(
+            CREATE_TODO_RESULT.createTodo.description
+          );
+        }
+
+        useEffect(() => {
+          doIt();
+        }, []);
+
+        return null;
+      };
+
+      render(
+        <MockedProvider mocks={mocks}>
+          <Component />
+        </MockedProvider>
+      );
+
+      return wait();
+    });
+
+    it('should return the current client instance in the result object', async () => {
+      const Component = () => {
+        const [, { client }] = useMutation(CREATE_TODO_MUTATION);
+        expect(client).toBeDefined();
+        expect(client instanceof ApolloClient).toBeTruthy();
+        return null;
+      };
+
+      render(
+        <MockedProvider>
+          <Component />
+        </MockedProvider>
+      );
+
+      return wait();
+    });
+  });
+
+  describe('Optimistic response', () => {
+    itAsync('should support optimistic response handling', async (resolve, reject) => {
+      const optimisticResponse = {
+        __typename: 'Mutation',
+        createTodo: {
+          id: 1,
+          description: 'TEMPORARY',
+          priority: 'High',
+          __typename: 'Todo'
+        }
+      };
+
+      const variables = {
+        description: 'Get milk!'
+      };
+
+      const mocks = [
+        {
+          request: {
+            query: CREATE_TODO_MUTATION,
+            variables
+          },
+          result: { data: CREATE_TODO_RESULT }
+        }
+      ];
+
+      const link = mockSingleLink(...mocks).setOnError(reject);
+      const cache = new InMemoryCache();
+      const client = new ApolloClient({
+        cache,
+        link
+      });
+
+      let renderCount = 0;
+      const Component = () => {
+        const [createTodo, { loading, data }] = useMutation(
+          CREATE_TODO_MUTATION,
+          { optimisticResponse }
+        );
+
+        switch (renderCount) {
+          case 0:
+            expect(loading).toBeFalsy();
+            expect(data).toBeUndefined();
+            createTodo({ variables });
+
+            const dataInStore = client.cache.extract(true);
+            expect(dataInStore['Todo:1']).toEqual(
+              optimisticResponse.createTodo
+            );
+
+            break;
+          case 1:
+            expect(loading).toBeTruthy();
+            expect(data).toBeUndefined();
+            break;
+          case 2:
+            expect(loading).toBeFalsy();
+            expect(data).toEqual(CREATE_TODO_RESULT);
+            break;
+          default:
+        }
+        renderCount += 1;
+        return null;
+      };
+
+      render(
+        <ApolloProvider client={client}>
+          <Component />
+        </ApolloProvider>
+      );
+
+      return wait(() => {
+        expect(renderCount).toBe(3);
+      }).then(resolve, reject);
+    });
+  });
+});
diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/__tests__/useQuery.test.tsx
@@ -0,0 +1,1267 @@
+import { DocumentNode, GraphQLError } from 'graphql';
+import gql from 'graphql-tag';
+import { render, cleanup, wait } from '@testing-library/react';
+
+import { Observable } from '../../../utilities/observables/Observable';
+import { ApolloLink } from '../../../link/core/ApolloLink';
+import { MockedProvider, mockSingleLink } from '../../../utilities/testing';
+import { MockLink } from '../../../utilities/testing/mocking/mockLink';
+import { itAsync } from '../../../utilities/testing/itAsync';
+import { ApolloClient } from '../../../ApolloClient';
+import { InMemoryCache } from '../../../cache/inmemory/inMemoryCache';
+import { ApolloProvider } from '../../context/ApolloProvider';
+import { useQuery } from '../useQuery';
+import { requireReactLazily } from '../../react';
+
+const React = requireReactLazily();
+const { useState, useReducer } = React;
+
+describe('useQuery Hook', () => {
+  const CAR_QUERY: DocumentNode = gql`
+    query {
+      cars {
+        make
+        model
+        vin
+      }
+    }
+  `;
+
+  const CAR_RESULT_DATA = {
+    cars: [
+      {
+        make: 'Audi',
+        model: 'RS8',
+        vin: 'DOLLADOLLABILL',
+        __typename: 'Car'
+      }
+    ]
+  };
+
+  const CAR_MOCKS = [
+    {
+      request: {
+        query: CAR_QUERY
+      },
+      result: { data: CAR_RESULT_DATA }
+    }
+  ];
+
+  afterEach(cleanup);
+
+  describe('General use', () => {
+    it('should handle a simple query properly', async () => {
+      const Component = () => {
+        const { data, loading } = useQuery(CAR_QUERY);
+        if (!loading) {
+          expect(data).toEqual(CAR_RESULT_DATA);
+        }
+        return null;
+      };
+
+      render(
+        <MockedProvider mocks={CAR_MOCKS}>
+          <Component />
+        </MockedProvider>
+      );
+
+      return wait();
+    });
+
+    it('should keep data as undefined until data is actually returned', async () => {
+      const Component = () => {
+        const { data, loading } = useQuery(CAR_QUERY);
+        if (loading) {
+          expect(data).toBeUndefined();
+        } else {
+          expect(data).toEqual(CAR_RESULT_DATA);
+        }
+        return null;
+      };
+
+      render(
+        <MockedProvider mocks={CAR_MOCKS}>
+          <Component />
+        </MockedProvider>
+      );
+
+      return wait();
+    });
+
+    it('should ensure ObservableQuery fields have a stable identity', async () => {
+      let refetchFn: any;
+      let fetchMoreFn: any;
+      let updateQueryFn: any;
+      let startPollingFn: any;
+      let stopPollingFn: any;
+      let subscribeToMoreFn: any;
+      const Component = () => {
+        const {
+          loading,
+          refetch,
+          fetchMore,
+          updateQuery,
+          startPolling,
+          stopPolling,
+          subscribeToMore
+        } = useQuery(CAR_QUERY);
+        if (loading) {
+          refetchFn = refetch;
+          fetchMoreFn = fetchMore;
+          updateQueryFn = updateQuery;
+          startPollingFn = startPolling;
+          stopPollingFn = stopPolling;
+          subscribeToMoreFn = subscribeToMore;
+        } else {
+          expect(refetch).toBe(refetchFn);
+          expect(fetchMore).toBe(fetchMoreFn);
+          expect(updateQuery).toBe(updateQueryFn);
+          expect(startPolling).toBe(startPollingFn);
+          expect(stopPolling).toBe(stopPollingFn);
+          expect(subscribeToMore).toBe(subscribeToMoreFn);
+        }
+        return null;
+      };
+
+      render(
+        <MockedProvider mocks={CAR_MOCKS}>
+          <Component />
+        </MockedProvider>
+      );
+
+      return wait();
+    });
+  });
+
+  describe('Polling', () => {
+    it('should support polling', async () => {
+      let renderCount = 0;
+      const Component = () => {
+        let { data, loading, stopPolling } = useQuery(CAR_QUERY, {
+          pollInterval: 10
+        });
+        switch (renderCount) {
+          case 0:
+            expect(loading).toBeTruthy();
+            break;
+          case 1:
+            expect(loading).toBeFalsy();
+            expect(data).toEqual(CAR_RESULT_DATA);
+            break;
+          case 2:
+            expect(loading).toBeFalsy();
+            expect(data).toEqual(CAR_RESULT_DATA);
+            stopPolling();
+            break;
+          case 3:
+            throw new Error('Uh oh - we should have stopped polling!');
+          default:
+          // Do nothing
+        }
+        renderCount += 1;
+        return null;
+      };
+
+      render(
+        <MockedProvider mocks={CAR_MOCKS}>
+          <Component />
+        </MockedProvider>
+      );
+
+      return wait(() => {
+        expect(renderCount).toBe(3);
+      });
+    });
+
+    it('should stop polling when skip is true', async () => {
+      let renderCount = 0;
+      const Component = () => {
+        const [shouldSkip, setShouldSkip] = useState(false);
+        let { data, loading } = useQuery(CAR_QUERY, {
+          pollInterval: 10,
+          skip: shouldSkip
+        });
+
+        switch (renderCount) {
+          case 0:
+            expect(loading).toBeTruthy();
+            break;
+          case 1:
+            expect(loading).toBeFalsy();
+            expect(data).toEqual(CAR_RESULT_DATA);
+            break;
+          case 2:
+            expect(loading).toBeFalsy();
+            expect(data).toEqual(CAR_RESULT_DATA);
+            setShouldSkip(true);
+            break;
+          case 3:
+            expect(loading).toBeFalsy();
+            expect(data).toBeUndefined();
+            break;
+          case 4:
+            throw new Error('Uh oh - we should have stopped polling!');
+          default:
+          // Do nothing
+        }
+        renderCount += 1;
+        return null;
+      };
+
+      render(
+        <MockedProvider mocks={CAR_MOCKS}>
+          <Component />
+        </MockedProvider>
+      );
+
+      return wait(() => {
+        expect(renderCount).toBe(4);
+      });
+    });
+
+    itAsync('should stop polling when the component is unmounted', async (resolve, reject) => {
+      const mockLink = new MockLink(CAR_MOCKS);
+      const linkRequestSpy = jest.spyOn(mockLink, 'request');
+      let renderCount = 0;
+      const QueryComponent = ({ unmount }: { unmount: () => void }) => {
+        const { data, loading } = useQuery(CAR_QUERY, { pollInterval: 10 });
+        switch (renderCount) {
+          case 0:
+            expect(loading).toBeTruthy();
+            break;
+          case 1:
+            expect(loading).toBeFalsy();
+            expect(data).toEqual(CAR_RESULT_DATA);
+            expect(linkRequestSpy).toHaveBeenCalledTimes(1);
+            break;
+          case 2:
+            expect(loading).toBeFalsy();
+            expect(data).toEqual(CAR_RESULT_DATA);
+            expect(linkRequestSpy).toHaveBeenCalledTimes(2);
+            unmount();
+            break;
+          default:
+        }
+        renderCount += 1;
+        return null;
+      };
+
+      const Component = () => {
+        const [queryMounted, setQueryMounted] = useState(true);
+        const unmount = () => setTimeout(() => setQueryMounted(false), 0);
+        return <>{queryMounted && <QueryComponent unmount={unmount} />}</>;
+      };
+
+      render(
+        <MockedProvider mocks={CAR_MOCKS} link={mockLink}>
+          <Component />
+        </MockedProvider>
+      );
+
+      return wait(() => {
+        expect(linkRequestSpy).toHaveBeenCalledTimes(2);
+      }).then(resolve, reject);
+    });
+
+    it(
+      'should not throw an error if `stopPolling` is called manually after ' +
+        'a component has unmounted (even though polling has already been ' +
+        'stopped automatically)',
+      async () => {
+        let unmount: any;
+        let renderCount = 0;
+        const Component = () => {
+          const { data, loading, stopPolling } = useQuery(CAR_QUERY, {
+            pollInterval: 10
+          });
+          switch (renderCount) {
+            case 0:
+              expect(loading).toBeTruthy();
+              break;
+            case 1:
+              expect(loading).toBeFalsy();
+              expect(data).toEqual(CAR_RESULT_DATA);
+              setTimeout(() => {
+                unmount();
+                stopPolling();
+              });
+              break;
+            default:
+          }
+          renderCount += 1;
+          return null;
+        };
+
+        unmount = render(
+          <MockedProvider mocks={CAR_MOCKS}>
+            <Component />
+          </MockedProvider>
+        ).unmount;
+
+        return wait(() => {
+          expect(renderCount).toBe(2);
+        });
+      }
+    );
+
+    it('should set called to true by default', () => {
+      const Component = () => {
+        const { loading, called } = useQuery(CAR_QUERY);
+        expect(loading).toBeTruthy();
+        expect(called).toBeTruthy();
+        return null;
+      };
+
+      render(
+        <MockedProvider mocks={CAR_MOCKS}>
+          <Component />
+        </MockedProvider>
+      );
+    });
+  });
+
+  describe('Error handling', () => {
+    it("should render GraphQLError's", async () => {
+      const query = gql`
+        query TestQuery {
+          rates(currency: "USD") {
+            rate
+          }
+        }
+      `;
+
+      const mocks = [
+        {
+          request: { query },
+          result: {
+            errors: [new GraphQLError('forced error')]
+          }
+        }
+      ];
+
+      const Component = () => {
+        const { loading, error } = useQuery(query);
+        if (!loading) {
+          expect(error).toBeDefined();
+          expect(error!.message).toEqual('GraphQL error: forced error');
+        }
+        return null;
+      };
+
+      render(
+        <MockedProvider mocks={mocks}>
+          <Component />
+        </MockedProvider>
+      );
+
+      return wait();
+    });
+
+    it('should only call onError callbacks once', async () => {
+      const query = gql`
+        query SomeQuery {
+          stuff {
+            thing
+          }
+        }
+      `;
+
+      const resultData = { stuff: { thing: 'it!', __typename: 'Stuff' } };
+
+      let callCount = 0;
+      const link = new ApolloLink(() => {
+        if (!callCount) {
+          callCount += 1;
+          return new Observable(observer => {
+            observer.error(new Error('Oh no!'));
+          });
+        } else {
+          return Observable.of({ data: resultData });
+        }
+      });
+
+      const client = new ApolloClient({
+        link,
+        cache: new InMemoryCache()
+      });
+
+      let onError;
+      const onErrorPromise = new Promise(resolve => onError = resolve);
+
+      let renderCount = 0;
+      const Component = () => {
+        const { loading, error, refetch, data, networkStatus } = useQuery(
+          query,
+          {
+            onError,
+            notifyOnNetworkStatusChange: true
+          }
+        );
+
+        switch (++renderCount) {
+          case 1:
+            expect(loading).toBeTruthy();
+            break;
+          case 2:
+            expect(loading).toBeFalsy();
+            expect(error).toBeDefined();
+            expect(error!.message).toEqual('Network error: Oh no!');
+            onErrorPromise.then(() => refetch());
+            break;
+          case 3:
+            expect(loading).toBeTruthy();
+            break;
+          case 4:
+            expect(loading).toBeFalsy();
+            expect(data).toEqual(resultData);
+            break;
+          default: // Do nothing
+        }
+
+        return null;
+      };
+
+      render(
+        <ApolloProvider client={client}>
+          <Component />
+        </ApolloProvider>
+      );
+
+      return wait(() => {
+        expect(renderCount).toBe(4);
+      });
+    });
+
+    it('should persist errors on re-render if they are still valid', async () => {
+      const query = gql`
+        query SomeQuery {
+          stuff {
+            thing
+          }
+        }
+      `;
+
+      const mocks = [
+        {
+          request: { query },
+          result: {
+            errors: [new GraphQLError('forced error')]
+          }
+        }
+      ];
+
+      let renderCount = 0;
+      function App() {
+        const [_, forceUpdate] = useReducer(x => x + 1, 0);
+        const { loading, error } = useQuery(query);
+
+        switch (renderCount) {
+          case 0:
+            expect(loading).toBeTruthy();
+            expect(error).toBeUndefined();
+            break;
+          case 1:
+            expect(error).toBeDefined();
+            expect(error!.message).toEqual('GraphQL error: forced error');
+            setTimeout(() => {
+              forceUpdate(0);
+            });
+            break;
+          case 2:
+            expect(error).toBeDefined();
+            expect(error!.message).toEqual('GraphQL error: forced error');
+            break;
+          default: // Do nothing
+        }
+
+        renderCount += 1;
+        return null;
+      }
+
+      render(
+        <MockedProvider mocks={mocks}>
+          <App />
+        </MockedProvider>
+      );
+
+      return wait(() => {
+        expect(renderCount).toBe(3);
+      });
+    });
+
+    it(
+      'should persist errors on re-render when inlining onError and/or ' +
+        'onCompleted callbacks',
+      async () => {
+        const query = gql`
+          query SomeQuery {
+            stuff {
+              thing
+            }
+          }
+        `;
+
+        const mocks = [
+          {
+            request: { query },
+            result: {
+              errors: [new GraphQLError('forced error')]
+            }
+          }
+        ];
+
+        let renderCount = 0;
+        function App() {
+          const [_, forceUpdate] = useReducer(x => x + 1, 0);
+          const { loading, error } = useQuery(query, {
+            onError: () => {},
+            onCompleted: () => {}
+          });
+
+          switch (renderCount) {
+            case 0:
+              expect(loading).toBeTruthy();
+              expect(error).toBeUndefined();
+              break;
+            case 1:
+              expect(error).toBeDefined();
+              expect(error!.message).toEqual('GraphQL error: forced error');
+              setTimeout(() => {
+                forceUpdate(0);
+              });
+              break;
+            case 2:
+              expect(error).toBeDefined();
+              expect(error!.message).toEqual('GraphQL error: forced error');
+              break;
+            default: // Do nothing
+          }
+
+          renderCount += 1;
+          return null;
+        }
+
+        render(
+          <MockedProvider mocks={mocks}>
+            <App />
+          </MockedProvider>
+        );
+
+        return wait(() => {
+          expect(renderCount).toBe(3);
+        });
+      }
+    );
+
+    it('should render errors (different error messages) with loading done on refetch', async () => {
+      const query = gql`
+        query SomeQuery {
+          stuff {
+            thing
+          }
+        }
+      `;
+
+      const mocks = [
+        {
+          request: { query },
+          result: {
+            errors: [new GraphQLError('an error 1')]
+          }
+        },
+        {
+          request: { query },
+          result: {
+            errors: [new GraphQLError('an error 2')]
+          }
+        }
+      ];
+
+      let renderCount = 0;
+      function App() {
+        const { loading, error, refetch } = useQuery(query, {
+          notifyOnNetworkStatusChange: true
+        });
+
+        switch (renderCount) {
+          case 0:
+            expect(loading).toBeTruthy();
+            expect(error).toBeUndefined();
+            break;
+          case 1:
+            expect(loading).toBeFalsy();
+            expect(error).toBeDefined();
+            expect(error!.message).toEqual('GraphQL error: an error 1');
+            setTimeout(() => {
+              // catch here to avoid failing due to 'uncaught promise rejection'
+              refetch().catch(() => {});
+            });
+            break;
+          case 2:
+            expect(loading).toBeTruthy();
+            break;
+          case 3:
+            expect(loading).toBeFalsy();
+            expect(error).toBeDefined();
+            expect(error!.message).toEqual('GraphQL error: an error 2');
+            break;
+          default: // Do nothing
+        }
+
+        renderCount += 1;
+        return null;
+      }
+
+      render(
+        <MockedProvider mocks={mocks}>
+          <App />
+        </MockedProvider>
+      );
+
+      return wait(() => {
+        expect(renderCount).toBe(4);
+      });
+    });
+
+    it('should render errors (same error messages) with loading done on refetch', async () => {
+      const query = gql`
+        query SomeQuery {
+          stuff {
+            thing
+          }
+        }
+      `;
+
+      const mocks = [
+        {
+          request: { query },
+          result: {
+            errors: [new GraphQLError('same error message')]
+          }
+        },
+        {
+          request: { query },
+          result: {
+            errors: [new GraphQLError('same error message')]
+          }
+        }
+      ];
+
+      let renderCount = 0;
+      function App() {
+        const { loading, error, refetch } = useQuery(query, {
+          notifyOnNetworkStatusChange: true
+        });
+
+        switch (renderCount) {
+          case 0:
+            expect(loading).toBeTruthy();
+            expect(error).toBeUndefined();
+            break;
+          case 1:
+            expect(loading).toBeFalsy();
+            expect(error).toBeDefined();
+            expect(error!.message).toEqual('GraphQL error: same error message');
+            setTimeout(() => {
+              // catch here to avoid failing due to 'uncaught promise rejection'
+              refetch().catch(() => {});
+            });
+            break;
+          case 2:
+            expect(loading).toBeTruthy();
+            break;
+          case 3:
+            expect(loading).toBeFalsy();
+            expect(error).toBeDefined();
+            expect(error!.message).toEqual('GraphQL error: same error message');
+            break;
+          default: // Do nothing
+        }
+
+        renderCount += 1;
+        return null;
+      }
+
+      render(
+        <MockedProvider mocks={mocks}>
+          <App />
+        </MockedProvider>
+      );
+
+      return wait(() => {
+        expect(renderCount).toBe(4);
+      });
+    });
+
+    it('should render both success and errors (same error messages) with loading done on refetch', async () => {
+      const mocks = [
+        {
+          request: { query: CAR_QUERY },
+          result: {
+            errors: [new GraphQLError('same error message')]
+          }
+        },
+        {
+          request: { query: CAR_QUERY },
+          result: {
+            data: CAR_RESULT_DATA
+          }
+        },
+        {
+          request: { query: CAR_QUERY },
+          result: {
+            errors: [new GraphQLError('same error message')]
+          }
+        }
+      ];
+
+      let renderCount = 0;
+      function App() {
+        const { loading, data, error, refetch } = useQuery(CAR_QUERY, {
+          notifyOnNetworkStatusChange: true
+        });
+
+        switch (renderCount) {
+          case 0:
+            expect(loading).toBeTruthy();
+            expect(error).toBeUndefined();
+            break;
+          case 1:
+            expect(loading).toBeFalsy();
+            expect(error).toBeDefined();
+            expect(error!.message).toEqual('GraphQL error: same error message');
+            setTimeout(() => {
+              // catch here to avoid failing due to 'uncaught promise rejection'
+              refetch().catch(() => {});
+            });
+            break;
+          case 2:
+            expect(loading).toBeTruthy();
+            break;
+          case 3:
+            expect(loading).toBeFalsy();
+            expect(error).toBeUndefined();
+            expect(data).toEqual(CAR_RESULT_DATA);
+            setTimeout(() => {
+              // catch here to avoid failing due to 'uncaught promise rejection'
+              refetch().catch(() => {});
+            });
+            break;
+          case 4:
+            expect(loading).toBeTruthy();
+            break;
+          case 5:
+            expect(loading).toBeFalsy();
+            expect(error).toBeDefined();
+            expect(error!.message).toEqual('GraphQL error: same error message');
+            break;
+          default: // Do nothing
+        }
+
+        renderCount += 1;
+        return null;
+      }
+
+      render(
+        <MockedProvider mocks={mocks}>
+          <App />
+        </MockedProvider>
+      );
+
+      return wait(() => {
+        expect(renderCount).toBe(6);
+      });
+    });
+  });
+
+  describe('Pagination', () => {
+    it(
+      'should render `fetchMore.updateQuery` updated results with proper ' +
+        'loading status, when `notifyOnNetworkStatusChange` is true',
+      async () => {
+        const carQuery: DocumentNode = gql`
+          query cars($limit: Int) {
+            cars(limit: $limit) {
+              id
+              make
+              model
+              vin
+              __typename
+            }
+          }
+        `;
+
+        const carResults = {
+          cars: [
+            {
+              id: 1,
+              make: 'Audi',
+              model: 'RS8',
+              vin: 'DOLLADOLLABILL',
+              __typename: 'Car'
+            }
+          ]
+        };
+
+        const moreCarResults = {
+          cars: [
+            {
+              id: 2,
+              make: 'Audi',
+              model: 'eTron',
+              vin: 'TREESRGOOD',
+              __typename: 'Car'
+            }
+          ]
+        };
+
+        const mocks = [
+          {
+            request: { query: carQuery, variables: { limit: 1 } },
+            result: { data: carResults }
+          },
+          {
+            request: { query: carQuery, variables: { limit: 1 } },
+            result: { data: moreCarResults }
+          }
+        ];
+
+        let renderCount = 0;
+        function App() {
+          const { loading, data, fetchMore } = useQuery(carQuery, {
+            variables: { limit: 1 },
+            notifyOnNetworkStatusChange: true
+          });
+
+          switch (renderCount) {
+            case 0:
+              expect(loading).toBeTruthy();
+              break;
+            case 1:
+              expect(loading).toBeFalsy();
+              expect(data).toEqual(carResults);
+              fetchMore({
+                variables: {
+                  limit: 1
+                },
+                updateQuery: (prev, { fetchMoreResult }) => ({
+                  cars: [...prev.cars, ...fetchMoreResult.cars]
+                })
+              });
+              break;
+            case 2:
+              expect(loading).toBeTruthy();
+              break;
+            case 3:
+              expect(loading).toBeFalsy();
+              expect(data).toEqual({
+                cars: [carResults.cars[0]]
+              });
+              break;
+            case 4:
+              expect(data).toEqual({
+                cars: [carResults.cars[0], moreCarResults.cars[0]]
+              });
+              break;
+            default:
+          }
+
+          renderCount += 1;
+          return null;
+        }
+
+        render(
+          <MockedProvider mocks={mocks}>
+            <App />
+          </MockedProvider>
+        );
+
+        return wait(() => {
+          expect(renderCount).toBe(5);
+        });
+      }
+    );
+
+    it(
+      'should render `fetchMore.updateQuery` updated results with no ' +
+        'loading status, when `notifyOnNetworkStatusChange` is false',
+      async () => {
+        const carQuery: DocumentNode = gql`
+          query cars($limit: Int) {
+            cars(limit: $limit) {
+              id
+              make
+              model
+              vin
+              __typename
+            }
+          }
+        `;
+
+        const carResults = {
+          cars: [
+            {
+              id: 1,
+              make: 'Audi',
+              model: 'RS8',
+              vin: 'DOLLADOLLABILL',
+              __typename: 'Car'
+            }
+          ]
+        };
+
+        const moreCarResults = {
+          cars: [
+            {
+              id: 2,
+              make: 'Audi',
+              model: 'eTron',
+              vin: 'TREESRGOOD',
+              __typename: 'Car'
+            }
+          ]
+        };
+
+        const mocks = [
+          {
+            request: { query: carQuery, variables: { limit: 1 } },
+            result: { data: carResults }
+          },
+          {
+            request: { query: carQuery, variables: { limit: 1 } },
+            result: { data: moreCarResults }
+          }
+        ];
+
+        let renderCount = 0;
+        function App() {
+          const { loading, data, fetchMore } = useQuery(carQuery, {
+            variables: { limit: 1 },
+            notifyOnNetworkStatusChange: false
+          });
+
+          switch (renderCount) {
+            case 0:
+              expect(loading).toBeTruthy();
+              break;
+            case 1:
+              expect(loading).toBeFalsy();
+              expect(data).toEqual(carResults);
+              fetchMore({
+                variables: {
+                  limit: 1
+                },
+                updateQuery: (prev, { fetchMoreResult }) => ({
+                  cars: [...prev.cars, ...fetchMoreResult.cars]
+                })
+              });
+              break;
+            case 2:
+              expect(loading).toBeFalsy();
+              expect(data).toEqual({
+                cars: [carResults.cars[0], moreCarResults.cars[0]]
+              });
+              break;
+            default:
+          }
+
+          renderCount += 1;
+          return null;
+        }
+
+        render(
+          <MockedProvider mocks={mocks}>
+            <App />
+          </MockedProvider>
+        );
+
+        return wait(() => {
+          expect(renderCount).toBe(3);
+        });
+      }
+    );
+  });
+
+  describe('Refetching', () => {
+    it('should properly handle refetching with different variables', async () => {
+      const carQuery: DocumentNode = gql`
+        query cars($id: Int) {
+          cars(id: $id) {
+            id
+            make
+            model
+            vin
+            __typename
+          }
+        }
+      `;
+
+      const carData1 = {
+        cars: [
+          {
+            id: 1,
+            make: 'Audi',
+            model: 'RS8',
+            vin: 'DOLLADOLLABILL',
+            __typename: 'Car'
+          }
+        ]
+      };
+
+      const carData2 = {
+        cars: [
+          {
+            id: 2,
+            make: 'Audi',
+            model: 'eTron',
+            vin: 'TREESRGOOD',
+            __typename: 'Car'
+          }
+        ]
+      };
+
+      const mocks = [
+        {
+          request: { query: carQuery, variables: { id: 1 } },
+          result: { data: carData1 }
+        },
+        {
+          request: { query: carQuery, variables: { id: 2 } },
+          result: { data: carData2 }
+        },
+        {
+          request: { query: carQuery, variables: { id: 1 } },
+          result: { data: carData1 }
+        }
+      ];
+
+      let renderCount = 0;
+      function App() {
+        const { loading, data, refetch } = useQuery(carQuery, {
+          variables: { id: 1 }
+        });
+
+        switch (renderCount) {
+          case 0:
+            expect(loading).toBeTruthy();
+            break;
+          case 1:
+            expect(loading).toBeFalsy();
+            expect(data).toEqual(carData1);
+            refetch({ id: 2 });
+            break;
+          case 2:
+            expect(loading).toBeTruthy();
+            break;
+          case 3:
+            expect(loading).toBeFalsy();
+            expect(data).toEqual(carData2);
+            refetch({ id: 1 });
+            break;
+          case 4:
+            expect(loading).toBeTruthy();
+            break;
+          case 5:
+            expect(loading).toBeFalsy();
+            expect(data).toEqual(carData1);
+            break;
+          default:
+        }
+
+        renderCount += 1;
+        return null;
+      }
+
+      render(
+        <MockedProvider mocks={mocks}>
+          <App />
+        </MockedProvider>
+      );
+
+      return wait(() => {
+        expect(renderCount).toBe(6);
+      });
+    });
+  });
+
+  describe('Partial refetching', () => {
+    it(
+      'should attempt a refetch when the query result was marked as being ' +
+        'partial, the returned data was reset to an empty Object by the ' +
+        'Apollo Client QueryManager (due to a cache miss), and the ' +
+        '`partialRefetch` prop is `true`',
+      async () => {
+        const query: DocumentNode = gql`
+          query AllPeople($name: String!) {
+            allPeople(name: $name) {
+              people {
+                name
+              }
+            }
+          }
+        `;
+
+        interface Data {
+          allPeople: {
+            people: Array<{ name: string }>;
+          };
+        }
+
+        const peopleData: Data = {
+          allPeople: { people: [{ name: 'Luke Skywalker' }] }
+        };
+
+        const link = mockSingleLink(
+          {
+            request: {
+              query,
+              variables: {
+                someVar: 'abc123'
+              }
+            },
+            result: {
+              data: undefined
+            }
+          },
+          {
+            request: {
+              query,
+              variables: {
+                someVar: 'abc123'
+              }
+            },
+            result: {
+              data: peopleData
+            }
+          }
+        );
+
+        const client = new ApolloClient({
+          link,
+          cache: new InMemoryCache()
+        });
+
+        let renderCount = 0;
+        const Component = () => {
+          const { loading, data } = useQuery(query, {
+            variables: { someVar: 'abc123' },
+            partialRefetch: true
+          });
+
+          switch (renderCount) {
+            case 0:
+              // Initial loading render
+              expect(loading).toBeTruthy();
+              break;
+            case 1:
+              // `data` is missing and `partialRetch` is true, so a refetch
+              // is triggered and loading is set as true again
+              expect(loading).toBeTruthy();
+              expect(data).toBeUndefined();
+              break;
+            case 2:
+              // Refetch has completed
+              expect(loading).toBeFalsy();
+              expect(data).toEqual(peopleData);
+              break;
+            default:
+          }
+
+          renderCount += 1;
+          return null;
+        };
+
+        render(
+          <ApolloProvider client={client}>
+            <Component />
+          </ApolloProvider>
+        );
+
+        return wait(() => {
+          expect(renderCount).toBe(3);
+        });
+      }
+    );
+  });
+
+  describe('Callbacks', () => {
+    it(
+      'should pass loaded data to onCompleted when using the cache-only ' +
+        'fetch policy',
+      async () => {
+        const cache = new InMemoryCache();
+        const client = new ApolloClient({
+          cache,
+          resolvers: {}
+        });
+
+        cache.writeQuery({
+          query: CAR_QUERY,
+          data: CAR_RESULT_DATA
+        });
+
+        let onCompletedCalled = false;
+        const Component = () => {
+          const { loading, data } = useQuery(CAR_QUERY, {
+            fetchPolicy: 'cache-only',
+            onCompleted(data) {
+              onCompletedCalled = true;
+              expect(data).toBeDefined();
+            }
+          });
+          if (!loading) {
+            expect(data).toEqual(CAR_RESULT_DATA);
+          }
+          return null;
+        };
+
+        render(
+          <ApolloProvider client={client}>
+            <Component />
+          </ApolloProvider>
+        );
+
+        return wait(() => {
+          expect(onCompletedCalled).toBeTruthy();
+        });
+      }
+    );
+
+    it('should only call onCompleted once per query run', async () => {
+      const cache = new InMemoryCache();
+      const client = new ApolloClient({
+        cache,
+        resolvers: {}
+      });
+
+      cache.writeQuery({
+        query: CAR_QUERY,
+        data: CAR_RESULT_DATA
+      });
+
+      let onCompletedCount = 0;
+      const Component = () => {
+        const { loading, data } = useQuery(CAR_QUERY, {
+          fetchPolicy: 'cache-only',
+          onCompleted() {
+            onCompletedCount += 1;
+          }
+        });
+        if (!loading) {
+          expect(data).toEqual(CAR_RESULT_DATA);
+        }
+        return null;
+      };
+
+      render(
+        <ApolloProvider client={client}>
+          <Component />
+        </ApolloProvider>
+      );
+
+      return wait(() => {
+        expect(onCompletedCount).toBe(1);
+      });
+    });
+  });
+});
diff --git a/src/react/hooks/__tests__/useSubscription.test.tsx b/src/react/hooks/__tests__/useSubscription.test.tsx
new file mode 100644
--- /dev/null
+++ b/src/react/hooks/__tests__/useSubscription.test.tsx
@@ -0,0 +1,298 @@
+import { render, cleanup, wait } from '@testing-library/react';
+import gql from 'graphql-tag';
+
+import { MockSubscriptionLink } from '../../../utilities/testing/mocking/mockSubscriptionLink';
+import { ApolloClient } from '../../../ApolloClient';
+import { InMemoryCache as Cache } from '../../../cache/inmemory/inMemoryCache';
+import { ApolloProvider } from '../../context/ApolloProvider';
+import { useSubscription } from '../useSubscription';
+import { requireReactLazily } from '../../react';
+
+const React = requireReactLazily();
+
+describe('useSubscription Hook', () => {
+  afterEach(cleanup);
+
+  it('should handle a simple subscription properly', async () => {
+    const subscription = gql`
+      subscription {
+        car {
+          make
+        }
+      }
+    `;
+
+    const results = ['Audi', 'BMW', 'Mercedes', 'Hyundai'].map(make => ({
+      result: { data: { car: { make } } }
+    }));
+
+    const link = new MockSubscriptionLink();
+    const client = new ApolloClient({
+      link,
+      cache: new Cache({ addTypename: false })
+    });
+
+    let renderCount = 0;
+    const Component = () => {
+      const { loading, data, error } = useSubscription(subscription);
+      switch (renderCount) {
+        case 0:
+          expect(loading).toBe(true);
+          expect(error).toBeUndefined();
+          expect(data).toBeUndefined();
+          break;
+        case 1:
+          expect(loading).toBe(false);
+          expect(data).toEqual(results[0].result.data);
+          break;
+        case 2:
+          expect(loading).toBe(false);
+          expect(data).toEqual(results[1].result.data);
+          break;
+        case 3:
+          expect(loading).toBe(false);
+          expect(data).toEqual(results[2].result.data);
+          break;
+        case 4:
+          expect(loading).toBe(false);
+          expect(data).toEqual(results[3].result.data);
+          break;
+        default:
+      }
+      setTimeout(() => {
+        renderCount <= results.length &&
+          link.simulateResult(results[renderCount - 1]);
+      });
+      renderCount += 1;
+      return null;
+    };
+
+    render(
+      <ApolloProvider client={client}>
+        <Component />
+      </ApolloProvider>
+    );
+
+    return wait(() => {
+      expect(renderCount).toBe(5);
+    });
+  });
+
+  it('should cleanup after the subscription component has been unmounted', async () => {
+    const subscription = gql`
+      subscription {
+        car {
+          make
+        }
+      }
+    `;
+
+    const results = [
+      {
+        result: { data: { car: { make: 'Pagani' } } }
+      }
+    ];
+
+    const link = new MockSubscriptionLink();
+    const client = new ApolloClient({
+      link,
+      cache: new Cache({ addTypename: false })
+    });
+
+    let renderCount = 0;
+    let onSubscriptionDataCount = 0;
+    let unmount: any;
+
+    const Component = () => {
+      const { loading, data, error } = useSubscription(subscription, {
+        onSubscriptionData() {
+          onSubscriptionDataCount += 1;
+        }
+      });
+      switch (renderCount) {
+        case 0:
+          expect(loading).toBe(true);
+          expect(error).toBeUndefined();
+          expect(data).toBeUndefined();
+          link.simulateResult(results[0]);
+          break;
+        case 1:
+          expect(loading).toBe(false);
+          expect(data).toEqual(results[0].result.data);
+
+          setTimeout(() => {
+            expect(onSubscriptionDataCount).toEqual(1);
+
+            // After the component has been unmounted, the internal
+            // ObservableQuery should be stopped, meaning it shouldn't
+            // receive any new data (so the onSubscriptionDataCount should
+            // stay at 1).
+            unmount();
+            link.simulateResult(results[0]);
+          });
+          break;
+        default:
+      }
+      renderCount += 1;
+      return null;
+    };
+
+    unmount = render(
+      <ApolloProvider client={client}>
+        <Component />
+      </ApolloProvider>
+    ).unmount;
+
+    return wait(() => {
+      expect(onSubscriptionDataCount).toEqual(1);
+    });
+  });
+
+  it('should never execute a subscription with the skip option', async () => {
+    const subscription = gql`
+      subscription {
+        car {
+          make
+        }
+      }
+    `;
+
+    const link = new MockSubscriptionLink();
+    const client = new ApolloClient({
+      link,
+      cache: new Cache({ addTypename: false })
+    });
+
+    let renderCount = 0;
+    let onSubscriptionDataCount = 0;
+    let unmount: any;
+
+    const Component = () => {
+      const { loading, data, error } = useSubscription(subscription, {
+        skip: true,
+        onSubscriptionData() {
+          onSubscriptionDataCount += 1;
+        }
+      });
+      switch (renderCount) {
+        case 0:
+          expect(loading).toBe(false);
+          expect(error).toBeUndefined();
+          expect(data).toBeUndefined();
+          setTimeout(() => {
+            unmount();
+          });
+          break;
+        default:
+      }
+      renderCount += 1;
+      return null;
+    };
+
+    unmount = render(
+      <ApolloProvider client={client}>
+        <Component />
+      </ApolloProvider>
+    ).unmount;
+
+    return wait(() => {
+      expect(onSubscriptionDataCount).toEqual(0);
+      expect(renderCount).toEqual(1);
+    });
+  });
+
+  it('should create a subscription after skip has changed from true to a falsy value', async () => {
+    const subscription = gql`
+      subscription {
+        car {
+          make
+        }
+      }
+    `;
+
+    const results = [
+      {
+        result: { data: { car: { make: 'Pagani' } } }
+      },
+      {
+        result: { data: { car: { make: 'Scoop' } } }
+      }
+    ];
+
+    const link = new MockSubscriptionLink();
+    const client = new ApolloClient({
+      link,
+      cache: new Cache({ addTypename: false })
+    });
+
+    let renderCount = 0;
+    let unmount: any;
+
+    const Component = () => {
+      const [, triggerRerender] = React.useState(0);
+      const [skip, setSkip] = React.useState(true);
+      const { loading, data, error } = useSubscription(subscription, {
+        skip
+      });
+      switch (renderCount) {
+        case 0:
+          expect(loading).toBe(false);
+          expect(error).toBeUndefined();
+          expect(data).toBeUndefined();
+          setSkip(false);
+          break;
+        case 1:
+          expect(loading).toBe(true);
+          expect(error).toBeUndefined();
+          expect(data).toBeUndefined();
+          link.simulateResult(results[0]);
+          break;
+        case 2:
+          expect(loading).toBe(false);
+          expect(data).toEqual(results[0].result.data);
+          setSkip(true);
+          break;
+        case 3:
+          expect(loading).toBe(false);
+          expect(data).toBeUndefined();
+          expect(error).toBeUndefined();
+          // ensure state persists across rerenders
+          triggerRerender(i => i + 1);
+          break;
+        case 4:
+          expect(loading).toBe(false);
+          expect(data).toBeUndefined();
+          expect(error).toBeUndefined();
+          setSkip(false);
+          break;
+        case 5:
+          expect(loading).toBe(true);
+          expect(error).toBeUndefined();
+          expect(data).toBeUndefined();
+          link.simulateResult(results[1]);
+          break;
+        case 6:
+          expect(loading).toBe(false);
+          expect(error).toBeUndefined();
+          expect(data).toEqual(results[1].result.data);
+          setTimeout(() => {
+            unmount();
+          });
+          break;
+        default:
+      }
+      renderCount += 1;
+      return null;
+    };
+
+    unmount = render(
+      <ApolloProvider client={client}>
+        <Component />
+      </ApolloProvider>
+    ).unmount;
+
+    return wait(() => {
+      expect(renderCount).toEqual(7);
+    });
+  });
+});
diff --git a/src/react/parser/__tests__/parser.test.ts b/src/react/parser/__tests__/parser.test.ts
new file mode 100644
--- /dev/null
+++ b/src/react/parser/__tests__/parser.test.ts
@@ -0,0 +1,227 @@
+import gql from 'graphql-tag';
+
+import { parser, DocumentType } from '../parser';
+
+type OperationDefinition = any;
+
+describe('parser', () => {
+  it('should error if both a query and a mutation is present', () => {
+    const query = gql`
+      query {
+        user {
+          name
+        }
+      }
+
+      mutation($t: String) {
+        addT(t: $t) {
+          user {
+            name
+          }
+        }
+      }
+    `;
+
+    expect(parser.bind(null, query)).toThrowError(/react-apollo only supports/);
+  });
+
+  it('should error if multiple operations are present', () => {
+    const query = gql`
+      query One {
+        user {
+          name
+        }
+      }
+
+      query Two {
+        user {
+          name
+        }
+      }
+    `;
+
+    expect(parser.bind(null, query)).toThrowError(/react-apollo only supports/);
+  });
+
+  it('should error if not a DocumentNode', () => {
+    const query = `
+      query One { user { name } }
+    `;
+    expect(parser.bind(null, query as any)).toThrowError(
+      /not a valid GraphQL DocumentNode/
+    );
+  });
+
+  it('should return the name of the operation', () => {
+    const query = gql`
+      query One {
+        user {
+          name
+        }
+      }
+    `;
+    expect(parser(query).name).toBe('One');
+
+    const mutation = gql`
+      mutation One {
+        user {
+          name
+        }
+      }
+    `;
+    expect(parser(mutation).name).toBe('One');
+
+    const subscription = gql`
+      subscription One {
+        user {
+          name
+        }
+      }
+    `;
+    expect(parser(subscription).name).toBe('One');
+  });
+
+  it('should return data as the name of the operation if not named', () => {
+    const query = gql`
+      query {
+        user {
+          name
+        }
+      }
+    `;
+    expect(parser(query).name).toBe('data');
+
+    const unnamedQuery = gql`
+      {
+        user {
+          name
+        }
+      }
+    `;
+    expect(parser(unnamedQuery).name).toBe('data');
+
+    const mutation = gql`
+      mutation {
+        user {
+          name
+        }
+      }
+    `;
+    expect(parser(mutation).name).toBe('data');
+
+    const subscription = gql`
+      subscription {
+        user {
+          name
+        }
+      }
+    `;
+    expect(parser(subscription).name).toBe('data');
+  });
+
+  it('should return the type of operation', () => {
+    const query = gql`
+      query One {
+        user {
+          name
+        }
+      }
+    `;
+    expect(parser(query).type).toBe(DocumentType.Query);
+
+    const unnamedQuery = gql`
+      {
+        user {
+          name
+        }
+      }
+    `;
+    expect(parser(unnamedQuery).type).toBe(DocumentType.Query);
+
+    const mutation = gql`
+      mutation One {
+        user {
+          name
+        }
+      }
+    `;
+    expect(parser(mutation).type).toBe(DocumentType.Mutation);
+
+    const subscription = gql`
+      subscription One {
+        user {
+          name
+        }
+      }
+    `;
+    expect(parser(subscription).type).toBe(DocumentType.Subscription);
+  });
+
+  it('should return the variable definitions of the operation', () => {
+    const query = gql`
+      query One($t: String!) {
+        user(t: $t) {
+          name
+        }
+      }
+    `;
+    let definition = query.definitions[0] as OperationDefinition;
+    expect(parser(query).variables).toEqual(definition.variableDefinitions);
+
+    const mutation = gql`
+      mutation One($t: String!) {
+        user(t: $t) {
+          name
+        }
+      }
+    `;
+    definition = mutation.definitions[0] as OperationDefinition;
+    expect(parser(mutation).variables).toEqual(definition.variableDefinitions);
+
+    const subscription = gql`
+      subscription One($t: String!) {
+        user(t: $t) {
+          name
+        }
+      }
+    `;
+    definition = subscription.definitions[0] as OperationDefinition;
+    expect(parser(subscription).variables).toEqual(
+      definition.variableDefinitions
+    );
+  });
+
+  it('should not error if the operation has no variables', () => {
+    const query = gql`
+      query {
+        user(t: $t) {
+          name
+        }
+      }
+    `;
+    let definition = query.definitions[0] as OperationDefinition;
+    expect(parser(query).variables).toEqual(definition.variableDefinitions);
+
+    const mutation = gql`
+      mutation {
+        user(t: $t) {
+          name
+        }
+      }
+    `;
+    definition = mutation.definitions[0] as OperationDefinition;
+    expect(parser(mutation).variables).toEqual(definition.variableDefinitions);
+
+    const subscription = gql`
+      subscription {
+        user(t: $t) {
+          name
+        }
+      }
+    `;
+    definition = subscription.definitions[0] as OperationDefinition;
+    expect(parser(subscription).variables).toEqual(
+      definition.variableDefinitions
+    );
+  });
+});
diff --git a/packages/apollo-utilities/src/util/__tests__/assign.ts b/src/utilities/common/__tests__/assign.ts
similarity index 100%
rename from packages/apollo-utilities/src/util/__tests__/assign.ts
rename to src/utilities/common/__tests__/assign.ts
diff --git a/packages/apollo-utilities/src/util/__tests__/cloneDeep.ts b/src/utilities/common/__tests__/cloneDeep.ts
similarity index 100%
rename from packages/apollo-utilities/src/util/__tests__/cloneDeep.ts
rename to src/utilities/common/__tests__/cloneDeep.ts
diff --git a/packages/apollo-utilities/src/util/__tests__/environment.ts b/src/utilities/common/__tests__/environment.ts
similarity index 78%
rename from packages/apollo-utilities/src/util/__tests__/environment.ts
rename to src/utilities/common/__tests__/environment.ts
--- a/packages/apollo-utilities/src/util/__tests__/environment.ts
+++ b/src/utilities/common/__tests__/environment.ts
@@ -1,4 +1,4 @@
-import { isEnv, isProduction, isDevelopment, isTest } from '../environment';
+import { isEnv, isDevelopment, isTest } from '../environment';
 
 describe('environment', () => {
   let keepEnv: string | undefined;
@@ -27,18 +27,6 @@ describe('environment', () => {
     });
   });
 
-  describe('isProduction', () => {
-    it('should return true if in production', () => {
-      process.env.NODE_ENV = 'production';
-      expect(isProduction()).toBe(true);
-    });
-
-    it('should return false if not in production', () => {
-      process.env.NODE_ENV = 'test';
-      expect(!isProduction()).toBe(true);
-    });
-  });
-
   describe('isTest', () => {
     it('should return true if in test', () => {
       process.env.NODE_ENV = 'test';
diff --git a/packages/apollo-utilities/src/util/__tests__/maybeDeepFeeze.ts b/src/utilities/common/__tests__/maybeDeepFeeze.ts
similarity index 100%
rename from packages/apollo-utilities/src/util/__tests__/maybeDeepFeeze.ts
rename to src/utilities/common/__tests__/maybeDeepFeeze.ts
diff --git a/src/utilities/common/__tests__/mergeDeep.ts b/src/utilities/common/__tests__/mergeDeep.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/common/__tests__/mergeDeep.ts
@@ -0,0 +1,326 @@
+import { mergeDeep, mergeDeepArray, DeepMerger } from '../mergeDeep';
+
+describe('mergeDeep', function() {
+  it('should return an object if first argument falsy', function() {
+    expect(mergeDeep()).toEqual({});
+    expect(mergeDeep(null)).toEqual({});
+    expect(mergeDeep(null, { foo: 42 })).toEqual({ foo: 42 });
+  });
+
+  it('should preserve identity for single arguments', function() {
+    const arg = Object.create(null);
+    expect(mergeDeep(arg)).toBe(arg);
+  });
+
+  it('should preserve identity when merging non-conflicting objects', function() {
+    const a = { a: { name: 'ay' } };
+    const b = { b: { name: 'bee' } };
+    const c = mergeDeep(a, b);
+    expect(c.a).toBe(a.a);
+    expect(c.b).toBe(b.b);
+    expect(c).toEqual({
+      a: { name: 'ay' },
+      b: { name: 'bee' },
+    });
+  });
+
+  it('should shallow-copy conflicting fields', function() {
+    const a = { conflict: { fromA: [1, 2, 3] } };
+    const b = { conflict: { fromB: [4, 5] } };
+    const c = mergeDeep(a, b);
+    expect(c.conflict).not.toBe(a.conflict);
+    expect(c.conflict).not.toBe(b.conflict);
+    expect(c.conflict.fromA).toBe(a.conflict.fromA);
+    expect(c.conflict.fromB).toBe(b.conflict.fromB);
+    expect(c).toEqual({
+      conflict: {
+        fromA: [1, 2, 3],
+        fromB: [4, 5],
+      },
+    });
+  });
+
+  it('should resolve conflicts among more than two objects', function() {
+    const sources = [];
+
+    for (let i = 0; i < 100; ++i) {
+      sources.push({
+        ['unique' + i]: { value: i },
+        conflict: {
+          ['from' + i]: { value: i },
+          nested: {
+            ['nested' + i]: { value: i },
+          },
+        },
+      });
+    }
+
+    const merged = mergeDeep(...sources);
+
+    sources.forEach((source, i) => {
+      expect(merged['unique' + i].value).toBe(i);
+      expect(source['unique' + i]).toBe(merged['unique' + i]);
+
+      expect(merged.conflict).not.toBe(source.conflict);
+      expect(merged.conflict['from' + i].value).toBe(i);
+      expect(merged.conflict['from' + i]).toBe(source.conflict['from' + i]);
+
+      expect(merged.conflict.nested).not.toBe(source.conflict.nested);
+      expect(merged.conflict.nested['nested' + i].value).toBe(i);
+      expect(merged.conflict.nested['nested' + i]).toBe(
+        source.conflict.nested['nested' + i],
+      );
+    });
+  });
+
+  it('can merge array elements', function() {
+    const a = [{ a: 1 }, { a: 'ay' }, 'a'];
+    const b = [{ b: 2 }, { b: 'bee' }, 'b'];
+    const c = [{ c: 3 }, { c: 'cee' }, 'c'];
+    const d = { 1: { d: 'dee' } };
+
+    expect(mergeDeep(a, b, c, d)).toEqual([
+      { a: 1, b: 2, c: 3 },
+      { a: 'ay', b: 'bee', c: 'cee', d: 'dee' },
+      'c',
+    ]);
+  });
+
+  it('lets the last conflicting value win', function() {
+    expect(mergeDeep('a', 'b', 'c')).toBe('c');
+
+    expect(
+      mergeDeep(
+        { a: 'a', conflict: 1 },
+        { b: 'b', conflict: 2 },
+        { c: 'c', conflict: 3 },
+      ),
+    ).toEqual({
+      a: 'a',
+      b: 'b',
+      c: 'c',
+      conflict: 3,
+    });
+
+    expect(mergeDeep(
+      ['a', ['b', 'c'], 'd'],
+      [/*empty*/, ['B'], 'D'],
+    )).toEqual(
+      ['a', ['B', 'c'], 'D'],
+    );
+
+    expect(mergeDeep(
+      ['a', ['b', 'c'], 'd'],
+      ['A', [/*empty*/, 'C']],
+    )).toEqual(
+      ['A', ['b', 'C'], 'd'],
+    );
+  });
+
+  it('mergeDeep returns the intersection of its argument types', function() {
+    const abc = mergeDeep({ str: "hi", a: 1 }, { a: 3, b: 2 }, { b: 1, c: 2 });
+    // The point of this test is that the following lines type-check without
+    // resorting to any `any` loopholes:
+    expect(abc.str.slice(0)).toBe("hi");
+    expect(abc.a * 2).toBe(6);
+    expect(abc.b - 0).toBe(1);
+    expect(abc.c / 2).toBe(1);
+  });
+
+  it('mergeDeepArray returns the supertype of its argument types', function() {
+    class F {
+      check() { return "ok" };
+    }
+    const fs: F[] = [new F, new F, new F];
+    // Although mergeDeepArray doesn't have the same tuple type awareness as
+    // mergeDeep, it does infer that F should be the return type here:
+    expect(mergeDeepArray(fs).check()).toBe("ok");
+  });
+
+  it('supports custom reconciler functions', function () {
+    const merger = new DeepMerger((target, source, key) => {
+      const targetValue = target[key];
+      const sourceValue = source[key];
+      if (Array.isArray(sourceValue)) {
+        if (!Array.isArray(targetValue)) {
+          return sourceValue;
+        }
+        return [...targetValue, ...sourceValue];
+      }
+      return this.merge(targetValue, sourceValue);
+    });
+
+    expect(merger.merge(
+      {
+        a: [1, 2, 3],
+        b: "replace me",
+      },
+      {
+        a: [4, 5],
+        b: ["I", "win"],
+      },
+    )).toEqual({
+      a: [1, 2, 3, 4, 5],
+      b: ["I", "win"],
+    });
+  });
+
+  it('returns original object references when possible', function () {
+    const target = {
+      a: 1,
+      b: {
+        c: 3,
+        d: 4,
+      },
+      e: 5,
+    };
+
+    expect(mergeDeep(target, {
+      b: {
+        c: 3,
+      },
+    })).toBe(target);
+
+    const partial = mergeDeep(target, {
+      a: 1,
+      b: {
+        c: 3,
+      },
+      e: "eee",
+    });
+
+    expect(partial).not.toBe(target);
+    expect(partial.b).toBe(target.b);
+
+    const multiple = mergeDeep(target, {
+      a: 1,
+    }, {
+      b: {
+        d: 4,
+      },
+    }, {
+      e: 5,
+    });
+
+    expect(multiple).toBe(target);
+
+    const targetWithArrays = {
+      a: 1,
+      b: [2, {
+        c: [3, 4],
+        d: 5,
+      }, 6],
+      e: [7, 8, 9],
+    };
+
+    expect(mergeDeep(targetWithArrays, {
+      e: [],
+    })).toBe(targetWithArrays);
+
+    expect(mergeDeep(targetWithArrays, {
+      e: [/*hole*/, /*hole*/, 9],
+    })).toBe(targetWithArrays);
+
+    expect(mergeDeep(targetWithArrays, {
+      a: 1,
+      e: [7, 8],
+    })).toBe(targetWithArrays);
+
+    expect(mergeDeep(targetWithArrays, {
+      b: [2, {
+        c: [],
+        d: 5,
+      }],
+    })).toBe(targetWithArrays);
+
+    expect(mergeDeep(targetWithArrays, {
+      b: [2, {
+        c: [3],
+        d: 5,
+      }, 6],
+      e: [],
+    })).toBe(targetWithArrays);
+
+    const nestedInequality = mergeDeep(targetWithArrays, {
+      b: [2, {
+        c: [3],
+        d: 5,
+      }, "wrong"],
+      e: [],
+    });
+
+    expect(nestedInequality).not.toBe(targetWithArrays);
+    expect(nestedInequality.b).not.toBe(targetWithArrays.b);
+    expect(nestedInequality.b[1]).toEqual({
+      c: [3, 4],
+      d: 5,
+    });
+    expect(nestedInequality.b[1]).toBe(targetWithArrays.b[1]);
+
+    expect(mergeDeep(
+      targetWithArrays,
+      JSON.parse(JSON.stringify(targetWithArrays)),
+      JSON.parse(JSON.stringify(targetWithArrays)),
+      JSON.parse(JSON.stringify(targetWithArrays)),
+    )).toBe(targetWithArrays);
+  });
+
+  it("provides optional context to reconciler function", function () {
+    const contextObject = {
+      contextWithSpaces: "c o n t e x t",
+    };
+
+    const shallowContextValues: any[] = [];
+    const shallowMerger = new DeepMerger(
+      function(target, source, property, context: typeof contextObject) {
+        shallowContextValues.push(context);
+        // Deliberately not passing context down to nested levels.
+        return this.merge(target[property], source[property]);
+      },
+    );
+
+    const typicalContextValues: any[] = [];
+    const typicalMerger = new DeepMerger<typeof contextObject>(
+      function(target, source, property, context) {
+        typicalContextValues.push(context);
+        // Passing context down this time.
+        return this.merge(target[property], source[property], context);
+      },
+    );
+
+    const left = {
+      a: 1,
+      b: {
+        c: 2,
+        d: [3, 4],
+      },
+      e: 5,
+    };
+
+    const right = {
+      b: {
+        d: [3, 4, 5],
+      },
+    };
+
+    const expected = {
+      a: 1,
+      b: {
+        c: 2,
+        d: [3, 4, 5],
+      },
+      e: 5,
+    };
+
+    expect(shallowMerger.merge(left, right, contextObject)).toEqual(expected);
+    expect(typicalMerger.merge(left, right, contextObject)).toEqual(expected);
+
+    expect(shallowContextValues.length).toBe(2);
+    expect(shallowContextValues[0]).toBe(contextObject);
+    expect(shallowContextValues[1]).toBeUndefined();
+
+    expect(typicalContextValues.length).toBe(2);
+    expect(typicalContextValues[0]).toBe(contextObject);
+    expect(typicalContextValues[1]).toBe(contextObject);
+  });
+});
diff --git a/packages/apollo-utilities/src/__tests__/directives.ts b/src/utilities/graphql/__tests__/directives.ts
similarity index 100%
rename from packages/apollo-utilities/src/__tests__/directives.ts
rename to src/utilities/graphql/__tests__/directives.ts
diff --git a/packages/apollo-utilities/src/__tests__/fragments.ts b/src/utilities/graphql/__tests__/fragments.ts
similarity index 89%
rename from packages/apollo-utilities/src/__tests__/fragments.ts
rename to src/utilities/graphql/__tests__/fragments.ts
--- a/packages/apollo-utilities/src/__tests__/fragments.ts
+++ b/src/utilities/graphql/__tests__/fragments.ts
@@ -5,7 +5,12 @@ import { disableFragmentWarnings } from 'graphql-tag';
 // Turn off warnings for repeated fragment names
 disableFragmentWarnings();
 
-import { getFragmentQueryDocument } from '../fragments';
+import {
+  getFragmentQueryDocument,
+  createFragmentMap,
+  FragmentMap
+} from '../fragments';
+import { getFragmentDefinitions } from '../getFromAST';
 
 describe('getFragmentQueryDocument', () => {
   it('will throw an error if there is an operation', () => {
@@ -325,3 +330,26 @@ describe('getFragmentQueryDocument', () => {
     );
   });
 });
+
+it('should create the fragment map correctly', () => {
+  const fragments = getFragmentDefinitions(gql`
+    fragment authorDetails on Author {
+      firstName
+      lastName
+    }
+
+    fragment moreAuthorDetails on Author {
+      address
+    }
+  `);
+  const fragmentMap = createFragmentMap(fragments);
+  const expectedTable: FragmentMap = {
+    authorDetails: fragments[0],
+    moreAuthorDetails: fragments[1],
+  };
+  expect(fragmentMap).toEqual(expectedTable);
+});
+
+it('should return an empty fragment map if passed undefined argument', () => {
+  expect(createFragmentMap(undefined)).toEqual({});
+});
diff --git a/packages/apollo-utilities/src/__tests__/getFromAST.ts b/src/utilities/graphql/__tests__/getFromAST.ts
similarity index 76%
rename from packages/apollo-utilities/src/__tests__/getFromAST.ts
rename to src/utilities/graphql/__tests__/getFromAST.ts
--- a/packages/apollo-utilities/src/__tests__/getFromAST.ts
+++ b/src/utilities/graphql/__tests__/getFromAST.ts
@@ -6,9 +6,6 @@ import {
   checkDocument,
   getFragmentDefinitions,
   getQueryDefinition,
-  getMutationDefinition,
-  createFragmentMap,
-  FragmentMap,
   getDefaultValues,
   getOperationName,
 } from '../getFromAST';
@@ -164,55 +161,6 @@ describe('AST utility functions', () => {
     }).toThrow();
   });
 
-  it('should get the correct mutation definition out of a mutation with multiple fragments', () => {
-    const mutationWithFragments = gql`
-      mutation {
-        createAuthor(firstName: "John", lastName: "Smith") {
-          ...authorDetails
-        }
-      }
-
-      fragment authorDetails on Author {
-        firstName
-        lastName
-      }
-    `;
-    const expectedDoc = gql`
-      mutation {
-        createAuthor(firstName: "John", lastName: "Smith") {
-          ...authorDetails
-        }
-      }
-    `;
-    const expectedResult: OperationDefinitionNode = expectedDoc
-      .definitions[0] as OperationDefinitionNode;
-    const actualResult = getMutationDefinition(mutationWithFragments);
-    expect(print(actualResult)).toEqual(print(expectedResult));
-  });
-
-  it('should create the fragment map correctly', () => {
-    const fragments = getFragmentDefinitions(gql`
-      fragment authorDetails on Author {
-        firstName
-        lastName
-      }
-
-      fragment moreAuthorDetails on Author {
-        address
-      }
-    `);
-    const fragmentMap = createFragmentMap(fragments);
-    const expectedTable: FragmentMap = {
-      authorDetails: fragments[0],
-      moreAuthorDetails: fragments[1],
-    };
-    expect(fragmentMap).toEqual(expectedTable);
-  });
-
-  it('should return an empty fragment map if passed undefined argument', () => {
-    expect(createFragmentMap(undefined)).toEqual({});
-  });
-
   it('should get the operation name out of a query', () => {
     const query = gql`
       query nameOfQuery {
@@ -293,24 +241,9 @@ describe('AST utility functions', () => {
         }
       `;
 
-      const complexMutation = gql`
-        mutation complexStuff(
-          $test: Input = { key1: ["value", "value2"], key2: { key3: 4 } }
-        ) {
-          complexStuff(test: $test) {
-            people {
-              name
-            }
-          }
-        }
-      `;
-
       expect(getDefaultValues(getQueryDefinition(basicQuery))).toEqual({
         first: 1,
       });
-      expect(getDefaultValues(getMutationDefinition(complexMutation))).toEqual({
-        test: { key1: ['value', 'value2'], key2: { key3: 4 } },
-      });
     });
   });
 });
diff --git a/packages/apollo-utilities/src/__tests__/storeUtils.ts b/src/utilities/graphql/__tests__/storeUtils.ts
similarity index 100%
rename from packages/apollo-utilities/src/__tests__/storeUtils.ts
rename to src/utilities/graphql/__tests__/storeUtils.ts
diff --git a/packages/apollo-utilities/src/__tests__/transform.ts b/src/utilities/graphql/__tests__/transform.ts
similarity index 67%
rename from packages/apollo-utilities/src/__tests__/transform.ts
rename to src/utilities/graphql/__tests__/transform.ts
--- a/packages/apollo-utilities/src/__tests__/transform.ts
+++ b/src/utilities/graphql/__tests__/transform.ts
@@ -8,7 +8,6 @@ disableFragmentWarnings();
 import {
   addTypenameToDocument,
   removeDirectivesFromDocument,
-  getDirectivesFromDocument,
   removeConnectionDirectiveFromDocument,
   removeArgumentsFromDocument,
   removeFragmentSpreadFromDocument,
@@ -803,408 +802,6 @@ describe('query transforms', () => {
   });
 });
 
-describe('getDirectivesFromDocument', () => {
-  it('should get query with fields of storage directive ', () => {
-    const query = gql`
-      query Simple {
-        field @storage(if: true)
-      }
-    `;
-
-    const expected = gql`
-      query Simple {
-        field @storage(if: true)
-      }
-    `;
-    const doc = getDirectivesFromDocument([{ name: 'storage' }], query);
-    expect(print(doc)).toBe(print(expected));
-  });
-
-  it('should get query with fields of storage directive [test function] ', () => {
-    const query = gql`
-      query Simple {
-        field @storage(if: true)
-      }
-    `;
-
-    const expected = gql`
-      query Simple {
-        field @storage(if: true)
-      }
-    `;
-    const test = ({ name: { value } }: { name: any }) => value === 'storage';
-    const doc = getDirectivesFromDocument([{ test }], query);
-    expect(print(doc)).toBe(print(expected));
-  });
-
-  it('should only get query with fields of storage directive ', () => {
-    const query = gql`
-      query Simple {
-        maybe @skip(if: false)
-        field @storage(if: true)
-      }
-    `;
-
-    const expected = gql`
-      query Simple {
-        field @storage(if: true)
-      }
-    `;
-    const doc = getDirectivesFromDocument([{ name: 'storage' }], query);
-    expect(print(doc)).toBe(print(expected));
-  });
-
-  it('should only get query with multiple fields of storage directive ', () => {
-    const query = gql`
-      query Simple {
-        maybe @skip(if: false)
-        field @storage(if: true)
-        other @storage
-      }
-    `;
-
-    const expected = gql`
-      query Simple {
-        field @storage(if: true)
-        other @storage
-      }
-    `;
-    const doc = getDirectivesFromDocument([{ name: 'storage' }], query);
-    expect(print(doc)).toBe(print(expected));
-  });
-
-  it('should get query with fields of both storage and client directives ', () => {
-    const query = gql`
-      query Simple {
-        maybe @skip(if: false)
-        field @storage(if: true)
-        user @client
-      }
-    `;
-
-    const expected = gql`
-      query Simple {
-        field @storage(if: true)
-        user @client
-      }
-    `;
-    const doc = getDirectivesFromDocument(
-      [{ name: 'storage' }, { name: 'client' }],
-      query,
-    );
-    expect(print(doc)).toBe(print(expected));
-  });
-
-  it('should get query with different types of directive matchers ', () => {
-    const query = gql`
-      query Simple {
-        maybe @skip(if: false)
-        field @storage(if: true)
-        user @client
-      }
-    `;
-
-    const expected = gql`
-      query Simple {
-        field @storage(if: true)
-        user @client
-      }
-    `;
-    const doc = getDirectivesFromDocument(
-      [
-        { name: 'storage' },
-        { test: directive => directive.name.value === 'client' },
-      ],
-      query,
-    );
-
-    expect(print(doc)).toBe(print(expected));
-  });
-
-  it('should get query with nested fields ', () => {
-    const query = gql`
-      query Simple {
-        user {
-          firstName @client
-          email
-        }
-      }
-    `;
-
-    const expected = gql`
-      query Simple {
-        user {
-          firstName @client
-        }
-      }
-    `;
-    const doc = getDirectivesFromDocument([{ name: 'client' }], query);
-    expect(print(doc)).toBe(print(expected));
-  });
-
-  it('should include all the nested fields of field that has client directive ', () => {
-    const query = gql`
-      query Simple {
-        user @client {
-          firstName
-          email
-        }
-      }
-    `;
-
-    const expected = gql`
-      query Simple {
-        user @client {
-          firstName
-          email
-        }
-      }
-    `;
-    const doc = getDirectivesFromDocument([{ name: 'client' }], query);
-    expect(print(doc)).toBe(print(expected));
-  });
-
-  it('should return null if the query is no longer valid', () => {
-    const query = gql`
-      query Simple {
-        field
-      }
-    `;
-    const doc = getDirectivesFromDocument([{ name: 'client' }], query);
-    expect(print(doc)).toBe(null);
-  });
-
-  it('should get query with client fields in fragment', function() {
-    const query = gql`
-      query Simple {
-        ...fragmentSpread
-      }
-
-      fragment fragmentSpread on Thing {
-        field @client
-        other
-      }
-    `;
-    const expected = gql`
-      query Simple {
-        ...fragmentSpread
-      }
-
-      fragment fragmentSpread on Thing {
-        field @client
-      }
-    `;
-    const doc = getDirectivesFromDocument([{ name: 'client' }], query);
-    expect(print(doc)).toBe(print(expected));
-  });
-
-  it('should get query with client fields in fragment with nested fields', function() {
-    const query = gql`
-      query Simple {
-        ...fragmentSpread
-      }
-
-      fragment fragmentSpread on Thing {
-        user {
-          firstName @client
-          lastName
-        }
-      }
-    `;
-    const expected = gql`
-      query Simple {
-        ...fragmentSpread
-      }
-
-      fragment fragmentSpread on Thing {
-        user {
-          firstName @client
-        }
-      }
-    `;
-    const doc = getDirectivesFromDocument([{ name: 'client' }], query);
-    expect(print(doc)).toBe(print(expected));
-  });
-
-  it('should get query with client fields in multiple fragments', function() {
-    const query = gql`
-      query Simple {
-        ...fragmentSpread
-        ...anotherFragmentSpread
-      }
-
-      fragment fragmentSpread on Thing {
-        field @client
-        other
-      }
-
-      fragment anotherFragmentSpread on AnotherThing {
-        user @client
-        product
-      }
-    `;
-    const expected = gql`
-      query Simple {
-        ...fragmentSpread
-        ...anotherFragmentSpread
-      }
-
-      fragment fragmentSpread on Thing {
-        field @client
-      }
-
-      fragment anotherFragmentSpread on AnotherThing {
-        user @client
-      }
-    `;
-    const doc = getDirectivesFromDocument([{ name: 'client' }], query);
-    expect(print(doc)).toBe(print(expected));
-  });
-
-  it("should return null if fragment didn't have client fields", function() {
-    const query = gql`
-      query Simple {
-        ...fragmentSpread
-      }
-
-      fragment fragmentSpread on Thing {
-        field
-      }
-    `;
-    const doc = getDirectivesFromDocument([{ name: 'client' }], query);
-    expect(print(doc)).toBe(print(null));
-  });
-
-  it('should get query with client fields when both fields and fragements are mixed', function() {
-    const query = gql`
-      query Simple {
-        user @client
-        product @storage
-        order
-        ...fragmentSpread
-      }
-
-      fragment fragmentSpread on Thing {
-        field @client
-        other
-      }
-    `;
-    const expected = gql`
-      query Simple {
-        user @client
-        ...fragmentSpread
-      }
-
-      fragment fragmentSpread on Thing {
-        field @client
-      }
-    `;
-    const doc = getDirectivesFromDocument([{ name: 'client' }], query);
-    expect(print(doc)).toBe(print(expected));
-  });
-
-  it('should get mutation with client fields', () => {
-    const query = gql`
-      mutation {
-        login @client
-      }
-    `;
-
-    const expected = gql`
-      mutation {
-        login @client
-      }
-    `;
-    const doc = getDirectivesFromDocument([{ name: 'client' }], query);
-    expect(print(doc)).toBe(print(expected));
-  });
-
-  it('should get mutation fields of client only', () => {
-    const query = gql`
-      mutation {
-        login @client
-        updateUser
-      }
-    `;
-
-    const expected = gql`
-      mutation {
-        login @client
-      }
-    `;
-    const doc = getDirectivesFromDocument([{ name: 'client' }], query);
-    expect(print(doc)).toBe(print(expected));
-  });
-
-  describe('includeAllFragments', () => {
-    it('= false: should remove the values without a client in fragment', () => {
-      const query = gql`
-        fragment client on ClientData {
-          hi @client
-          bye @storage
-          bar
-        }
-
-        query Mixed {
-          foo @client {
-            ...client
-          }
-          bar {
-            baz
-          }
-        }
-      `;
-
-      const expected = gql`
-        fragment client on ClientData {
-          hi @client
-        }
-
-        query Mixed {
-          foo @client {
-            ...client
-          }
-        }
-      `;
-      const doc = getDirectivesFromDocument([{ name: 'client' }], query, false);
-      expect(print(doc)).toBe(print(expected));
-    });
-
-    it('= true: should include the values without a client in fragment', () => {
-      const query = gql`
-        fragment client on ClientData {
-          hi @client
-          bye @storage
-          bar
-        }
-
-        query Mixed {
-          foo @client {
-            ...client
-          }
-          bar {
-            baz
-          }
-        }
-      `;
-
-      const expected = gql`
-        fragment client on ClientData {
-          hi @client
-        }
-
-        query Mixed {
-          foo @client {
-            ...client
-          }
-        }
-      `;
-      const doc = getDirectivesFromDocument([{ name: 'client' }], query, true);
-      expect(print(doc)).toBe(print(expected));
-    });
-  });
-});
-
 describe('removeClientSetsFromDocument', () => {
   it('should remove @client fields from document', () => {
     const query = gql`
diff --git a/packages/apollo-utilities/src/util/__tests__/stripSymbols.ts b/src/utilities/testing/__tests__/stripSymbols.ts
similarity index 100%
rename from packages/apollo-utilities/src/util/__tests__/stripSymbols.ts
rename to src/utilities/testing/__tests__/stripSymbols.ts
diff --git a/src/utilities/testing/index.ts b/src/utilities/testing/index.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/testing/index.ts
@@ -0,0 +1,8 @@
+export { MockedProvider } from './mocking/MockedProvider';
+export { MockLink, mockSingleLink, MockedResponse } from './mocking/mockLink';
+export {
+  MockSubscriptionLink,
+  mockObservableLink
+} from './mocking/mockSubscriptionLink';
+export { createMockClient } from './mocking/mockClient';
+export { stripSymbols } from './stripSymbols';
diff --git a/src/utilities/testing/itAsync.ts b/src/utilities/testing/itAsync.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/testing/itAsync.ts
@@ -0,0 +1,27 @@
+function wrap<TResult>(
+  original: (...args: any[]) => TResult,
+) {
+  return (
+    message: string,
+    callback: (
+      resolve: (result?: any) => void,
+      reject: (reason?: any) => void,
+    ) => any,
+    timeout?: number,
+  ) => original(message, function () {
+    return new Promise(
+      (resolve, reject) => callback.call(this, resolve, reject),
+    );
+  }, timeout);
+}
+
+const wrappedIt = wrap(it);
+export function itAsync(...args: Parameters<typeof wrappedIt>) {
+  return wrappedIt.apply(this, args);
+}
+
+export namespace itAsync {
+  export const only = wrap(it.only);
+  export const skip = wrap(it.skip);
+  export const todo = wrap(it.todo);
+}
diff --git a/src/utilities/testing/mocking/MockedProvider.tsx b/src/utilities/testing/mocking/MockedProvider.tsx
new file mode 100644
--- /dev/null
+++ b/src/utilities/testing/mocking/MockedProvider.tsx
@@ -0,0 +1,73 @@
+import React from 'react';
+
+import { ApolloClient, DefaultOptions } from '../../../ApolloClient';
+import { InMemoryCache as Cache } from '../../../cache/inmemory/inMemoryCache';
+import { ApolloProvider } from '../../../react/context/ApolloProvider';
+import { MockLink } from '../../../utilities/testing/mocking/mockLink';
+import { ApolloLink } from '../../../link/core/ApolloLink';
+import { Resolvers } from '../../../core/types';
+import { ApolloCache } from '../../../cache/core/cache';
+import { MockedResponse } from '../../../utilities/testing/mocking/mockLink';
+
+export interface MockedProviderProps<TSerializedCache = {}> {
+  mocks?: ReadonlyArray<MockedResponse>;
+  addTypename?: boolean;
+  defaultOptions?: DefaultOptions;
+  cache?: ApolloCache<TSerializedCache>;
+  resolvers?: Resolvers;
+  childProps?: object;
+  children?: React.ReactElement;
+  link?: ApolloLink;
+}
+
+export interface MockedProviderState {
+  client: ApolloClient<any>;
+}
+
+export class MockedProvider extends React.Component<
+  MockedProviderProps,
+  MockedProviderState
+> {
+  public static defaultProps: MockedProviderProps = {
+    addTypename: true
+  };
+
+  constructor(props: MockedProviderProps) {
+    super(props);
+
+    const {
+      mocks,
+      addTypename,
+      defaultOptions,
+      cache,
+      resolvers,
+      link
+    } = this.props;
+    const client = new ApolloClient({
+      cache: cache || new Cache({ addTypename }),
+      defaultOptions,
+      link: link || new MockLink(
+        mocks || [],
+        addTypename,
+      ),
+      resolvers,
+    });
+
+    this.state = { client };
+  }
+
+  public render() {
+    const { children, childProps } = this.props;
+    return children ? (
+      <ApolloProvider client={this.state.client}>
+        {React.cloneElement(React.Children.only(children), { ...childProps })}
+      </ApolloProvider>
+    ) : null;
+  }
+
+  public componentWillUnmount() {
+    // Since this.state.client was created in the constructor, it's this
+    // MockedProvider's responsibility to terminate it.
+    this.state.client.stop();
+  }
+}
diff --git a/src/utilities/testing/mocking/mockClient.ts b/src/utilities/testing/mocking/mockClient.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/testing/mocking/mockClient.ts
@@ -0,0 +1,20 @@
+import { DocumentNode } from 'graphql';
+
+import { ApolloClient } from '../../../ApolloClient';
+import { InMemoryCache } from '../../../cache/inmemory/inMemoryCache';
+import { NormalizedCacheObject } from '../../../cache/inmemory/types';
+import { mockSingleLink } from '../../../utilities/testing/mocking/mockLink';
+
+export function createMockClient<TData>(
+  data: TData,
+  query: DocumentNode,
+  variables = {},
+): ApolloClient<NormalizedCacheObject> {
+  return new ApolloClient({
+    link: mockSingleLink({
+      request: { query, variables },
+      result: { data },
+    }).setOnError(error => { throw error }),
+    cache: new InMemoryCache({ addTypename: false }),
+  });
+}
diff --git a/packages/apollo-client/src/__mocks__/mockFetch.ts b/src/utilities/testing/mocking/mockFetch.ts
similarity index 100%
rename from packages/apollo-client/src/__mocks__/mockFetch.ts
rename to src/utilities/testing/mocking/mockFetch.ts
diff --git a/src/utilities/testing/mocking/mockLink.ts b/src/utilities/testing/mocking/mockLink.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/testing/mocking/mockLink.ts
@@ -0,0 +1,178 @@
+import { print } from 'graphql/language/printer';
+import stringify from 'fast-json-stable-stringify';
+import { equal } from '@wry/equality';
+
+import { Observable } from '../../../utilities/observables/Observable';
+import { ApolloLink } from '../../../link/core/ApolloLink';
+import {
+  Operation,
+  GraphQLRequest,
+  FetchResult,
+} from '../../../link/core/types';
+import {
+  addTypenameToDocument,
+  removeClientSetsFromDocument,
+  removeConnectionDirectiveFromDocument,
+} from '../../../utilities/graphql/transform';
+import { cloneDeep } from '../../../utilities/common/cloneDeep';
+
+export type ResultFunction<T> = () => T;
+
+export interface MockedResponse {
+  request: GraphQLRequest;
+  result?: FetchResult | ResultFunction<FetchResult>;
+  error?: Error;
+  delay?: number;
+  newData?: ResultFunction<FetchResult>;
+}
+
+function requestToKey(request: GraphQLRequest, addTypename: Boolean): string {
+  const queryString =
+    request.query &&
+    print(addTypename ? addTypenameToDocument(request.query) : request.query);
+  const requestKey = { query: queryString };
+  return JSON.stringify(requestKey);
+}
+
+export class MockLink extends ApolloLink {
+  public operation: Operation;
+  public addTypename: Boolean = true;
+  private mockedResponsesByKey: { [key: string]: MockedResponse[] } = {};
+
+  constructor(
+    mockedResponses: ReadonlyArray<MockedResponse>,
+    addTypename: Boolean = true
+  ) {
+    super();
+    this.addTypename = addTypename;
+    if (mockedResponses) {
+      mockedResponses.forEach(mockedResponse => {
+        this.addMockedResponse(mockedResponse);
+      });
+    }
+  }
+
+  public addMockedResponse(mockedResponse: MockedResponse) {
+    const normalizedMockedResponse = this.normalizeMockedResponse(
+      mockedResponse
+    );
+    const key = requestToKey(
+      normalizedMockedResponse.request,
+      this.addTypename
+    );
+    let mockedResponses = this.mockedResponsesByKey[key];
+    if (!mockedResponses) {
+      mockedResponses = [];
+      this.mockedResponsesByKey[key] = mockedResponses;
+    }
+    mockedResponses.push(normalizedMockedResponse);
+  }
+
+  public request(operation: Operation): Observable<FetchResult> | null {
+    this.operation = operation;
+    const key = requestToKey(operation, this.addTypename);
+    let responseIndex;
+    const response = (this.mockedResponsesByKey[key] || []).find(
+      (res, index) => {
+        const requestVariables = operation.variables || {};
+        const mockedResponseVariables = res.request.variables || {};
+        if (
+          !equal(
+            stringify(requestVariables),
+            stringify(mockedResponseVariables)
+          )
+        ) {
+          return false;
+        }
+        responseIndex = index;
+        return true;
+      }
+    );
+
+    if (!response || typeof responseIndex === 'undefined') {
+      this.onError(new Error(
+        `No more mocked responses for the query: ${print(
+          operation.query
+        )}, variables: ${JSON.stringify(operation.variables)}`
+      ));
+    }
+
+    this.mockedResponsesByKey[key].splice(responseIndex, 1);
+
+    const { newData } = response;
+
+    if (newData) {
+      response.result = newData();
+      this.mockedResponsesByKey[key].push(response);
+    }
+
+    const { result, error, delay } = response;
+
+    if (!result && !error) {
+      this.onError(new Error(
+        `Mocked response should contain either result or error: ${key}`
+      ));
+    }
+
+    return new Observable(observer => {
+      let timer = setTimeout(
+        () => {
+          if (error) {
+            observer.error(error);
+          } else {
+            if (result) {
+              observer.next(
+                typeof result === 'function'
+                  ? (result as ResultFunction<FetchResult>)()
+                  : result
+              );
+            }
+            observer.complete();
+          }
+        },
+        delay ? delay : 0
+      );
+
+      return () => {
+        clearTimeout(timer);
+      };
+    });
+  }
+
+  private normalizeMockedResponse(
+    mockedResponse: MockedResponse
+  ): MockedResponse {
+    const newMockedResponse = cloneDeep(mockedResponse);
+    newMockedResponse.request.query = removeConnectionDirectiveFromDocument(
+      newMockedResponse.request.query
+    );
+    const query = removeClientSetsFromDocument(newMockedResponse.request.query);
+    if (query) {
+      newMockedResponse.request.query = query;
+    }
+    return newMockedResponse;
+  }
+}
+
+interface MockApolloLink extends ApolloLink {
+  operation?: Operation;
+}
+
+// Pass in multiple mocked responses, so that you can test flows that end up
+// making multiple queries to the server.
+// NOTE: The last arg can optionally be an `addTypename` arg.
+export function mockSingleLink(
+  ...mockedResponses: Array<any>
+): MockApolloLink {
+  // To pull off the potential typename. If this isn't a boolean, we'll just
+  // set it true later.
+  let maybeTypename = mockedResponses[mockedResponses.length - 1];
+  let mocks = mockedResponses.slice(0, mockedResponses.length - 1);
+
+  if (typeof maybeTypename !== 'boolean') {
+    mocks = mockedResponses;
+    maybeTypename = true;
+  }
+
+  return new MockLink(mocks, maybeTypename);
+}
diff --git a/src/utilities/testing/mocking/mockQueryManager.ts b/src/utilities/testing/mocking/mockQueryManager.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/testing/mocking/mockQueryManager.ts
@@ -0,0 +1,15 @@
+import { QueryManager } from '../../../core/QueryManager';
+import { mockSingleLink, MockedResponse } from './mockLink';
+import { InMemoryCache } from '../../../cache/inmemory/inMemoryCache';
+
+// Helper method for the tests that construct a query manager out of a
+// a list of mocked responses for a mocked network interface.
+export default (
+  reject: (reason: any) => any,
+  ...mockedResponses: MockedResponse[]
+) => {
+  return new QueryManager({
+    link: mockSingleLink(...mockedResponses).setOnError(reject),
+    cache: new InMemoryCache({ addTypename: false }),
+  });
+};
diff --git a/src/utilities/testing/mocking/mockSubscriptionLink.ts b/src/utilities/testing/mocking/mockSubscriptionLink.ts
new file mode 100644
--- /dev/null
+++ b/src/utilities/testing/mocking/mockSubscriptionLink.ts
@@ -0,0 +1,62 @@
+import { Observable } from '../../../utilities/observables/Observable';
+import { ApolloLink } from '../../../link/core/ApolloLink';
+import { FetchResult, Operation } from '../../../link/core/types';
+
+export interface MockedSubscription {
+  request: Operation;
+}
+
+export interface MockedSubscriptionResult {
+  result?: FetchResult;
+  error?: Error;
+  delay?: number;
+}
+
+export class MockSubscriptionLink extends ApolloLink {
+  public unsubscribers: any[] = [];
+  public setups: any[] = [];
+
+  private observer: any;
+
+  constructor() {
+    super();
+  }
+
+  public request(_req: any) {
+    return new Observable<FetchResult>(observer => {
+      this.setups.forEach(x => x());
+      this.observer = observer;
+      return () => {
+        this.unsubscribers.forEach(x => x());
+      };
+    });
+  }
+
+  public simulateResult(result: MockedSubscriptionResult, complete = false) {
+    setTimeout(() => {
+      const { observer } = this;
+      if (!observer) throw new Error('subscription torn down');
+      if (complete && observer.complete) observer.complete();
+      if (result.result && observer.next) observer.next(result.result);
+      if (result.error && observer.error) observer.error(result.error);
+    }, result.delay || 0);
+  }
+
+  public simulateComplete() {
+    const { observer } = this;
+    if (!observer) throw new Error('subscription torn down');
+    if (observer.complete) observer.complete();
+  }
+
+  public onSetup(listener: any): void {
+    this.setups = this.setups.concat([listener]);
+  }
+
+  public onUnsubscribe(listener: any): void {
+    this.unsubscribers = this.unsubscribers.concat([listener]);
+  }
+}
+
+export function mockObservableLink(): MockSubscriptionLink {
+  return new MockSubscriptionLink();
+}
diff --git a/packages/apollo-client/src/__mocks__/mockWatchQuery.ts b/src/utilities/testing/mocking/mockWatchQuery.ts
similarity index 52%
rename from packages/apollo-client/src/__mocks__/mockWatchQuery.ts
rename to src/utilities/testing/mocking/mockWatchQuery.ts
--- a/packages/apollo-client/src/__mocks__/mockWatchQuery.ts
+++ b/src/utilities/testing/mocking/mockWatchQuery.ts
@@ -1,11 +1,12 @@
-import { MockedResponse } from './mockLinks';
-
+import { MockedResponse } from './mockLink';
 import mockQueryManager from './mockQueryManager';
+import { ObservableQuery } from '../../../core/ObservableQuery';
 
-import { ObservableQuery } from '../core/ObservableQuery';
-
-export default (...mockedResponses: MockedResponse[]): ObservableQuery<any> => {
-  const queryManager = mockQueryManager(...mockedResponses);
+export default (
+  reject: (reason: any) => any,
+  ...mockedResponses: MockedResponse[]
+): ObservableQuery<any> => {
+  const queryManager = mockQueryManager(reject, ...mockedResponses);
   const firstRequest = mockedResponses[0].request;
   return queryManager.watchQuery({
     query: firstRequest.query!,
diff --git a/packages/apollo-client/src/util/observableToPromise.ts b/src/utilities/testing/observableToPromise.ts
similarity index 93%
rename from packages/apollo-client/src/util/observableToPromise.ts
rename to src/utilities/testing/observableToPromise.ts
--- a/packages/apollo-client/src/util/observableToPromise.ts
+++ b/src/utilities/testing/observableToPromise.ts
@@ -1,6 +1,6 @@
-import { ObservableQuery } from '../core/ObservableQuery';
-import { ApolloQueryResult } from '../core/types';
-import { Subscription } from '../util/Observable';
+import { ObservableQuery } from '../../core/ObservableQuery';
+import { ApolloQueryResult } from '../../core/types';
+import { Subscription } from '../../utilities/observables/Observable';
 
 /**
  *
diff --git a/packages/apollo-utilities/src/util/stripSymbols.ts b/src/utilities/testing/stripSymbols.ts
similarity index 100%
rename from packages/apollo-utilities/src/util/stripSymbols.ts
rename to src/utilities/testing/stripSymbols.ts
diff --git a/packages/apollo-client/src/util/subscribeAndCount.ts b/src/utilities/testing/subscribeAndCount.ts
similarity index 67%
rename from packages/apollo-client/src/util/subscribeAndCount.ts
rename to src/utilities/testing/subscribeAndCount.ts
--- a/packages/apollo-client/src/util/subscribeAndCount.ts
+++ b/src/utilities/testing/subscribeAndCount.ts
@@ -1,10 +1,10 @@
-import { ObservableQuery } from '../core/ObservableQuery';
-import { ApolloQueryResult } from '../core/types';
-import { Subscription } from '../util/Observable';
-import { asyncMap } from './observables';
+import { ObservableQuery } from '../../core/ObservableQuery';
+import { ApolloQueryResult } from '../../core/types';
+import { Subscription } from '../../utilities/observables/Observable';
+import { asyncMap } from '../../utilities/observables/observables';
 
 export default function subscribeAndCount(
-  done: jest.DoneCallback,
+  reject: (reason: any) => any,
   observable: ObservableQuery<any>,
   cb: (handleCount: number, result: ApolloQueryResult<any>) => any,
 ): Subscription {
@@ -20,12 +20,12 @@ export default function subscribeAndCount(
         // to be defined.
         setImmediate(() => {
           subscription.unsubscribe();
-          done.fail(e);
+          reject(e);
         });
       }
     },
   ).subscribe({
-    error: done.fail,
+    error: reject,
   });
   return subscription;
 }
diff --git a/packages/apollo-client/src/util/wrap.ts b/src/utilities/testing/wrap.ts
similarity index 83%
rename from packages/apollo-client/src/util/wrap.ts
rename to src/utilities/testing/wrap.ts
--- a/packages/apollo-client/src/util/wrap.ts
+++ b/src/utilities/testing/wrap.ts
@@ -1,12 +1,13 @@
 // I'm not sure why mocha doesn't provide something like this, you can't
 // always use promises
-export default (done: jest.DoneCallback, cb: (...args: any[]) => any) => (
-  ...args: any[]
-) => {
+export default <TArgs extends any[], TResult>(
+  reject: (reason: any) => any,
+  cb: (...args: TArgs) => TResult,
+) => (...args: TArgs) => {
   try {
     return cb(...args);
   } catch (e) {
-    done.fail(e);
+    reject(e);
   }
 };