Repository: FormidableLabs/urql
Pull request #593 (2020-03-09 17:19:59)

(graphcache) - Apply commutativity to all operations
## Summary

This will now apply mutations (including optimistic updates for mutations) commutatively. Before the ordering was a little confusing in that overlapping queries and mutations would have data from the queries always take precedence, which isn't always expected.

## Changes

- Remove direct `clearLayer` calls and instead have `initDataState` clean up optimistic updates.
- Let `commutativeKeys` in `data.ts` keep track of commutative query layers, while optimistic layers are only added to `optimisticOrder`
- Add consistent ordering of `optimisticOrder` instead of prioritising optimistic updates
- Prevent squashing layers when a layer has only optimistic results

changeset-bot[bot]: ###  💥  No Changeset

Latest commit: d4dfea344217598c52f031c147255f62542953f7

Merging this PR will not cause any packages to be released. If these changes should not cause updates to packages in this repo, this is fine 🙂

**If these changes should be published to npm, you need to add a changeset.**

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/FormidableLabs/urql/new/fix/apply-commutativity-to-everything?filename=.changeset/silly-toys-shop.md&value=---%0A%22%40fake-scope%2Ffake-pkg%22%3A%20patch%0A---%0A%0AUpdate%20the%20package%20info%20above%20to%20the%20changes%20you%20want%2C%20and%20write%20your%20description%20here.%20You%20can%20add%20more%20packages%20on%20new%20lines.%0A)



Patch:
diff --git a/.changeset/funny-parents-beg.md b/.changeset/funny-parents-beg.md
new file mode 100644
index 00000000..f50e8b92
--- /dev/null
+++ b/.changeset/funny-parents-beg.md
@@ -0,0 +1,5 @@
+---
+'@urql/exchange-graphcache': patch
+---
+
+Apply commutative layers to all operations, so now including mutations and subscriptions, to ensure that unordered data is written in the correct order.
diff --git a/.changeset/green-owls-pump.md b/.changeset/green-owls-pump.md
new file mode 100644
index 00000000..649b5843
--- /dev/null
+++ b/.changeset/green-owls-pump.md
@@ -0,0 +1,6 @@
+---
+'@urql/core': patch
+'next-urql': patch
+---
+
+Add missing GraphQLError serialization for extensions and path field to ssrExchange
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9a16706d..136074bf 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -160,6 +160,30 @@ yarn
 yarn run check
 ```
 
+## When and how do I add a changeset?
+
+Our changelogs and releases are maintained using `changeset`. For each PR that actively changes the
+behaviour of one or more packages in our monorepo it allows you to log the change and classify it
+per package as either `patch`, `minor`, or `major`.
+
+You won't need to add a changeset if you're simply making "non-visible" changes to the docs or other
+files that aren't published to `npm`.
+
+If you are however making visible changes you can track a change by calling the `changeset` CLI.
+
+```sh
+# In the root of the urql repository call:
+yarn changeset
+```
+
+This will open an interactive CLI that asks you to track your changes and enter a change message.
+You should always add to the message, what you've changed (not which package if it's only for a
+single package though) and what the impact is. If it's a breaking change it should also include some
+instructions on how users should update their code.
+
+This will create a new "changeset file" in the `.changeset` folder, which you should commit and
+push, so that it's added to your PR.
+
 ## How do I release new versions of our packages?
 
 The process of releasing versions is automated using `changeset`. This moves a lot of
@@ -169,12 +193,38 @@ applied and our `CHANGELOG`s are updated.
 
 First check what changes you're about to release with `yarn changeset status`.
 
-Then the process is similar to using `yarn version` and `yarn publish`:
+Then the process is similar to using `yarn version` and `yarn publish`.
+
+First we'll run `yarn changeset version` to bump all packages' versions and update CHANGELOG files:
 
 ```sh
 # This will automatically bump versions as necessary and update CHANGELOG files:
 yarn changeset version
-# Please check all versions and CHANGELOGs manually.
+```
+
+> **Note:** This command requires you to create a [GitHub Personal Access Token](https://github.com/settings/tokens/new)
+> and have it set on the `GITHUB_TOKEN` environment variable, either globally, e.g. in your
+> `~/.profile` file, or locally in a `.env` file.
+
+Then verify that the updated `package.json` and `CHANGELOG.md` files look correct and commit the
+changes:
+
+```sh
+# Commit all updated files
+git commit -a -m 'Version Packages'
+```
+
+At this point we have a new commit with packages that have been updated and should be published.
+Please don't push this commit before publishing so that we keep `master` in a pre-release state, in
+case the publish actually fails!
+
+```
 # Then publish all new packages / versions:
 yarn changeset publish
+# And push the "Version Packages" commit and all tags afterwards:
+git push && git push --tags
 ```
+
+Publishing the packages will also create the tags, it won't however push them automatically. After
+you've pushed the new tags, please make sure to update releases with the new content in each
+`CHANGELOG.md`, like so: https://github.com/FormidableLabs/urql/releases/tag/%40urql%2Fexchange-graphcache%402.2.2
diff --git a/docs/README.md b/docs/README.md
index b6cda233..0fb5e133 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -7,20 +7,21 @@ order: 0
 
 `urql` is an implementation of a GraphQL client, built to be both easy to use for newcomers to
 GraphQL, as well as extensible, to grow to support dynamic single-app applications and highly
-customised GraphQL infrastructure. In short, `urql` prioritizes usability and adaptability.
+customized GraphQL infrastructure. In short, `urql` prioritizes usability and adaptability.
 
 As you're adopting GraphQL, `urql` becomes your primary data layer and can handle content-heavy
 pages through ["Document Caching"](./basics/document-caching.md) as well as dynamic and data-heavy
-pages through ["Normalized Caching"](./graphcache/normalized-caching.md).
+apps through ["Normalized Caching"](./graphcache/normalized-caching.md).
 
 ## Constituent Parts
 
-`urql` can be understood as a collection of connected parts and packages. When you [get
+`urql` can be understood as a collection of connected parts and packages. When you [first get
 started](./basics/getting-started.md) you'll only need to install a single package for your
 framework of choice. We're then able to declaratively send GraphQL requests to our API.
 
-All framework packages — like `urql` and `@urql/preact` — wrap the core package, which we can
-imagine as the brain of `urql` with most of its logic.
+All framework packages — like `urql`, `@urql/preact`, and `@urql/svelte` — wrap the [core
+package](./concepts/core-package.md), which we can imagine as the brain of `urql` with most of its
+logic.
 
 As we progress with implementing `urql` into our application, we're later able to extend it by
 adding ["addon packages", which we call _Exchanges_](./concepts/exchanges.md)
@@ -34,14 +35,14 @@ interest.
   guide](./basics/getting-started.md) and usage patterns for our framework of choice.
 - **Main Concepts** then explains more about how `urql` functions, what it's made up of, and covers
   the main aspects of the `Client` and GraphQL clients in general, on the ["Philosophy"
-  page](./concepts/philosophy.md)
+  page](./concepts/philosophy.md).
 - **Advanced** covers all more uncommon use-cases and contains guides that we won't need immediately
   when we get started with `urql`.
 - **Graphcache** documents one of the most important addons to `urql`, which adds ["Normalized
   Caching" support](./graphcache/normalized-caching.md) to the `Client` and enables more complex
   use-cases, smarter caching, and more dynamic apps to function.
-- **Showcase** aims to list some companies that use `urql`, third-party packages, and other helpful
-  resources, like tutorials or guides.
+- **Showcase** aims to list users of `urql`, third-party packages, and other helpful resources,
+  like tutorials and guides.
 - **API** contains a detailed list of all helpers, utilities, components, and other parts of each of
   `urql`'s packages, which may contain all details of each part and package.
 
diff --git a/docs/advanced/README.md b/docs/advanced/README.md
index 35350514..32231d26 100644
--- a/docs/advanced/README.md
+++ b/docs/advanced/README.md
@@ -9,9 +9,9 @@ In this chapter we'll dive into various topics of "advanced" `urql` usage. This
 catch-all chapter of various use-cases that can only be covered after [the "Concepts"
 chapter.](../concepts/README.md)
 
-- **Subscriptions** covers how to use `useSubscription` and how to set up GraphQL subscriptions with
+- [**Subscriptions**](./subscriptions.md) covers how to use `useSubscription` and how to set up GraphQL subscriptions with
   `urql`.
-- **Server-side Rendering** guides us through how to set up server-side rendering and rehydration.
-- **Auto-populate Mutations** presents the `populateExchange` addon which can make it easier to
+- [**Server-side Rendering**](./server-side-rendering.md) guides us through how to set up server-side rendering and rehydration.
+- [**Auto-populate Mutations**](./auto-populate-mutations.md) presents the `populateExchange` addon which can make it easier to
   update normalized data after mutations.
-- **Testing** covers how to test components that use `urql` particularly in React.
+- [**Testing**](./testing.md) covers how to test components that use `urql` particularly in React.
diff --git a/docs/advanced/server-side-rendering.md b/docs/advanced/server-side-rendering.md
index d10b9e1a..f6c7db89 100644
--- a/docs/advanced/server-side-rendering.md
+++ b/docs/advanced/server-side-rendering.md
@@ -12,8 +12,8 @@ through the `ssrExchange.`
 ## The SSR Exchange
 
 The `ssrExchange` has two functions. On the server-side it's able to gather all results as they're
-being fetched, which can then be serialised and sent to the client. On the client-side it's able to
-use these serialised results to rehydrate and render the application without refetching this data.
+being fetched, which can then be serialized and sent to the client. On the client-side it's able to
+use these serialized results to rehydrate and render the application without refetching this data.
 
 To start out with the `ssrExchange` we have to add the exchange to our `Client`:
 
@@ -28,7 +28,7 @@ import {
 
 const isServerSide = typeof window === 'undefined';
 
-// The `ssrExchange` must be initialised with `isClient` and `initialState`
+// The `ssrExchange` must be initialized with `isClient` and `initialState`
 const ssr = ssrExchange({
   isClient: !isServerSide,
   initialState: !isServerSide ? window.__URQL_DATA__ : undefined,
@@ -44,11 +44,11 @@ const client createClient({
 });
 ```
 
-The `ssrExchange` must be initialised with the `isClient` and `initialState` options. The `isClient`
+The `ssrExchange` must be initialized with the `isClient` and `initialState` options. The `isClient`
 option tells the exchange whether it's on the server- or client-side. In our example we use `typeof window` to determine this, but in Webpack environments you may also be able to use `process.browser`.
 
-The `initialState` option should be set to the serialised data you retrieve on your server-side.
-This data may be retrieved using methods on `ssrExchange()`. You can retrive the serialised data
+The `initialState` option should be set to the serialized data you retrieve on your server-side.
+This data may be retrieved using methods on `ssrExchange()`. You can retrive the serialized data
 after server-side rendering using `ssr.extractData()`:
 
 ```js
diff --git a/docs/api/graphcache.md b/docs/api/graphcache.md
index 69431365..21f41ada 100644
--- a/docs/api/graphcache.md
+++ b/docs/api/graphcache.md
@@ -200,7 +200,7 @@ generated using `cache.keyOfField`.
 cache.resolveFieldByKey({ __typename: 'Query' }, cache.keyOfField('todo', { id: 1 })); // 'Todo:1'
 ```
 
-This specialised method is likely only going to be useful in combination with
+This specialized method is likely only going to be useful in combination with
 [`cache.inspectFields`](#inspectfields).
 
 ### inspectFields
diff --git a/docs/basics/README.md b/docs/basics/README.md
index 9f6067dd..cd99045a 100644
--- a/docs/basics/README.md
+++ b/docs/basics/README.md
@@ -8,12 +8,12 @@ order: 1
 In this chapter we'll explain the basics of `urql` and how to get started with using it without any
 prior knowledge. It's split into multiple pages that are best read in order.
 
-- **Getting started** covers how to install the package and set it up.
-- **Queries** explains how to write your first GraphQL queries with `urql`.
-- **Mutations** follows up with how to define mutations.
-- **Document Caching** explains the default cache mechanism of `urql`, as opposed to the [Normalized
-  Cache](../graphcache/normalized-caching.md) which is not the default.
-- **Errors** contains information on error handling in `urql`.
+- [**Getting started**](./getting-started.md) covers how to install the package for your framework of choice and set it up.
+- [**Queries**](./queries.md) explains how to write your first GraphQL queries with `urql`.
+- [**Mutations**](./mutations.md) follows up with how to define GraphQL mutations.
+- [**Document Caching**](./document-caching.md) explains the default cache mechanism of `urql`, as opposed to the opt-in
+  [Normalized Cache](../graphcache/normalized-caching.md).
+- [**Errors**](./errors.md) contains information on error handling in `urql`.
 
 After reading "Basics" you may want to [read the "Concepts" chapter of the
 documentation](../concepts/README.md) as it explains the motivation and architecture that drives
diff --git a/docs/basics/document-caching.md b/docs/basics/document-caching.md
index 3f08af4e..f7a8debc 100644
--- a/docs/basics/document-caching.md
+++ b/docs/basics/document-caching.md
@@ -11,28 +11,31 @@ to a GraphQL API repeatedly by caching the result of each query.
 This works like the cache in a browser. `urql` creates a key for each request that is sent based on
 a query and its variables.
 
+The default _document caching_ logic is implemented in the default `cacheExchange`. We'll learn more
+about ["Exchanges" on a later page.](../concepts/exchanges.md)
+
 ## Operation Keys
 
 ![Keys for GraphQL Requests](../assets/urql-operation-keys.png)
 
-Once a result comes in it's cached indefinitely by their key. This means that each unique request
+Once a result comes in it's cached indefinitely by its key. This means that each unique request
 can have exactly one cached result.
 
-However, we also need to invalidate the cache result so new requests for the same data can be
-sent when we know that some results are outdated. A result may be outdated because a mutation has
-been executed on data that has been queried previously.
+However, we also need to invalidate the cached results so that requests are sent again and updated,
+when we know that some results are out-of-date. With document caching we assume that a result may
+be invalidated by a mutation that executes on data that has been queried previously.
 
-In GraphQL the client can request additional type information on a query by adding the `__typename`
-field. This field return the name of the type of a piece of data in a requests, and we use it
-to detect commonalities and data dependencies between queries and mutations.
+In GraphQL the client can request additional type information by adding the `__typename` field to a
+query's _selection set_. This field returns the name of the type for an object in the results, and
+we use it to detect commonalities and data dependencies between queries and mutations.
 
 ![Document Caching](../assets/urql-document-caching.png)
 
-When we send a mutation that contains types that another query's results contains as well, that
-query's result is removed from the cache.
+In short, when we send a mutation that contains types that another query's results contains as well,
+that query's result is removed from the cache.
 
 This is an aggressive form of cache invalidation. However, it works well for content-driven sites,
-although it doesn't deal with normalized data or IDs.
+while it doesn't deal with normalized data or IDs.
 
 ## Request Policies
 
diff --git a/docs/basics/errors.md b/docs/basics/errors.md
index 47e05d8e..8c2495a2 100644
--- a/docs/basics/errors.md
+++ b/docs/basics/errors.md
@@ -6,7 +6,7 @@ order: 4
 # Error handling
 
 When we use a GraphQL API there are two kinds of errors we may encounter: Network Errors and GraphQL
-Errors from the API. Since it's common to encounter either of them in development, there's a
+Errors from the API. Since it's common to encounter either of them, there's a
 [`CombinedError`](../api/core.md#combinederror-class) class that can hold and abstract either.
 
 We may encounter a `CombinedError` when using `urql` wherever an `error` may be returned, typically
diff --git a/docs/basics/getting-started.md b/docs/basics/getting-started.md
index 1432039c..d3ee1bda 100644
--- a/docs/basics/getting-started.md
+++ b/docs/basics/getting-started.md
@@ -7,14 +7,14 @@ order: 0
 
 In the ["Introduction"](./README.md) we read how `urql` consists of multiple constituent parts that
 make up a GraphQL client. Hence there are multiple packages — one for each framework we officially
-support — that you'll get started with.
+support — that we'll get started with on this page.
 
 ## React & Preact
 
-This "Getting Started" guide covers how to install and set up `urql` and the Client, for React and
-Preact. Since the `urql` and `@urql/preact` packages share most of their API one-to-one, when
-reading the documentation on React, all examples are essentially the same, except that we'll use the
-`@urql/preact` package instead of the `urql` package for Preact.
+This "Getting Started" guide covers how to install and set up `urql` and the `Client`, for React and
+Preact. Since the `urql` and `@urql/preact` packages share most of their API and are used in the
+same way, when reading the documentation on React, all examples are essentially the same, except
+that we'd want to use the `@urql/preact` package instead of the `urql` package for Preact.
 
 ### Installation
 
@@ -27,17 +27,17 @@ yarn add urql graphql
 npm install --save urql graphql
 ```
 
-To use urql with Preact, we have to install `@urql/preact` instead of `urql` and import from
+To use `urql` with Preact, we have to install `@urql/preact` instead of `urql` and import from
 that package instead. Otherwise all examples for Preact will be the same.
 
 Most libraries related to GraphQL also need the `graphql` package to be installed as a peer
-dependency, so that they can adapt to your specific versioning requirements, which is why we'll need
-to install `graphql` as well.
+dependency, so that they can adapt to your specific versioning requirements. That's why we'll need
+to install `graphql` alongside `urql`.
 
-Both the `urql` packages and `graphql` follow [semantic versioning](https://semver.org) and `urql`
-packages will define a range of compatible versions of `graphql`. Watch out for breaking changes in
-the future however, in which case your package manager may warn you about `graphql` being out of the
-defined peer dependency range.
+Both the `urql` and `graphql` packages follow [semantic versioning](https://semver.org) and all
+`urql` packages will define a range of compatible versions of `graphql`. Watch out for breaking
+changes in the future however, in which case your package manager may warn you about `graphql` being
+out of the defined peer dependency range.
 
 ### Setting up the `Client`
 
@@ -55,8 +55,8 @@ const client = createClient({
 At the bare minimum we'll need to pass an API's `url` when we create a `Client` to get started.
 
 Another common option is `fetchOptions`. This option allows us to customize the options that will be
-passed to `fetch` when a request is sent to the given API `url`. We may pass in an object or a
-function returning an object to this option.
+passed to `fetch` when a request is sent to the given API `url`. We may pass in an options object or
+a function returning an options object.
 
 In the following example we'll add a token to each `fetch` request that our `Client` sends to our
 GraphQL API.
diff --git a/docs/basics/mutations.md b/docs/basics/mutations.md
index fd3cd20a..78c367cc 100644
--- a/docs/basics/mutations.md
+++ b/docs/basics/mutations.md
@@ -11,7 +11,7 @@ API [previously on the "Queries" page.](./queries.md)
 
 ## React & Preact
 
-This guide covers how to query data with React and Preact, which share almost the same API.
+This guide covers how to send mutations in React and Preact, which share almost the same API.
 
 Both libraries offer a `useMutation` hook and a `Mutation` component. The latter accepts the same
 parameters but we won't cover it in this guide. [Look it up in the API docs if you prefer
@@ -19,14 +19,15 @@ render-props components.](../api/urql.md#components)
 
 ### Sending a mutation
 
-Let's pick up an example in an imaginary todo items GraphQL API and dive into an example!
-We'll set up a mutation that _updates_ a todo item.
+Let's again pick up an example with an imaginary GraphQL API for todo items, and dive into an
+example! We'll set up a mutation that _updates_ a todo item's title.
 
 ```jsx
 const UpdateTodo = `
   mutation ($id: ID!, $title: String!) {
     updateTodo (id: $id, title: $title) {
       id
+      title
     }
   }
 `;
diff --git a/docs/basics/queries.md b/docs/basics/queries.md
index 4e3147b8..f9df531f 100644
--- a/docs/basics/queries.md
+++ b/docs/basics/queries.md
@@ -18,8 +18,8 @@ render-props components.](../api/urql.md#components)
 
 ### Run a first query
 
-For the following examples imagine we are querying data from a GraphQL API that contains todo items
-Let's dive right into an example!
+For the following examples, we'll imagine that we're querying data from a GraphQL API that contains
+todo items. Let's dive right into it!
 
 ```jsx
 import { useQuery } from 'urql';
@@ -53,19 +53,19 @@ const Todos = () => {
 };
 ```
 
-Here we have implemented our first GraphQL query to fetch todos. We see that using `useQuery`
-accepts options — in this case we've set `query` to our GraphQL query — and returns a tuple — an
-array that contains a result and a reexecute function.
+Here we have implemented our first GraphQL query to fetch todos. We see that `useQuery` accepts
+options and returns a tuple. In this case we've set the `query` option to our GraphQL query. The
+tuple we then get in return is an array that contains a result object and a re-execute function.
 
-The result contains several properties. The `fetching` field indicates whether we're currently loading
-data, `data` contains the actual `data` from the API's result, and `error` is set when either the
-request to the API has failed or when our result contained some `GraphQLError`s, which
+The result object contains several properties. The `fetching` field indicates whether we're currently
+loading data, `data` contains the actual `data` from the API's result, and `error` is set when either
+the request to the API has failed or when our API result contained some `GraphQLError`s, which
 we'll get into later on the ["Errors" page](./errors.md).
 
 ### Variables
 
-Typically we'll also need to pass variables to our queries, for instance What if we are dealing with
-pagination? For this purpose the `useQuery` hook also accepts a `variables` option, which we can use
+Typically we'll also need to pass variables to our queries, for instance, if we are dealing with
+pagination. For this purpose the `useQuery` hook also accepts a `variables` option, which we can use
 to supply variables to our query.
 
 ```jsx
@@ -89,24 +89,27 @@ const Todos = ({ from, limit }) => {
 ```
 
 As when we're sending GraphQL queries manually using `fetch`, the variables will be attached to the
-`POST` request that is sent to our GraphQL API.
+`POST` request body that is sent to our GraphQL API.
 
-Whenever the `variables` (or the `query`) option on
-the `useQuery` hook changes `fetching` will return to being `true` and a new request will be sent to
-our API, unless a result has already been cached previously.
+Whenever the `variables` (or the `query`) option on the `useQuery` hook changes `fetching` will
+switch to `true` and a new request will be sent to our API, unless a result has already been cached
+previously.
 
 ### Pausing `useQuery`
 
-In some cases we may want `useQuery` to execute a query when a precondition has been met. For
-instance, we may be building a form and want validation to only take place when a field has been
-filled out.
+In some cases we may want `useQuery` to execute a query when a pre-condition has been met, and not
+execute the query otherwise. For instance, we may be building a form and want a validation query to
+only take place when a field has been filled out.
+
+Since hooks in React can't just be commented out, the `useQuery` hook accepts a `pause` option that
+temporarily _freezes_ all changes and stops requests.
 
 In the previous example we've defined a query with mandatory arguments. The `$from` and `$limit`
 variables have been defined to be non-nullable `Int!` values.
 
 Let's pause the query we've just
 written to not execute when these variables are empty, to prevent `null` variables from being
-executed. We can do this by means of the `pause` option.
+executed. We can do this by means of setting the `pause` option to `true`:
 
 ```jsx
 const Todos = ({ from, limit }) => {
@@ -150,6 +153,9 @@ the background.
 For this reason there's another field on results, `result.stale`, which indicates that the cached
 result is either outdated or that another request is being sent in the background.
 
+Request policies aren't specific to `urql`'s React API, but are a common feature in its core. [You
+can learn more about request policies on the API docs.](../api/core.md#requestpolicy)
+
 ### Reexecuting Queries
 
 The `useQuery` hook updates and executes queries whenever its inputs, like the `query` or
diff --git a/docs/concepts/README.md b/docs/concepts/README.md
index a3fc8499..c4b973e7 100644
--- a/docs/concepts/README.md
+++ b/docs/concepts/README.md
@@ -10,13 +10,13 @@ inner workings of the `Client`, and how to write extensions and addons, also kno
 
 Each page goes a little further in explaning a core concept of `urql`.
 
-- **Philosophy** gives a quick overview of the different aspects of GraphQL clients and `urql` in
+- [**Philosophy**](./philosophy.md) gives a quick overview of the different aspects of GraphQL clients and `urql` in
   particular, which shines a light on why you may want to use `urql`.
-- **Stream Pattern** explains the inner working of `urql`, which is _stream-based_, also known as
+- [**Stream Pattern**](./stream-patterns.md) explains the inner working of `urql`, which is _stream-based_, also known as
   Observable patterns in JS.
-- **Core Package** defines why a shared package exists that contains the main logic of `urql`, and
+- [**Core Package**](./core-package.md) defines why a shared package exists that contains the main logic of `urql`, and
   how we can use it directly in Node.js.
-- **Exchanges** finally introduces _Exchanges_ and how to write extensions or addons and use them
+- [**Exchanges**](./exchanges.md) finally introduces _Exchanges_ and how to write extensions or addons and use them
   in `urql`.
 
 Finally, some _Exchanges_ are covered in different sections of the documentation, like
diff --git a/exchanges/graphcache/src/cacheExchange.ts b/exchanges/graphcache/src/cacheExchange.ts
index 279e6770..979fb06a 100644
--- a/exchanges/graphcache/src/cacheExchange.ts
+++ b/exchanges/graphcache/src/cacheExchange.ts
@@ -29,7 +29,7 @@ import {
 import { query, write, writeOptimistic } from './operations';
 import { hydrateData } from './store/data';
 import { makeDict } from './helpers/dict';
-import { Store, noopDataState, clearLayer, reserveLayer } from './store';
+import { Store, noopDataState, reserveLayer } from './store';
 
 import {
   UpdatesConfig,
@@ -78,6 +78,10 @@ const isQueryOperation = (op: Operation): boolean =>
 const isMutationOperation = (op: Operation): boolean =>
   op.operationName === 'mutation';
 
+// Returns whether an operation is a subscription
+const isSubscriptionOperation = (op: Operation): boolean =>
+  op.operationName === 'subscription';
+
 // Returns whether an operation can potentially be read from cache
 const isCacheableQuery = (op: Operation): boolean => {
   return isQueryOperation(op) && getRequestPolicy(op) !== 'network-only';
@@ -171,7 +175,7 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({
 
   // This registers queries with the data layer to ensure commutativity
   const prepareCacheForResult = (operation: Operation) => {
-    if (operation.operationName === 'query') {
+    if (isQueryOperation(operation)) {
       reserveLayer(store.data, operation.key);
     } else if (operation.operationName === 'teardown') {
       noopDataState(store.data, operation.key);
@@ -180,8 +184,8 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({
 
   // This executes an optimistic update for mutations and registers it if necessary
   const optimisticUpdate = (operation: Operation) => {
+    const { key } = operation;
     if (isOptimisticMutation(operation)) {
-      const { key } = operation;
       const { dependencies } = writeOptimistic(store, operation, key);
       if (dependencies.size !== 0) {
         optimisticKeysToDependencies.set(key, dependencies);
@@ -189,6 +193,8 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({
         collectPendingOperations(pendingOperations, dependencies);
         executePendingOperations(operation, pendingOperations);
       }
+    } else {
+      noopDataState(store.data, key, true);
     }
   };
 
@@ -244,13 +250,15 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({
     const { key } = operation;
     const pendingOperations = new Set<number>();
 
-    if (!isQuery) {
+    if (isMutationOperation(operation)) {
       collectPendingOperations(
         pendingOperations,
         optimisticKeysToDependencies.get(key)
       );
       optimisticKeysToDependencies.delete(key);
-      clearLayer(store.data, key);
+    } else if (isSubscriptionOperation(operation)) {
+      // If we're writing a subscription, we ad-hoc reserve a layer
+      reserveLayer(store.data, operation.key);
     }
 
     let writeDependencies: Set<string> | void;
diff --git a/exchanges/graphcache/src/store/data.ts b/exchanges/graphcache/src/store/data.ts
index 32104f8d..66644a8a 100644
--- a/exchanges/graphcache/src/store/data.ts
+++ b/exchanges/graphcache/src/store/data.ts
@@ -60,7 +60,7 @@ const makeNodeMap = <T>(): NodeMap<T> => ({
 export const initDataState = (
   data: InMemoryData,
   layerKey: number | null,
-  forceOptimistic?: boolean
+  isOptimistic?: boolean
 ) => {
   currentData = data;
   currentDependencies = new Set();
@@ -71,9 +71,17 @@ export const initDataState = (
   if (!layerKey) {
     currentOptimisticKey = null;
   } else if (
-    forceOptimistic ||
-    (data.commutativeKeys.size > 1 && data.commutativeKeys.has(layerKey))
+    isOptimistic ||
+    (data.optimisticOrder.length > 1 &&
+      data.optimisticOrder.indexOf(layerKey) > -1)
   ) {
+    // If this operation isn't optimistic and we see it for the first time,
+    // then it must've been optimistic in the past, so we can proactively
+    // clear the optimistic data before writing
+    if (!isOptimistic && !data.commutativeKeys.has(layerKey)) {
+      clearLayer(data, layerKey);
+      data.commutativeKeys.add(layerKey);
+    }
     // An optimistic update of a mutation may force an optimistic layer,
     // or this Query update may be applied optimistically since it's part
     // of a commutate chain
@@ -83,18 +91,18 @@ export const initDataState = (
     // Otherwise we don't create an optimistic layer and clear the
     // operation's one if it already exists
     currentOptimisticKey = null;
-    clearLayer(data, layerKey);
+    deleteLayer(data, layerKey);
   }
 };
 
 /** Reset the data state after read/write is complete */
 export const clearDataState = () => {
   const data = currentData!;
-  const optimisticKey = currentOptimisticKey;
+  const layerKey = currentOptimisticKey;
   currentOptimisticKey = null;
 
   // Determine whether the current operation has been a commutative layer
-  if (optimisticKey && data.commutativeKeys.has(optimisticKey)) {
+  if (layerKey && data.optimisticOrder.indexOf(layerKey) > -1) {
     // Find the lowest index of the commutative layers
     // The first part of `optimisticOrder` are the non-commutative layers
     const commutativeIndex =
@@ -103,12 +111,12 @@ export const clearDataState = () => {
     // Squash all layers in reverse order (low priority upwards) that have
     // been written already
     let i = data.optimisticOrder.length;
-    while (--i >= commutativeIndex) {
-      if (data.refLock[data.optimisticOrder[i]]) {
-        squashLayer(data.optimisticOrder[i]);
-      } else {
-        break;
-      }
+    while (
+      --i >= commutativeIndex &&
+      data.refLock[data.optimisticOrder[i]] &&
+      data.commutativeKeys.has(data.optimisticOrder[i])
+    ) {
+      squashLayer(data.optimisticOrder[i]);
     }
   }
 
@@ -136,8 +144,12 @@ export const clearDataState = () => {
 };
 
 /** Initialises then resets the data state, which may squash this layer if necessary */
-export const noopDataState = (data: InMemoryData, layerKey: number | null) => {
-  initDataState(data, layerKey);
+export const noopDataState = (
+  data: InMemoryData,
+  layerKey: number | null,
+  isOptimistic?: boolean
+) => {
+  initDataState(data, layerKey, isOptimistic);
   clearDataState();
 };
 
@@ -309,8 +321,8 @@ export const gc = (data: InMemoryData) => {
     const rc = data.refCount[entityKey] || 0;
     if (rc <= 0) {
       // Each optimistic layer may also still contain some references to marked entities
-      for (const optimisticKey in data.refLock) {
-        const refCount = data.refLock[optimisticKey];
+      for (const layerKey in data.refLock) {
+        const refCount = data.refLock[layerKey];
         const locks = refCount[entityKey] || 0;
         // If the optimistic layer has any references to the entity, don't GC it,
         // otherwise delete the reference count from the optimistic layer
@@ -455,11 +467,7 @@ export const reserveLayer = (data: InMemoryData, layerKey: number) => {
   if (!data.commutativeKeys.has(layerKey)) {
     // The new layer needs to be reserved in front of all other commutative
     // keys but after all non-commutative keys (which are added by `forceUpdate`)
-    data.optimisticOrder.splice(
-      data.optimisticOrder.length - data.commutativeKeys.size,
-      0,
-      layerKey
-    );
+    data.optimisticOrder.unshift(layerKey);
     data.commutativeKeys.add(layerKey);
   }
 };
@@ -477,19 +485,24 @@ const createLayer = (data: InMemoryData, layerKey: number) => {
   }
 };
 
-/** Removes an optimistic layer of links and records */
+/** Clears all links and records of an optimistic layer */
 export const clearLayer = (data: InMemoryData, layerKey: number) => {
+  if (data.refLock[layerKey]) {
+    delete data.refLock[layerKey];
+    delete data.records.optimistic[layerKey];
+    delete data.links.optimistic[layerKey];
+  }
+};
+
+/** Deletes links and records of an optimistic layer, and the layer itself */
+const deleteLayer = (data: InMemoryData, layerKey: number) => {
   const index = data.optimisticOrder.indexOf(layerKey);
   if (index > -1) {
     data.optimisticOrder.splice(index, 1);
     data.commutativeKeys.delete(layerKey);
   }
 
-  if (data.refLock[layerKey]) {
-    delete data.refLock[layerKey];
-    delete data.records.optimistic[layerKey];
-    delete data.links.optimistic[layerKey];
-  }
+  clearLayer(data, layerKey);
 };
 
 /** Merges an optimistic layer of links and records into the base data */
@@ -515,7 +528,7 @@ const squashLayer = (layerKey: number) => {
   }
 
   currentDependencies = prevDependencies;
-  clearLayer(currentData!, layerKey);
+  deleteLayer(currentData!, layerKey);
 };
 
 /** Return an array of FieldInfo (info on all the fields and their arguments) for a given entity */
diff --git a/packages/core/src/exchanges/ssr.ts b/packages/core/src/exchanges/ssr.ts
index ccb406a5..65e49ea6 100644
--- a/packages/core/src/exchanges/ssr.ts
+++ b/packages/core/src/exchanges/ssr.ts
@@ -1,3 +1,4 @@
+import { GraphQLError } from 'graphql';
 import { pipe, share, filter, merge, map, tap } from 'wonka';
 import { Exchange, OperationResult, Operation } from '../types';
 import { CombinedError } from '../utils';
@@ -5,8 +6,8 @@ import { CombinedError } from '../utils';
 export interface SerializedResult {
   data?: any;
   error?: {
+    graphQLErrors: Array<Partial<GraphQLError> | string>;
     networkError?: string;
-    graphQLErrors: string[];
   };
 }
 
@@ -35,9 +36,17 @@ const serializeResult = ({
   error,
 }: OperationResult): SerializedResult => {
   const result: SerializedResult = { data, error: undefined };
-  if (error !== undefined) {
+  if (error) {
     result.error = {
-      graphQLErrors: error.graphQLErrors.map(x => x.message),
+      graphQLErrors: error.graphQLErrors.map(error => {
+        if (!error.path && !error.extensions) return error.message;
+
+        return {
+          message: error.message,
+          path: error.path,
+          extensions: error.extensions,
+        };
+      }),
       networkError: error.networkError ? '' + error.networkError : undefined,
     };
   }
diff --git a/packages/core/src/utils/error.ts b/packages/core/src/utils/error.ts
index 53526a6e..f5e95c40 100644
--- a/packages/core/src/utils/error.ts
+++ b/packages/core/src/utils/error.ts
@@ -50,7 +50,7 @@ export class CombinedError extends Error {
     response,
   }: {
     networkError?: Error;
-    graphQLErrors?: Array<string | GraphQLError | Error>;
+    graphQLErrors?: Array<string | Partial<GraphQLError> | Error>;
     response?: any;
   }) {
     const normalizedGraphQLErrors = (graphQLErrors || []).map(
diff --git a/packages/next-urql/src/types.ts b/packages/next-urql/src/types.ts
index a8bdc90f..4dd966c4 100644
--- a/packages/next-urql/src/types.ts
+++ b/packages/next-urql/src/types.ts
@@ -1,3 +1,4 @@
+import { GraphQLError } from 'graphql';
 import { NextPageContext } from 'next';
 import { ClientOptions, Exchange, Client } from 'urql';
 import { AppContext } from 'next/app';
@@ -38,8 +39,8 @@ export interface WithUrqlProps extends WithUrqlClient, WithUrqlState {
 export interface SerializedResult {
   data?: any;
   error?: {
+    graphQLErrors: Array<Partial<GraphQLError> | string>;
     networkError?: string;
-    graphQLErrors: string[];
   };
 }
 
diff --git a/packages/site/src/assets/anchor.js b/packages/site/src/assets/anchor.js
new file mode 100644
index 00000000..8ba5b4f5
--- /dev/null
+++ b/packages/site/src/assets/anchor.js
@@ -0,0 +1,13 @@
+import React from 'react';
+
+const SvgAnchor = props => (
+  <svg viewBox="0 0 16 16" {...props}>
+    <path
+      fill="currentColor"
+      fillRule="evenodd"
+      d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"
+    />
+  </svg>
+);
+
+export default SvgAnchor;
diff --git a/packages/site/src/assets/anchor.svg b/packages/site/src/assets/anchor.svg
new file mode 100644
index 00000000..5f8fc157
--- /dev/null
+++ b/packages/site/src/assets/anchor.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg viewBox="0 0 16 16" version="1.1">
+  <path fill="currentColor" fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path>
+</svg>
+
diff --git a/packages/site/src/components/mdx.js b/packages/site/src/components/mdx.js
index 0ec7e1e7..91a729f8 100644
--- a/packages/site/src/components/mdx.js
+++ b/packages/site/src/components/mdx.js
@@ -7,6 +7,7 @@ import { useMarkdownPage } from 'react-static-plugin-md-pages';
 import Highlight, { Prism } from 'prism-react-renderer';
 import nightOwlLight from 'prism-react-renderer/themes/nightOwlLight';
 
+import AnchorSvg from '../assets/anchor';
 import { relative } from './sidebar';
 
 const getLanguage = className => {
@@ -223,7 +224,51 @@ const MdLink = ({ href, children }) => {
     return <Link to={relative(from, to)}>{children}</Link>;
   }
 
-  return <a href={href}>{children}</a>;
+  return (
+    <a rel="external" href={href}>
+      {children}
+    </a>
+  );
+};
+
+const HeadingText = styled.h1`
+  &:target:before {
+    content: '';
+    display: block;
+    height: 1.5em;
+    margin: -1.5em 0 0;
+  }
+`;
+
+const AnchorLink = styled.a`
+  display: inline-block;
+  color: ${p => p.theme.colors.accent};
+  padding-right: 0.5rem;
+  width: 2rem;
+
+  @media ${({ theme }) => theme.media.sm} {
+    margin-left: -2rem;
+    display: none;
+
+    ${HeadingText}:hover > & {
+      display: inline-block;
+    }
+  }
+`;
+
+const AnchorIcon = styled(AnchorSvg)`
+  height: 100%;
+`;
+
+const Header = tag => ({ id, children }) => {
+  return (
+    <HeadingText as={tag} id={id}>
+      <AnchorLink href={`#${id}`}>
+        <AnchorIcon />
+      </AnchorLink>
+      {children}
+    </HeadingText>
+  );
 };
 
 const components = {
@@ -236,6 +281,9 @@ const components = {
   th: TableHeader,
   td: TableCell,
   a: MdLink,
+  h1: HeadingText,
+  h2: Header('h2'),
+  h3: Header('h3'),
 };
 
 export const MDXComponents = ({ children }) => (
diff --git a/packages/site/src/components/navigation.js b/packages/site/src/components/navigation.js
index 66ce9743..4864bada 100644
--- a/packages/site/src/components/navigation.js
+++ b/packages/site/src/components/navigation.js
@@ -6,6 +6,10 @@ import ChevronIcon from '../assets/chevron';
 export const SidebarContainer = styled.div`
   display: ${p => (p.hidden ? 'none' : 'block')};
   position: absolute;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  min-height: 100%;
 
   @media ${({ theme }) => theme.media.sm} {
     display: block;
@@ -73,7 +77,9 @@ export const SidebarNavItem = styled(NavLink).attrs(() => ({
   }
 `;
 
-export const ChevronItem = styled(ChevronIcon)`
+export const ChevronItem = styled(ChevronIcon).attrs(() => ({
+  'aria-hidden': 'true',
+}))`
   display: inline-block;
   color: inherit;
   vertical-align: baseline;
diff --git a/packages/site/src/components/sidebar.js b/packages/site/src/components/sidebar.js
index 5605afec..56ad63eb 100644
--- a/packages/site/src/components/sidebar.js
+++ b/packages/site/src/components/sidebar.js
@@ -59,14 +59,14 @@ export const relative = (from, to) => {
   return { pathname };
 };
 
-export const SidebarStyling = ({ children, sidebarOpen }) => {
+export const SidebarStyling = ({ children, sidebarOpen, closeSidebar }) => {
   const basepath = useBasepath() || '';
   const homepage = basepath ? `/${basepath}/` : '/';
 
   return (
     <>
       <SideBarStripes />
-      <SidebarContainer hidden={!sidebarOpen}>
+      <SidebarContainer hidden={!sidebarOpen} onClick={closeSidebar}>
         <SidebarWrapper>
           <HeroLogoLink to={homepage}>
             <HeroLogo />
diff --git a/packages/site/src/screens/docs/article.js b/packages/site/src/screens/docs/article.js
index 1f7bfaa0..40fcd282 100644
--- a/packages/site/src/screens/docs/article.js
+++ b/packages/site/src/screens/docs/article.js
@@ -21,7 +21,7 @@ const Content = styled.article.attrs(() => ({
   id: 'page-content',
 }))`
   flex: 1;
-  min-height: 100vh;
+  min-height: calc(100vh - ${p => p.theme.layout.header});
   background: ${p => p.theme.colors.bg};
   padding: ${p => p.theme.spacing.md};
 
diff --git a/packages/site/src/screens/docs/header.js b/packages/site/src/screens/docs/header.js
index 3579cf1d..628f103c 100644
--- a/packages/site/src/screens/docs/header.js
+++ b/packages/site/src/screens/docs/header.js
@@ -13,6 +13,9 @@ const Fixed = styled.header`
   width: 100%;
   z-index: 1;
 
+  box-sizing: border-box;
+  height: ${p => p.theme.layout.header};
+
   background: ${p => p.theme.colors.bg};
   border-bottom: 1px solid ${p => p.theme.colors.border};
   padding: 0 ${p => p.theme.spacing.md};
@@ -21,9 +24,9 @@ const Fixed = styled.header`
 
 const Wrapper = styled.div`
   width: 100%;
+  height: 100%;
   max-width: ${p => p.theme.layout.page};
   margin: 0 auto;
-  height: ${p => p.theme.layout.header};
   padding-top: 2px;
   display: flex;
   flex-direction: row;
diff --git a/packages/site/src/screens/docs/index.js b/packages/site/src/screens/docs/index.js
index 37e98d48..fa0293b0 100644
--- a/packages/site/src/screens/docs/index.js
+++ b/packages/site/src/screens/docs/index.js
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useCallback } from 'react';
 import styled from 'styled-components';
 
 import Article, { ArticleStyling } from './article';
@@ -38,6 +38,10 @@ const OpenCloseSidebar = styled.img.attrs(props => ({
 const Docs = ({ isLoading, children }) => {
   const [sidebarOpen, setSidebarOpen] = useState(false);
 
+  const closeSidebar = useCallback(() => {
+    setSidebarOpen(false);
+  }, [setSidebarOpen]);
+
   return (
     <>
       <Header />
@@ -49,12 +53,15 @@ const Docs = ({ isLoading, children }) => {
         {/* load just the styles if Suspense fallback in use */}
         {isLoading ? (
           <>
-            <SidebarStyling sidebarOpen={sidebarOpen} />
+            <SidebarStyling
+              sidebarOpen={sidebarOpen}
+              closeSidebar={closeSidebar}
+            />
             <ArticleStyling>{children}</ArticleStyling>
           </>
         ) : (
           <>
-            <Sidebar sidebarOpen={sidebarOpen} />
+            <Sidebar sidebarOpen={sidebarOpen} closeSidebar={closeSidebar} />
             <Article>{children}</Article>
           </>
         )}
diff --git a/scripts/rollup/settings.js b/scripts/rollup/settings.js
index 0b8c91bc..ad0660d5 100644
--- a/scripts/rollup/settings.js
+++ b/scripts/rollup/settings.js
@@ -47,4 +47,4 @@ export const hasReact = externalModules.includes('react');
 export const hasPreact = externalModules.includes('preact');
 export const hasSvelte = externalModules.includes('svelte');
 export const mayReexport = hasReact || hasPreact || hasSvelte;
-export const isCI = !!process.env.CI;
+export const isCI = !!process.env.CIRCLECI;


Test patch:
diff --git a/exchanges/graphcache/src/cacheExchange.test.ts b/exchanges/graphcache/src/cacheExchange.test.ts
index 90471d57..b462e771 100644
--- a/exchanges/graphcache/src/cacheExchange.test.ts
+++ b/exchanges/graphcache/src/cacheExchange.test.ts
@@ -9,6 +9,7 @@ import {
   Source,
   pipe,
   map,
+  merge,
   mergeMap,
   filter,
   fromValue,
@@ -37,1039 +38,1430 @@ const queryOneData = {
   },
 };
 
-it('writes queries to the cache', () => {
-  const client = createClient({ url: 'http://0.0.0.0' });
-  const op = client.createRequestOperation('query', {
-    key: 1,
-    query: queryOne,
-  });
-
-  const response = jest.fn(
-    (forwardOp: Operation): OperationResult => {
-      expect(forwardOp.key).toBe(op.key);
-      return { operation: forwardOp, data: queryOneData };
-    }
-  );
-
-  const { source: ops$, next } = makeSubject<Operation>();
-  const result = jest.fn();
-  const forward: ExchangeIO = ops$ => pipe(ops$, map(response));
-
-  pipe(cacheExchange({})({ forward, client })(ops$), tap(result), publish);
-
-  next(op);
-  next(op);
-  expect(response).toHaveBeenCalledTimes(1);
-  expect(result).toHaveBeenCalledTimes(2);
-
-  expect(result.mock.calls[0][0]).toHaveProperty(
-    'operation.context.meta.cacheOutcome',
-    'miss'
-  );
-  expect(result.mock.calls[1][0]).toHaveProperty(
-    'operation.context.meta.cacheOutcome',
-    'hit'
-  );
-});
-
-it('updates related queries when their data changes', () => {
-  const queryMultiple = gql`
-    {
-      authors {
-        id
-        name
+describe('data dependencies', () => {
+  it('writes queries to the cache', () => {
+    const client = createClient({ url: 'http://0.0.0.0' });
+    const op = client.createRequestOperation('query', {
+      key: 1,
+      query: queryOne,
+    });
+
+    const response = jest.fn(
+      (forwardOp: Operation): OperationResult => {
+        expect(forwardOp.key).toBe(op.key);
+        return { operation: forwardOp, data: queryOneData };
       }
-    }
-  `;
-
-  const queryMultipleData = {
-    __typename: 'Query',
-    authors: [
-      {
-        __typename: 'Author',
-        id: '123',
-        name: 'Author',
-      },
-    ],
-  };
+    );
 
-  const client = createClient({ url: 'http://0.0.0.0' });
-  const { source: ops$, next } = makeSubject<Operation>();
+    const { source: ops$, next } = makeSubject<Operation>();
+    const result = jest.fn();
+    const forward: ExchangeIO = ops$ => pipe(ops$, map(response));
 
-  const reexec = jest
-    .spyOn(client, 'reexecuteOperation')
-    .mockImplementation(next);
+    pipe(cacheExchange({})({ forward, client })(ops$), tap(result), publish);
 
-  const opOne = client.createRequestOperation('query', {
-    key: 1,
-    query: queryOne,
-  });
+    next(op);
+    next(op);
+    expect(response).toHaveBeenCalledTimes(1);
+    expect(result).toHaveBeenCalledTimes(2);
 
-  const opMultiple = client.createRequestOperation('query', {
-    key: 2,
-    query: queryMultiple,
+    expect(result.mock.calls[0][0]).toHaveProperty(
+      'operation.context.meta.cacheOutcome',
+      'miss'
+    );
+    expect(result.mock.calls[1][0]).toHaveProperty(
+      'operation.context.meta.cacheOutcome',
+      'hit'
+    );
   });
 
-  const response = jest.fn(
-    (forwardOp: Operation): OperationResult => {
-      if (forwardOp.key === 1) {
-        return { operation: opOne, data: queryOneData };
-      } else if (forwardOp.key === 2) {
-        return { operation: opMultiple, data: queryMultipleData };
+  it('updates related queries when their data changes', () => {
+    const queryMultiple = gql`
+      {
+        authors {
+          id
+          name
+        }
       }
+    `;
+
+    const queryMultipleData = {
+      __typename: 'Query',
+      authors: [
+        {
+          __typename: 'Author',
+          id: '123',
+          name: 'Author',
+        },
+      ],
+    };
+
+    const client = createClient({ url: 'http://0.0.0.0' });
+    const { source: ops$, next } = makeSubject<Operation>();
+
+    const reexec = jest
+      .spyOn(client, 'reexecuteOperation')
+      .mockImplementation(next);
+
+    const opOne = client.createRequestOperation('query', {
+      key: 1,
+      query: queryOne,
+    });
+
+    const opMultiple = client.createRequestOperation('query', {
+      key: 2,
+      query: queryMultiple,
+    });
+
+    const response = jest.fn(
+      (forwardOp: Operation): OperationResult => {
+        if (forwardOp.key === 1) {
+          return { operation: opOne, data: queryOneData };
+        } else if (forwardOp.key === 2) {
+          return { operation: opMultiple, data: queryMultipleData };
+        }
 
-      return undefined as any;
-    }
-  );
-
-  const forward: ExchangeIO = ops$ => pipe(ops$, map(response));
-  const result = jest.fn();
+        return undefined as any;
+      }
+    );
 
-  pipe(cacheExchange({})({ forward, client })(ops$), tap(result), publish);
+    const forward: ExchangeIO = ops$ => pipe(ops$, map(response));
+    const result = jest.fn();
 
-  next(opOne);
-  expect(response).toHaveBeenCalledTimes(1);
-  expect(result).toHaveBeenCalledTimes(1);
+    pipe(cacheExchange({})({ forward, client })(ops$), tap(result), publish);
 
-  next(opMultiple);
-  expect(response).toHaveBeenCalledTimes(2);
-  expect(reexec).toHaveBeenCalledWith(opOne);
-  expect(result).toHaveBeenCalledTimes(3);
-});
+    next(opOne);
+    expect(response).toHaveBeenCalledTimes(1);
+    expect(result).toHaveBeenCalledTimes(1);
 
-it('updates related queries when a mutation update touches query data', () => {
-  jest.useFakeTimers();
+    next(opMultiple);
+    expect(response).toHaveBeenCalledTimes(2);
+    expect(reexec).toHaveBeenCalledWith(opOne);
+    expect(result).toHaveBeenCalledTimes(3);
+  });
 
-  const balanceFragment = gql`
-    fragment BalanceFragment on Author {
-      id
-      balance {
-        amount
-      }
-    }
-  `;
+  it('updates related queries when a mutation update touches query data', () => {
+    jest.useFakeTimers();
 
-  const queryById = gql`
-    query($id: ID!) {
-      author(id: $id) {
+    const balanceFragment = gql`
+      fragment BalanceFragment on Author {
         id
-        name
-        ...BalanceFragment
-      }
-    }
-
-    ${balanceFragment}
-  `;
-
-  const queryByIdDataA = {
-    __typename: 'Query',
-    author: {
-      __typename: 'Author',
-      id: '1',
-      name: 'Author 1',
-      balance: {
-        __typename: 'Balance',
-        amount: 100,
-      },
-    },
-  };
-
-  const queryByIdDataB = {
-    __typename: 'Query',
-    author: {
-      __typename: 'Author',
-      id: '2',
-      name: 'Author 2',
-      balance: {
-        __typename: 'Balance',
-        amount: 200,
-      },
-    },
-  };
-
-  const mutation = gql`
-    mutation($userId: ID!, $amount: Int!) {
-      updateBalance(userId: $userId, amount: $amount) {
-        userId
         balance {
           amount
         }
       }
-    }
-  `;
-
-  const mutationData = {
-    __typename: 'Mutation',
-    updateBalance: {
-      __typename: 'UpdateBalanceResult',
-      userId: '1',
-      balance: {
-        __typename: 'Balance',
-        amount: 1000,
-      },
-    },
-  };
-
-  const client = createClient({ url: 'http://0.0.0.0' });
-  const { source: ops$, next } = makeSubject<Operation>();
+    `;
 
-  const reexec = jest
-    .spyOn(client, 'reexecuteOperation')
-    .mockImplementation(next);
-
-  const opOne = client.createRequestOperation('query', {
-    key: 1,
-    query: queryById,
-    variables: { id: 1 },
-  });
+    const queryById = gql`
+      query($id: ID!) {
+        author(id: $id) {
+          id
+          name
+          ...BalanceFragment
+        }
+      }
 
-  const opTwo = client.createRequestOperation('query', {
-    key: 2,
-    query: queryById,
-    variables: { id: 2 },
-  });
+      ${balanceFragment}
+    `;
 
-  const opMutation = client.createRequestOperation('mutation', {
-    key: 3,
-    query: mutation,
-    variables: { userId: '1', amount: 1000 },
-  });
+    const queryByIdDataA = {
+      __typename: 'Query',
+      author: {
+        __typename: 'Author',
+        id: '1',
+        name: 'Author 1',
+        balance: {
+          __typename: 'Balance',
+          amount: 100,
+        },
+      },
+    };
 
-  const response = jest.fn(
-    (forwardOp: Operation): OperationResult => {
-      if (forwardOp.key === 1) {
-        return { operation: opOne, data: queryByIdDataA };
-      } else if (forwardOp.key === 2) {
-        return { operation: opTwo, data: queryByIdDataB };
-      } else if (forwardOp.key === 3) {
-        return { operation: opMutation, data: mutationData };
+    const queryByIdDataB = {
+      __typename: 'Query',
+      author: {
+        __typename: 'Author',
+        id: '2',
+        name: 'Author 2',
+        balance: {
+          __typename: 'Balance',
+          amount: 200,
+        },
+      },
+    };
+
+    const mutation = gql`
+      mutation($userId: ID!, $amount: Int!) {
+        updateBalance(userId: $userId, amount: $amount) {
+          userId
+          balance {
+            amount
+          }
+        }
       }
+    `;
+
+    const mutationData = {
+      __typename: 'Mutation',
+      updateBalance: {
+        __typename: 'UpdateBalanceResult',
+        userId: '1',
+        balance: {
+          __typename: 'Balance',
+          amount: 1000,
+        },
+      },
+    };
+
+    const client = createClient({ url: 'http://0.0.0.0' });
+    const { source: ops$, next } = makeSubject<Operation>();
+
+    const reexec = jest
+      .spyOn(client, 'reexecuteOperation')
+      .mockImplementation(next);
+
+    const opOne = client.createRequestOperation('query', {
+      key: 1,
+      query: queryById,
+      variables: { id: 1 },
+    });
+
+    const opTwo = client.createRequestOperation('query', {
+      key: 2,
+      query: queryById,
+      variables: { id: 2 },
+    });
+
+    const opMutation = client.createRequestOperation('mutation', {
+      key: 3,
+      query: mutation,
+      variables: { userId: '1', amount: 1000 },
+    });
+
+    const response = jest.fn(
+      (forwardOp: Operation): OperationResult => {
+        if (forwardOp.key === 1) {
+          return { operation: opOne, data: queryByIdDataA };
+        } else if (forwardOp.key === 2) {
+          return { operation: opTwo, data: queryByIdDataB };
+        } else if (forwardOp.key === 3) {
+          return { operation: opMutation, data: mutationData };
+        }
 
-      return undefined as any;
-    }
-  );
-
-  const result = jest.fn();
-  const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response));
-
-  const updates = {
-    Mutation: {
-      updateBalance: jest.fn((result, _args, cache) => {
-        const {
-          updateBalance: { userId, balance },
-        } = result;
-        cache.writeFragment(balanceFragment, { id: userId, balance });
-      }),
-    },
-  };
+        return undefined as any;
+      }
+    );
 
-  const keys = {
-    Balance: () => null,
-  };
+    const result = jest.fn();
+    const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response));
+
+    const updates = {
+      Mutation: {
+        updateBalance: jest.fn((result, _args, cache) => {
+          const {
+            updateBalance: { userId, balance },
+          } = result;
+          cache.writeFragment(balanceFragment, { id: userId, balance });
+        }),
+      },
+    };
 
-  pipe(
-    cacheExchange({ updates, keys })({ forward, client })(ops$),
-    tap(result),
-    publish
-  );
+    const keys = {
+      Balance: () => null,
+    };
 
-  next(opTwo);
-  jest.runAllTimers();
-  expect(response).toHaveBeenCalledTimes(1);
+    pipe(
+      cacheExchange({ updates, keys })({ forward, client })(ops$),
+      tap(result),
+      publish
+    );
 
-  next(opOne);
-  jest.runAllTimers();
-  expect(response).toHaveBeenCalledTimes(2);
+    next(opTwo);
+    jest.runAllTimers();
+    expect(response).toHaveBeenCalledTimes(1);
 
-  next(opMutation);
-  jest.runAllTimers();
+    next(opOne);
+    jest.runAllTimers();
+    expect(response).toHaveBeenCalledTimes(2);
 
-  expect(response).toHaveBeenCalledTimes(3);
-  expect(updates.Mutation.updateBalance).toHaveBeenCalledTimes(1);
+    next(opMutation);
+    jest.runAllTimers();
 
-  expect(reexec).toHaveBeenCalledTimes(1);
-  expect(reexec.mock.calls[0][0].key).toBe(1);
+    expect(response).toHaveBeenCalledTimes(3);
+    expect(updates.Mutation.updateBalance).toHaveBeenCalledTimes(1);
 
-  expect(result.mock.calls[2][0]).toHaveProperty(
-    'data.author.balance.amount',
-    1000
-  );
-});
+    expect(reexec).toHaveBeenCalledTimes(1);
+    expect(reexec.mock.calls[0][0].key).toBe(1);
 
-it('does nothing when no related queries have changed', () => {
-  const queryUnrelated = gql`
-    {
-      user {
-        id
-        name
-      }
-    }
-  `;
-
-  const queryUnrelatedData = {
-    __typename: 'Query',
-    user: {
-      __typename: 'User',
-      id: 'me',
-      name: 'Me',
-    },
-  };
-
-  const client = createClient({ url: 'http://0.0.0.0' });
-  const { source: ops$, next } = makeSubject<Operation>();
-  const reexec = jest
-    .spyOn(client, 'reexecuteOperation')
-    .mockImplementation(next);
-
-  const opOne = client.createRequestOperation('query', {
-    key: 1,
-    query: queryOne,
-  });
-  const opUnrelated = client.createRequestOperation('query', {
-    key: 2,
-    query: queryUnrelated,
+    expect(result.mock.calls[2][0]).toHaveProperty(
+      'data.author.balance.amount',
+      1000
+    );
   });
 
-  const response = jest.fn(
-    (forwardOp: Operation): OperationResult => {
-      if (forwardOp.key === 1) {
-        return { operation: opOne, data: queryOneData };
-      } else if (forwardOp.key === 2) {
-        return { operation: opUnrelated, data: queryUnrelatedData };
+  it('does nothing when no related queries have changed', () => {
+    const queryUnrelated = gql`
+      {
+        user {
+          id
+          name
+        }
       }
+    `;
+
+    const queryUnrelatedData = {
+      __typename: 'Query',
+      user: {
+        __typename: 'User',
+        id: 'me',
+        name: 'Me',
+      },
+    };
+
+    const client = createClient({ url: 'http://0.0.0.0' });
+    const { source: ops$, next } = makeSubject<Operation>();
+    const reexec = jest
+      .spyOn(client, 'reexecuteOperation')
+      .mockImplementation(next);
+
+    const opOne = client.createRequestOperation('query', {
+      key: 1,
+      query: queryOne,
+    });
+    const opUnrelated = client.createRequestOperation('query', {
+      key: 2,
+      query: queryUnrelated,
+    });
+
+    const response = jest.fn(
+      (forwardOp: Operation): OperationResult => {
+        if (forwardOp.key === 1) {
+          return { operation: opOne, data: queryOneData };
+        } else if (forwardOp.key === 2) {
+          return { operation: opUnrelated, data: queryUnrelatedData };
+        }
 
-      return undefined as any;
-    }
-  );
+        return undefined as any;
+      }
+    );
 
-  const forward: ExchangeIO = ops$ => pipe(ops$, map(response));
-  const result = jest.fn();
+    const forward: ExchangeIO = ops$ => pipe(ops$, map(response));
+    const result = jest.fn();
 
-  pipe(cacheExchange({})({ forward, client })(ops$), tap(result), publish);
+    pipe(cacheExchange({})({ forward, client })(ops$), tap(result), publish);
 
-  next(opOne);
-  expect(response).toHaveBeenCalledTimes(1);
+    next(opOne);
+    expect(response).toHaveBeenCalledTimes(1);
 
-  next(opUnrelated);
-  expect(response).toHaveBeenCalledTimes(2);
+    next(opUnrelated);
+    expect(response).toHaveBeenCalledTimes(2);
 
-  expect(reexec).not.toHaveBeenCalled();
-  expect(result).toHaveBeenCalledTimes(2);
+    expect(reexec).not.toHaveBeenCalled();
+    expect(result).toHaveBeenCalledTimes(2);
+  });
 });
 
-it('writes optimistic mutations to the cache', () => {
-  jest.useFakeTimers();
+describe('optimistic updates', () => {
+  it('writes optimistic mutations to the cache', () => {
+    jest.useFakeTimers();
 
-  const mutation = gql`
-    mutation {
-      concealAuthor {
-        id
-        name
+    const mutation = gql`
+      mutation {
+        concealAuthor {
+          id
+          name
+        }
       }
-    }
-  `;
-
-  const optimisticMutationData = {
-    __typename: 'Mutation',
-    concealAuthor: {
-      __typename: 'Author',
-      id: '123',
-      name: '[REDACTED OFFLINE]',
-    },
-  };
-
-  const mutationData = {
-    __typename: 'Mutation',
-    concealAuthor: {
-      __typename: 'Author',
-      id: '123',
-      name: '[REDACTED ONLINE]',
-    },
-  };
-
-  const client = createClient({ url: 'http://0.0.0.0' });
-  const { source: ops$, next } = makeSubject<Operation>();
-
-  const reexec = jest
-    .spyOn(client, 'reexecuteOperation')
-    .mockImplementation(next);
-
-  const opOne = client.createRequestOperation('query', {
-    key: 1,
-    query: queryOne,
-  });
+    `;
 
-  const opMutation = client.createRequestOperation('mutation', {
-    key: 2,
-    query: mutation,
-  });
+    const optimisticMutationData = {
+      __typename: 'Mutation',
+      concealAuthor: {
+        __typename: 'Author',
+        id: '123',
+        name: '[REDACTED OFFLINE]',
+      },
+    };
+
+    const mutationData = {
+      __typename: 'Mutation',
+      concealAuthor: {
+        __typename: 'Author',
+        id: '123',
+        name: '[REDACTED ONLINE]',
+      },
+    };
+
+    const client = createClient({ url: 'http://0.0.0.0' });
+    const { source: ops$, next } = makeSubject<Operation>();
+
+    const reexec = jest
+      .spyOn(client, 'reexecuteOperation')
+      .mockImplementation(next);
+
+    const opOne = client.createRequestOperation('query', {
+      key: 1,
+      query: queryOne,
+    });
+
+    const opMutation = client.createRequestOperation('mutation', {
+      key: 2,
+      query: mutation,
+    });
+
+    const response = jest.fn(
+      (forwardOp: Operation): OperationResult => {
+        if (forwardOp.key === 1) {
+          return { operation: opOne, data: queryOneData };
+        } else if (forwardOp.key === 2) {
+          return { operation: opMutation, data: mutationData };
+        }
 
-  const response = jest.fn(
-    (forwardOp: Operation): OperationResult => {
-      if (forwardOp.key === 1) {
-        return { operation: opOne, data: queryOneData };
-      } else if (forwardOp.key === 2) {
-        return { operation: opMutation, data: mutationData };
+        return undefined as any;
       }
+    );
 
-      return undefined as any;
-    }
-  );
+    const result = jest.fn();
+    const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response));
 
-  const result = jest.fn();
-  const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response));
+    const optimistic = {
+      concealAuthor: jest.fn(() => optimisticMutationData.concealAuthor) as any,
+    };
 
-  const optimistic = {
-    concealAuthor: jest.fn(() => optimisticMutationData.concealAuthor) as any,
-  };
+    pipe(
+      cacheExchange({ optimistic })({ forward, client })(ops$),
+      tap(result),
+      publish
+    );
 
-  pipe(
-    cacheExchange({ optimistic })({ forward, client })(ops$),
-    tap(result),
-    publish
-  );
+    next(opOne);
+    jest.runAllTimers();
+    expect(response).toHaveBeenCalledTimes(1);
 
-  next(opOne);
-  jest.runAllTimers();
-  expect(response).toHaveBeenCalledTimes(1);
+    next(opMutation);
+    expect(response).toHaveBeenCalledTimes(1);
+    expect(optimistic.concealAuthor).toHaveBeenCalledTimes(1);
+    expect(reexec).toHaveBeenCalledTimes(1);
 
-  next(opMutation);
-  expect(response).toHaveBeenCalledTimes(1);
-  expect(optimistic.concealAuthor).toHaveBeenCalledTimes(1);
-  expect(reexec).toHaveBeenCalledTimes(1);
+    jest.runAllTimers();
+    expect(response).toHaveBeenCalledTimes(2);
+    expect(result).toHaveBeenCalledTimes(4);
+  });
 
-  jest.runAllTimers();
-  expect(response).toHaveBeenCalledTimes(2);
-  expect(result).toHaveBeenCalledTimes(4);
-});
+  it('correctly clears on error', () => {
+    jest.useFakeTimers();
 
-it('correctly clears on error', () => {
-  jest.useFakeTimers();
+    const authorsQuery = gql`
+      query {
+        authors {
+          id
+          name
+        }
+      }
+    `;
 
-  const authorsQuery = gql`
-    query {
-      authors {
-        id
-        name
+    const authorsQueryData = {
+      __typename: 'Query',
+      authors: [
+        {
+          __typename: 'Author',
+          id: '1',
+          name: 'Author',
+        },
+      ],
+    };
+
+    const mutation = gql`
+      mutation {
+        addAuthor {
+          id
+          name
+        }
       }
-    }
-  `;
+    `;
 
-  const authorsQueryData = {
-    __typename: 'Query',
-    authors: [
-      {
+    const optimisticMutationData = {
+      __typename: 'Mutation',
+      addAuthor: {
         __typename: 'Author',
-        id: '1',
-        name: 'Author',
+        id: '123',
+        name: '[REDACTED OFFLINE]',
       },
-    ],
-  };
+    };
+
+    const client = createClient({ url: 'http://0.0.0.0' });
+    const { source: ops$, next } = makeSubject<Operation>();
+
+    const reexec = jest
+      .spyOn(client, 'reexecuteOperation')
+      .mockImplementation(next);
+
+    const opOne = client.createRequestOperation('query', {
+      key: 1,
+      query: authorsQuery,
+    });
+
+    const opMutation = client.createRequestOperation('mutation', {
+      key: 2,
+      query: mutation,
+    });
+
+    const response = jest.fn(
+      (forwardOp: Operation): OperationResult => {
+        if (forwardOp.key === 1) {
+          return { operation: opOne, data: authorsQueryData };
+        } else if (forwardOp.key === 2) {
+          return {
+            operation: opMutation,
+            error: 'error' as any,
+            data: { __typename: 'Mutation', addAuthor: null },
+          };
+        }
 
-  const mutation = gql`
-    mutation {
-      addAuthor {
-        id
-        name
+        return undefined as any;
       }
-    }
-  `;
-
-  const optimisticMutationData = {
-    __typename: 'Mutation',
-    addAuthor: {
-      __typename: 'Author',
-      id: '123',
-      name: '[REDACTED OFFLINE]',
-    },
-  };
-
-  const client = createClient({ url: 'http://0.0.0.0' });
-  const { source: ops$, next } = makeSubject<Operation>();
-
-  const reexec = jest
-    .spyOn(client, 'reexecuteOperation')
-    .mockImplementation(next);
-
-  const opOne = client.createRequestOperation('query', {
-    key: 1,
-    query: authorsQuery,
-  });
+    );
 
-  const opMutation = client.createRequestOperation('mutation', {
-    key: 2,
-    query: mutation,
-  });
+    const result = jest.fn();
+    const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response));
+
+    const optimistic = {
+      addAuthor: jest.fn(() => optimisticMutationData.addAuthor) as any,
+    };
+
+    const updates = {
+      Mutation: {
+        addAuthor: jest.fn((data, _, cache) => {
+          cache.updateQuery({ query: authorsQuery }, (prevData: any) => ({
+            ...prevData,
+            authors: [...prevData.authors, data.addAuthor],
+          }));
+        }),
+      },
+    };
 
-  const response = jest.fn(
-    (forwardOp: Operation): OperationResult => {
-      if (forwardOp.key === 1) {
-        return { operation: opOne, data: authorsQueryData };
-      } else if (forwardOp.key === 2) {
-        return {
-          operation: opMutation,
-          error: 'error' as any,
-          data: { __typename: 'Mutation', addAuthor: null },
-        };
-      }
+    pipe(
+      cacheExchange({ optimistic, updates })({ forward, client })(ops$),
+      tap(result),
+      publish
+    );
 
-      return undefined as any;
-    }
-  );
-
-  const result = jest.fn();
-  const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response));
-
-  const optimistic = {
-    addAuthor: jest.fn(() => optimisticMutationData.addAuthor) as any,
-  };
-
-  const updates = {
-    Mutation: {
-      addAuthor: jest.fn((data, _, cache) => {
-        cache.updateQuery({ query: authorsQuery }, (prevData: any) => ({
-          ...prevData,
-          authors: [...prevData.authors, data.addAuthor],
-        }));
-      }),
-    },
-  };
-
-  pipe(
-    cacheExchange({ optimistic, updates })({ forward, client })(ops$),
-    tap(result),
-    publish
-  );
-
-  next(opOne);
-  jest.runAllTimers();
-  expect(response).toHaveBeenCalledTimes(1);
-
-  next(opMutation);
-  expect(response).toHaveBeenCalledTimes(1);
-  expect(optimistic.addAuthor).toHaveBeenCalledTimes(1);
-  expect(updates.Mutation.addAuthor).toHaveBeenCalledTimes(1);
-  expect(reexec).toHaveBeenCalledTimes(1);
-
-  jest.runAllTimers();
-  expect(updates.Mutation.addAuthor).toHaveBeenCalledTimes(2);
-  expect(response).toHaveBeenCalledTimes(2);
-  expect(result).toHaveBeenCalledTimes(4);
-});
+    next(opOne);
+    jest.runAllTimers();
+    expect(response).toHaveBeenCalledTimes(1);
 
-it('follows resolvers on initial write', () => {
-  const client = createClient({ url: 'http://0.0.0.0' });
-  const { source: ops$, next } = makeSubject<Operation>();
+    next(opMutation);
+    expect(response).toHaveBeenCalledTimes(1);
+    expect(optimistic.addAuthor).toHaveBeenCalledTimes(1);
+    expect(updates.Mutation.addAuthor).toHaveBeenCalledTimes(1);
+    expect(reexec).toHaveBeenCalledTimes(1);
 
-  const opOne = client.createRequestOperation('query', {
-    key: 1,
-    query: queryOne,
+    jest.runAllTimers();
+    expect(updates.Mutation.addAuthor).toHaveBeenCalledTimes(2);
+    expect(response).toHaveBeenCalledTimes(2);
+    expect(result).toHaveBeenCalledTimes(4);
   });
+});
 
-  const response = jest.fn(
-    (forwardOp: Operation): OperationResult => {
-      if (forwardOp.key === 1) {
-        return { operation: opOne, data: queryOneData };
-      }
+describe('custom resolvers', () => {
+  it('follows resolvers on initial write', () => {
+    const client = createClient({ url: 'http://0.0.0.0' });
+    const { source: ops$, next } = makeSubject<Operation>();
 
-      return undefined as any;
-    }
-  );
+    const opOne = client.createRequestOperation('query', {
+      key: 1,
+      query: queryOne,
+    });
 
-  const forward: ExchangeIO = ops$ => pipe(ops$, map(response));
+    const response = jest.fn(
+      (forwardOp: Operation): OperationResult => {
+        if (forwardOp.key === 1) {
+          return { operation: opOne, data: queryOneData };
+        }
+
+        return undefined as any;
+      }
+    );
+
+    const forward: ExchangeIO = ops$ => pipe(ops$, map(response));
 
-  const result = jest.fn();
-  const fakeResolver = jest.fn();
+    const result = jest.fn();
+    const fakeResolver = jest.fn();
 
-  pipe(
-    cacheExchange({
-      resolvers: {
-        Author: {
-          name: () => {
-            fakeResolver();
-            return 'newName';
+    pipe(
+      cacheExchange({
+        resolvers: {
+          Author: {
+            name: () => {
+              fakeResolver();
+              return 'newName';
+            },
           },
         },
+      })({ forward, client })(ops$),
+      tap(result),
+      publish
+    );
+
+    next(opOne);
+    expect(response).toHaveBeenCalledTimes(1);
+    expect(fakeResolver).toHaveBeenCalledTimes(1);
+    expect(result).toHaveBeenCalledTimes(1);
+    expect(result.mock.calls[0][0].data).toEqual({
+      __typename: 'Query',
+      author: {
+        __typename: 'Author',
+        id: '123',
+        name: 'newName',
       },
-    })({ forward, client })(ops$),
-    tap(result),
-    publish
-  );
-
-  next(opOne);
-  expect(response).toHaveBeenCalledTimes(1);
-  expect(fakeResolver).toHaveBeenCalledTimes(1);
-  expect(result).toHaveBeenCalledTimes(1);
-  expect(result.mock.calls[0][0].data).toEqual({
-    __typename: 'Query',
-    author: {
-      __typename: 'Author',
-      id: '123',
-      name: 'newName',
-    },
+    });
   });
-});
 
-it('follows resolvers for mutations', () => {
-  jest.useFakeTimers();
+  it('follows resolvers for mutations', () => {
+    jest.useFakeTimers();
 
-  const mutation = gql`
-    mutation {
-      concealAuthor {
-        id
-        name
-        __typename
+    const mutation = gql`
+      mutation {
+        concealAuthor {
+          id
+          name
+          __typename
+        }
       }
-    }
-  `;
-
-  const mutationData = {
-    __typename: 'Mutation',
-    concealAuthor: {
-      __typename: 'Author',
-      id: '123',
-      name: '[REDACTED ONLINE]',
-    },
-  };
-
-  const client = createClient({ url: 'http://0.0.0.0' });
-  const { source: ops$, next } = makeSubject<Operation>();
-
-  const opOne = client.createRequestOperation('query', {
-    key: 1,
-    query: queryOne,
-  });
+    `;
 
-  const opMutation = client.createRequestOperation('mutation', {
-    key: 2,
-    query: mutation,
-  });
+    const mutationData = {
+      __typename: 'Mutation',
+      concealAuthor: {
+        __typename: 'Author',
+        id: '123',
+        name: '[REDACTED ONLINE]',
+      },
+    };
+
+    const client = createClient({ url: 'http://0.0.0.0' });
+    const { source: ops$, next } = makeSubject<Operation>();
+
+    const opOne = client.createRequestOperation('query', {
+      key: 1,
+      query: queryOne,
+    });
+
+    const opMutation = client.createRequestOperation('mutation', {
+      key: 2,
+      query: mutation,
+    });
+
+    const response = jest.fn(
+      (forwardOp: Operation): OperationResult => {
+        if (forwardOp.key === 1) {
+          return { operation: opOne, data: queryOneData };
+        } else if (forwardOp.key === 2) {
+          return { operation: opMutation, data: mutationData };
+        }
 
-  const response = jest.fn(
-    (forwardOp: Operation): OperationResult => {
-      if (forwardOp.key === 1) {
-        return { operation: opOne, data: queryOneData };
-      } else if (forwardOp.key === 2) {
-        return { operation: opMutation, data: mutationData };
+        return undefined as any;
       }
+    );
 
-      return undefined as any;
-    }
-  );
-
-  const result = jest.fn();
-  const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response));
+    const result = jest.fn();
+    const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response));
 
-  const fakeResolver = jest.fn();
+    const fakeResolver = jest.fn();
 
-  pipe(
-    cacheExchange({
-      resolvers: {
-        Author: {
-          name: () => {
-            fakeResolver();
-            return 'newName';
+    pipe(
+      cacheExchange({
+        resolvers: {
+          Author: {
+            name: () => {
+              fakeResolver();
+              return 'newName';
+            },
           },
         },
+      })({ forward, client })(ops$),
+      tap(result),
+      publish
+    );
+
+    next(opOne);
+    jest.runAllTimers();
+    expect(response).toHaveBeenCalledTimes(1);
+
+    next(opMutation);
+    expect(response).toHaveBeenCalledTimes(1);
+    expect(fakeResolver).toHaveBeenCalledTimes(1);
+
+    jest.runAllTimers();
+    expect(result.mock.calls[1][0].data).toEqual({
+      __typename: 'Mutation',
+      concealAuthor: {
+        __typename: 'Author',
+        id: '123',
+        name: 'newName',
       },
-    })({ forward, client })(ops$),
-    tap(result),
-    publish
-  );
-
-  next(opOne);
-  jest.runAllTimers();
-  expect(response).toHaveBeenCalledTimes(1);
-
-  next(opMutation);
-  expect(response).toHaveBeenCalledTimes(1);
-  expect(fakeResolver).toHaveBeenCalledTimes(1);
-
-  jest.runAllTimers();
-  expect(result.mock.calls[1][0].data).toEqual({
-    __typename: 'Mutation',
-    concealAuthor: {
-      __typename: 'Author',
-      id: '123',
-      name: 'newName',
-    },
+    });
   });
-});
 
-it('follows nested resolvers for mutations', () => {
-  jest.useFakeTimers();
+  it('follows nested resolvers for mutations', () => {
+    jest.useFakeTimers();
 
-  const mutation = gql`
-    mutation {
-      concealAuthors {
-        id
-        name
-        book {
+    const mutation = gql`
+      mutation {
+        concealAuthors {
           id
-          title
+          name
+          book {
+            id
+            title
+            __typename
+          }
           __typename
         }
-        __typename
       }
-    }
-  `;
+    `;
 
-  const client = createClient({ url: 'http://0.0.0.0' });
-  const { source: ops$, next } = makeSubject<Operation>();
+    const client = createClient({ url: 'http://0.0.0.0' });
+    const { source: ops$, next } = makeSubject<Operation>();
 
-  const query = gql`
-    query {
-      authors {
-        id
-        name
-        book {
+    const query = gql`
+      query {
+        authors {
           id
-          title
+          name
+          book {
+            id
+            title
+            __typename
+          }
           __typename
         }
-        __typename
       }
-    }
-  `;
+    `;
+
+    const queryOperation = client.createRequestOperation('query', {
+      key: 1,
+      query,
+    });
+
+    const mutationOperation = client.createRequestOperation('mutation', {
+      key: 2,
+      query: mutation,
+    });
+
+    const mutationData = {
+      __typename: 'Mutation',
+      concealAuthors: [
+        {
+          __typename: 'Author',
+          id: '123',
+          book: null,
+          name: '[REDACTED ONLINE]',
+        },
+        {
+          __typename: 'Author',
+          id: '456',
+          name: 'Formidable',
+          book: {
+            id: '1',
+            title: 'AwesomeGQL',
+            __typename: 'Book',
+          },
+        },
+      ],
+    };
+
+    const queryData = {
+      __typename: 'Query',
+      authors: [
+        {
+          __typename: 'Author',
+          id: '123',
+          name: '[REDACTED ONLINE]',
+          book: null,
+        },
+        {
+          __typename: 'Author',
+          id: '456',
+          name: 'Formidable',
+          book: {
+            id: '1',
+            title: 'AwesomeGQL',
+            __typename: 'Book',
+          },
+        },
+      ],
+    };
+
+    const response = jest.fn(
+      (forwardOp: Operation): OperationResult => {
+        if (forwardOp.key === 1) {
+          return { operation: queryOperation, data: queryData };
+        } else if (forwardOp.key === 2) {
+          return { operation: mutationOperation, data: mutationData };
+        }
 
-  const queryOperation = client.createRequestOperation('query', {
-    key: 1,
-    query,
-  });
+        return undefined as any;
+      }
+    );
 
-  const mutationOperation = client.createRequestOperation('mutation', {
-    key: 2,
-    query: mutation,
-  });
+    const result = jest.fn();
+    const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response));
 
-  const mutationData = {
-    __typename: 'Mutation',
-    concealAuthors: [
-      {
-        __typename: 'Author',
-        id: '123',
-        book: null,
-        name: '[REDACTED ONLINE]',
-      },
-      {
-        __typename: 'Author',
-        id: '456',
-        name: 'Formidable',
-        book: {
-          id: '1',
-          title: 'AwesomeGQL',
-          __typename: 'Book',
-        },
-      },
-    ],
-  };
+    const fakeResolver = jest.fn();
+    const called: any[] = [];
 
-  const queryData = {
-    __typename: 'Query',
-    authors: [
-      {
-        __typename: 'Author',
-        id: '123',
-        name: '[REDACTED ONLINE]',
-        book: null,
-      },
-      {
-        __typename: 'Author',
-        id: '456',
-        name: 'Formidable',
-        book: {
-          id: '1',
-          title: 'AwesomeGQL',
-          __typename: 'Book',
+    pipe(
+      cacheExchange({
+        resolvers: {
+          Author: {
+            name: parent => {
+              called.push(parent.name);
+              fakeResolver();
+              return 'Secret Author';
+            },
+          },
+          Book: {
+            title: parent => {
+              called.push(parent.title);
+              fakeResolver();
+              return 'Secret Book';
+            },
+          },
         },
-      },
-    ],
-  };
-
-  const response = jest.fn(
-    (forwardOp: Operation): OperationResult => {
-      if (forwardOp.key === 1) {
-        return { operation: queryOperation, data: queryData };
-      } else if (forwardOp.key === 2) {
-        return { operation: mutationOperation, data: mutationData };
-      }
+      })({ forward, client })(ops$),
+      tap(result),
+      publish
+    );
 
-      return undefined as any;
-    }
-  );
-
-  const result = jest.fn();
-  const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response));
-
-  const fakeResolver = jest.fn();
-  const called: any[] = [];
-
-  pipe(
-    cacheExchange({
-      resolvers: {
-        Author: {
-          name: parent => {
-            called.push(parent.name);
-            fakeResolver();
-            return 'Secret Author';
-          },
+    next(queryOperation);
+    jest.runAllTimers();
+    expect(response).toHaveBeenCalledTimes(1);
+    expect(fakeResolver).toHaveBeenCalledTimes(3);
+
+    next(mutationOperation);
+    jest.runAllTimers();
+    expect(response).toHaveBeenCalledTimes(2);
+    expect(fakeResolver).toHaveBeenCalledTimes(6);
+    expect(result.mock.calls[1][0].data).toEqual({
+      __typename: 'Mutation',
+      concealAuthors: [
+        {
+          __typename: 'Author',
+          id: '123',
+          book: null,
+          name: 'Secret Author',
         },
-        Book: {
-          title: parent => {
-            called.push(parent.title);
-            fakeResolver();
-            return 'Secret Book';
+        {
+          __typename: 'Author',
+          id: '456',
+          name: 'Secret Author',
+          book: {
+            id: '1',
+            title: 'Secret Book',
+            __typename: 'Book',
           },
         },
-      },
-    })({ forward, client })(ops$),
-    tap(result),
-    publish
-  );
-
-  next(queryOperation);
-  jest.runAllTimers();
-  expect(response).toHaveBeenCalledTimes(1);
-  expect(fakeResolver).toHaveBeenCalledTimes(3);
-
-  next(mutationOperation);
-  jest.runAllTimers();
-  expect(response).toHaveBeenCalledTimes(2);
-  expect(fakeResolver).toHaveBeenCalledTimes(6);
-  expect(result.mock.calls[1][0].data).toEqual({
-    __typename: 'Mutation',
-    concealAuthors: [
-      {
-        __typename: 'Author',
-        id: '123',
-        book: null,
-        name: 'Secret Author',
-      },
-      {
-        __typename: 'Author',
-        id: '456',
-        name: 'Secret Author',
-        book: {
-          id: '1',
-          title: 'Secret Book',
-          __typename: 'Book',
-        },
-      },
-    ],
+      ],
+    });
+
+    expect(called).toEqual([
+      // Query
+      '[REDACTED ONLINE]',
+      'Formidable',
+      'AwesomeGQL',
+      // Mutation
+      '[REDACTED ONLINE]',
+      'Formidable',
+      'AwesomeGQL',
+    ]);
   });
-
-  expect(called).toEqual([
-    // Query
-    '[REDACTED ONLINE]',
-    'Formidable',
-    'AwesomeGQL',
-    // Mutation
-    '[REDACTED ONLINE]',
-    'Formidable',
-    'AwesomeGQL',
-  ]);
 });
 
-it('reexecutes query and returns data on partial result', () => {
-  jest.useFakeTimers();
-  const client = createClient({ url: 'http://0.0.0.0' });
-  const { source: ops$, next } = makeSubject<Operation>();
-  const reexec = jest
-    .spyOn(client, 'reexecuteOperation')
-    // Empty mock to avoid going in an endless loop, since we would again return
-    // partial data.
-    .mockImplementation(() => undefined);
-
-  const initialQuery = gql`
-    query {
-      todos {
-        id
-        text
-        __typename
+describe('schema awareness', () => {
+  it('reexecutes query and returns data on partial result', () => {
+    jest.useFakeTimers();
+    const client = createClient({ url: 'http://0.0.0.0' });
+    const { source: ops$, next } = makeSubject<Operation>();
+    const reexec = jest
+      .spyOn(client, 'reexecuteOperation')
+      // Empty mock to avoid going in an endless loop, since we would again return
+      // partial data.
+      .mockImplementation(() => undefined);
+
+    const initialQuery = gql`
+      query {
+        todos {
+          id
+          text
+          __typename
+        }
       }
-    }
-  `;
+    `;
 
-  const query = gql`
-    query {
-      todos {
-        id
-        text
-        complete
-        author {
+    const query = gql`
+      query {
+        todos {
           id
-          name
+          text
+          complete
+          author {
+            id
+            name
+            __typename
+          }
           __typename
         }
-        __typename
       }
-    }
-  `;
+    `;
+
+    const initialQueryOperation = client.createRequestOperation('query', {
+      key: 1,
+      query: initialQuery,
+    });
+
+    const queryOperation = client.createRequestOperation('query', {
+      key: 2,
+      query,
+    });
+
+    const queryData = {
+      __typename: 'Query',
+      todos: [
+        {
+          __typename: 'Todo',
+          id: '123',
+          text: 'Learn',
+        },
+        {
+          __typename: 'Todo',
+          id: '456',
+          text: 'Teach',
+        },
+      ],
+    };
+
+    const response = jest.fn(
+      (forwardOp: Operation): OperationResult => {
+        if (forwardOp.key === 1) {
+          return { operation: initialQueryOperation, data: queryData };
+        } else if (forwardOp.key === 2) {
+          return { operation: queryOperation, data: queryData };
+        }
 
-  const initialQueryOperation = client.createRequestOperation('query', {
-    key: 1,
-    query: initialQuery,
-  });
+        return undefined as any;
+      }
+    );
+
+    const result = jest.fn();
+    const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response));
+
+    pipe(
+      cacheExchange({
+        // eslint-disable-next-line
+        schema: require('./test-utils/simple_schema.json'),
+      })({ forward, client })(ops$),
+      tap(result),
+      publish
+    );
+
+    next(initialQueryOperation);
+    jest.runAllTimers();
+    expect(response).toHaveBeenCalledTimes(1);
+    expect(reexec).toHaveBeenCalledTimes(0);
+    expect(result.mock.calls[0][0].data).toEqual({
+      __typename: 'Query',
+      todos: [
+        {
+          __typename: 'Todo',
+          id: '123',
+          text: 'Learn',
+        },
+        {
+          __typename: 'Todo',
+          id: '456',
+          text: 'Teach',
+        },
+      ],
+    });
+
+    expect(result.mock.calls[0][0]).toHaveProperty(
+      'operation.context.meta',
+      undefined
+    );
 
-  const queryOperation = client.createRequestOperation('query', {
-    key: 2,
-    query,
+    next(queryOperation);
+    jest.runAllTimers();
+    expect(result).toHaveBeenCalledTimes(2);
+    expect(reexec).toHaveBeenCalledTimes(1);
+    expect(result.mock.calls[1][0].stale).toBe(true);
+    expect(result.mock.calls[1][0].data).toEqual({
+      __typename: 'Query',
+      todos: [
+        {
+          __typename: 'Todo',
+          author: null,
+          complete: null,
+          id: '123',
+          text: 'Learn',
+        },
+        {
+          __typename: 'Todo',
+          author: null,
+          complete: null,
+          id: '456',
+          text: 'Teach',
+        },
+      ],
+    });
+
+    expect(result.mock.calls[1][0]).toHaveProperty(
+      'operation.context.meta.cacheOutcome',
+      'partial'
+    );
   });
+});
 
-  const queryData = {
-    __typename: 'Query',
-    todos: [
-      {
-        __typename: 'Todo',
-        id: '123',
-        text: 'Learn',
-      },
+describe('commutativity', () => {
+  it('applies results that come in out-of-order commutatively and consistently', () => {
+    jest.useFakeTimers();
+
+    let data: any;
+
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      requestPolicy: 'cache-and-network',
+    });
+    const { source: ops$, next: next } = makeSubject<Operation>();
+    const query = gql`
       {
-        __typename: 'Todo',
-        id: '456',
-        text: 'Teach',
-      },
-    ],
-  };
-
-  const response = jest.fn(
-    (forwardOp: Operation): OperationResult => {
-      if (forwardOp.key === 1) {
-        return { operation: initialQueryOperation, data: queryData };
-      } else if (forwardOp.key === 2) {
-        return { operation: queryOperation, data: queryData };
+        index
       }
+    `;
+
+    const result = (operation: Operation): Source<OperationResult> =>
+      pipe(
+        fromValue({
+          operation,
+          data: {
+            __typename: 'Query',
+            index: operation.key,
+          },
+        }),
+        delay(operation.key === 2 ? 5 : operation.key * 10)
+      );
 
-      return undefined as any;
-    }
-  );
-
-  const result = jest.fn();
-  const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response));
-
-  pipe(
-    cacheExchange({
-      // eslint-disable-next-line
-      schema: require('./test-utils/simple_schema.json'),
-    })({ forward, client })(ops$),
-    tap(result),
-    publish
-  );
-
-  next(initialQueryOperation);
-  jest.runAllTimers();
-  expect(response).toHaveBeenCalledTimes(1);
-  expect(reexec).toHaveBeenCalledTimes(0);
-  expect(result.mock.calls[0][0].data).toEqual({
-    __typename: 'Query',
-    todos: [
+    const output = jest.fn(result => {
+      data = result.data;
+    });
+
+    const forward = (ops$: Source<Operation>): Source<OperationResult> =>
+      pipe(
+        ops$,
+        filter(op => op.operationName !== 'teardown'),
+        mergeMap(result)
+      );
+
+    pipe(cacheExchange()({ forward, client })(ops$), tap(output), publish);
+
+    next(client.createRequestOperation('query', { key: 1, query }));
+    next(client.createRequestOperation('query', { key: 2, query }));
+
+    // This shouldn't have any effect:
+    next(client.createRequestOperation('teardown', { key: 2, query }));
+
+    next(client.createRequestOperation('query', { key: 3, query }));
+
+    jest.advanceTimersByTime(5);
+    expect(output).toHaveBeenCalledTimes(1);
+    expect(data.index).toBe(2);
+
+    jest.advanceTimersByTime(10);
+    expect(output).toHaveBeenCalledTimes(2);
+    expect(data.index).toBe(2);
+
+    jest.advanceTimersByTime(30);
+    expect(output).toHaveBeenCalledTimes(3);
+    expect(data.index).toBe(3);
+  });
+
+  it('applies optimistic updates on top of commutative queries', () => {
+    let data: any;
+    const client = createClient({ url: 'http://0.0.0.0' });
+    const { source: ops$, next: nextOp } = makeSubject<Operation>();
+    const { source: res$, next: nextRes } = makeSubject<OperationResult>();
+
+    const reexec = jest
+      .spyOn(client, 'reexecuteOperation')
+      .mockImplementation(nextOp);
+
+    const query = gql`
       {
-        __typename: 'Todo',
-        id: '123',
-        text: 'Learn',
+        node {
+          id
+          name
+        }
+      }
+    `;
+
+    const mutation = gql`
+      mutation {
+        node {
+          id
+          name
+        }
+      }
+    `;
+
+    const forward = (ops$: Source<Operation>): Source<OperationResult> =>
+      merge([
+        pipe(
+          ops$,
+          filter(() => false)
+        ) as any,
+        res$,
+      ]);
+
+    const optimistic = {
+      node: () => ({
+        __typename: 'Node',
+        id: 'node',
+        name: 'optimistic',
+      }),
+    };
+
+    pipe(
+      cacheExchange({ optimistic })({ forward, client })(ops$),
+      tap(result => {
+        if (result.operation.operationName === 'query') {
+          data = result.data;
+        }
+      }),
+      publish
+    );
+
+    const queryOpA = client.createRequestOperation('query', { key: 1, query });
+    const mutationOp = client.createRequestOperation('mutation', {
+      key: 2,
+      query: mutation,
+    });
+    const queryOpB = client.createRequestOperation('query', { key: 3, query });
+
+    expect(data).toBe(undefined);
+
+    nextOp(queryOpA);
+
+    nextRes({
+      operation: queryOpA,
+      data: {
+        __typename: 'Query',
+        node: {
+          __typename: 'Node',
+          id: 'node',
+          name: 'query a',
+        },
       },
-      {
-        __typename: 'Todo',
-        id: '456',
-        text: 'Teach',
+    });
+
+    expect(data).toHaveProperty('node.name', 'query a');
+
+    nextOp(mutationOp);
+    expect(reexec).toHaveBeenCalledTimes(1);
+    expect(data).toHaveProperty('node.name', 'optimistic');
+
+    nextOp(queryOpB);
+    nextRes({
+      operation: queryOpB,
+      data: {
+        __typename: 'Query',
+        node: {
+          __typename: 'Node',
+          id: 'node',
+          name: 'query b',
+        },
       },
-    ],
+    });
+
+    expect(data).toHaveProperty('node.name', 'query b');
   });
 
-  expect(result.mock.calls[0][0]).toHaveProperty(
-    'operation.context.meta',
-    undefined
-  );
-
-  next(queryOperation);
-  jest.runAllTimers();
-  expect(result).toHaveBeenCalledTimes(2);
-  expect(reexec).toHaveBeenCalledTimes(1);
-  expect(result.mock.calls[1][0].stale).toBe(true);
-  expect(result.mock.calls[1][0].data).toEqual({
-    __typename: 'Query',
-    todos: [
+  it('applies mutation results on top of commutative queries', () => {
+    let data: any;
+    const client = createClient({ url: 'http://0.0.0.0' });
+    const { source: ops$, next: nextOp } = makeSubject<Operation>();
+    const { source: res$, next: nextRes } = makeSubject<OperationResult>();
+
+    const reexec = jest
+      .spyOn(client, 'reexecuteOperation')
+      .mockImplementation(nextOp);
+
+    const query = gql`
       {
-        __typename: 'Todo',
-        author: null,
-        complete: null,
-        id: '123',
-        text: 'Learn',
+        node {
+          id
+          name
+        }
+      }
+    `;
+
+    const mutation = gql`
+      mutation {
+        node {
+          id
+          name
+        }
+      }
+    `;
+
+    const forward = (ops$: Source<Operation>): Source<OperationResult> =>
+      merge([
+        pipe(
+          ops$,
+          filter(() => false)
+        ) as any,
+        res$,
+      ]);
+
+    pipe(
+      cacheExchange()({ forward, client })(ops$),
+      tap(result => {
+        if (result.operation.operationName === 'query') {
+          data = result.data;
+        }
+      }),
+      publish
+    );
+
+    const queryOpA = client.createRequestOperation('query', { key: 1, query });
+    const mutationOp = client.createRequestOperation('mutation', {
+      key: 2,
+      query: mutation,
+    });
+    const queryOpB = client.createRequestOperation('query', { key: 3, query });
+
+    expect(data).toBe(undefined);
+
+    nextOp(queryOpA);
+    nextOp(mutationOp);
+    nextOp(queryOpB);
+
+    nextRes({
+      operation: queryOpA,
+      data: {
+        __typename: 'Query',
+        node: {
+          __typename: 'Node',
+          id: 'node',
+          name: 'query a',
+        },
       },
-      {
-        __typename: 'Todo',
-        author: null,
-        complete: null,
-        id: '456',
-        text: 'Teach',
+    });
+
+    expect(data).toHaveProperty('node.name', 'query a');
+
+    nextRes({
+      operation: mutationOp,
+      data: {
+        __typename: 'Mutation',
+        node: {
+          __typename: 'Node',
+          id: 'node',
+          name: 'mutation',
+        },
+      },
+    });
+
+    expect(reexec).toHaveBeenCalledTimes(1);
+    expect(data).toHaveProperty('node.name', 'mutation');
+
+    nextRes({
+      operation: queryOpB,
+      data: {
+        __typename: 'Query',
+        node: {
+          __typename: 'Node',
+          id: 'node',
+          name: 'query b',
+        },
       },
-    ],
+    });
+
+    expect(reexec).toHaveBeenCalledTimes(2);
+    expect(data).toHaveProperty('node.name', 'query b');
   });
 
-  expect(result.mock.calls[1][0]).toHaveProperty(
-    'operation.context.meta.cacheOutcome',
-    'partial'
-  );
-});
+  it('applies optimistic updates on top of commutative queries', () => {
+    let data: any;
+    const client = createClient({ url: 'http://0.0.0.0' });
+    const { source: ops$, next: nextOp } = makeSubject<Operation>();
+    const { source: res$, next: nextRes } = makeSubject<OperationResult>();
 
-it('applies results that come in out-of-order commutatively and consistently', () => {
-  jest.useFakeTimers();
+    jest.spyOn(client, 'reexecuteOperation').mockImplementation(nextOp);
 
-  let data: any;
+    const query = gql`
+      {
+        node {
+          id
+          name
+        }
+      }
+    `;
 
-  const client = createClient({
-    url: 'http://0.0.0.0',
-    requestPolicy: 'cache-and-network',
-  });
-  const { source: ops$, next: next } = makeSubject<Operation>();
-  const query = gql`
-    {
-      index
-    }
-  `;
+    const mutation = gql`
+      mutation {
+        node {
+          id
+          name
+          optimistic
+        }
+      }
+    `;
+
+    const forward = (ops$: Source<Operation>): Source<OperationResult> =>
+      merge([
+        pipe(
+          ops$,
+          filter(() => false)
+        ) as any,
+        res$,
+      ]);
+
+    const optimistic = {
+      node: () => ({
+        __typename: 'Node',
+        id: 'node',
+        name: 'optimistic',
+      }),
+    };
 
-  const result = (operation: Operation): Source<OperationResult> =>
     pipe(
-      fromValue({
-        operation,
-        data: {
-          __typename: 'Query',
-          index: operation.key,
-        },
+      cacheExchange({ optimistic })({ forward, client })(ops$),
+      tap(result => {
+        if (result.operation.operationName === 'query') {
+          data = result.data;
+        }
       }),
-      delay(operation.key === 2 ? 5 : operation.key * 10)
+      publish
     );
 
-  const output = jest.fn(result => {
-    data = result.data;
+    const queryOp = client.createRequestOperation('query', { key: 1, query });
+    const mutationOp = client.createRequestOperation('mutation', {
+      key: 2,
+      query: mutation,
+    });
+
+    expect(data).toBe(undefined);
+
+    nextOp(queryOp);
+    nextOp(mutationOp);
+
+    nextRes({
+      operation: queryOp,
+      data: {
+        __typename: 'Query',
+        node: {
+          __typename: 'Node',
+          id: 'node',
+          name: 'query a',
+        },
+      },
+    });
+
+    expect(data).toHaveProperty('node.name', 'optimistic');
+
+    nextRes({
+      operation: mutationOp,
+      data: {
+        __typename: 'Query',
+        node: {
+          __typename: 'Node',
+          id: 'node',
+          name: 'mutation',
+        },
+      },
+    });
+
+    expect(data).toHaveProperty('node.name', 'mutation');
   });
 
-  const forward = (ops$: Source<Operation>): Source<OperationResult> =>
-    pipe(
-      ops$,
-      filter(op => op.operationName !== 'teardown'),
-      mergeMap(result)
-    );
+  it('allows subscription results to be commutative when necessary', () => {
+    let data: any;
+    const client = createClient({ url: 'http://0.0.0.0' });
+    const { source: ops$, next: nextOp } = makeSubject<Operation>();
+    const { source: res$, next: nextRes } = makeSubject<OperationResult>();
 
-  pipe(cacheExchange()({ forward, client })(ops$), tap(output), publish);
+    jest.spyOn(client, 'reexecuteOperation').mockImplementation(nextOp);
 
-  next(client.createRequestOperation('query', { key: 1, query }));
-  next(client.createRequestOperation('query', { key: 2, query }));
+    const query = gql`
+      {
+        node {
+          id
+          name
+        }
+      }
+    `;
 
-  // This shouldn't have any effect:
-  next(client.createRequestOperation('teardown', { key: 2, query }));
+    const subscription = gql`
+      subscription {
+        node {
+          id
+          name
+        }
+      }
+    `;
 
-  next(client.createRequestOperation('query', { key: 3, query }));
+    const forward = (ops$: Source<Operation>): Source<OperationResult> =>
+      merge([
+        pipe(
+          ops$,
+          filter(() => false)
+        ) as any,
+        res$,
+      ]);
 
-  jest.advanceTimersByTime(5);
-  expect(output).toHaveBeenCalledTimes(1);
-  expect(data.index).toBe(2);
+    pipe(
+      cacheExchange()({ forward, client })(ops$),
+      tap(result => {
+        if (result.operation.operationName === 'query') {
+          data = result.data;
+        }
+      }),
+      publish
+    );
 
-  jest.advanceTimersByTime(10);
-  expect(output).toHaveBeenCalledTimes(2);
-  expect(data.index).toBe(2);
+    const queryOpA = client.createRequestOperation('query', { key: 1, query });
+    const subscriptionOp = client.createRequestOperation('subscription', {
+      key: 3,
+      query: subscription,
+    });
+
+    nextOp(queryOpA);
+    // Force commutative layers to be created:
+    nextOp(client.createRequestOperation('query', { key: 2, query }));
+    nextOp(subscriptionOp);
+
+    nextRes({
+      operation: queryOpA,
+      data: {
+        __typename: 'Query',
+        node: {
+          __typename: 'Node',
+          id: 'node',
+          name: 'query a',
+        },
+      },
+    });
+
+    nextRes({
+      operation: subscriptionOp,
+      data: {
+        node: {
+          __typename: 'Node',
+          id: 'node',
+          name: 'subscription',
+        },
+      },
+    });
 
-  jest.advanceTimersByTime(30);
-  expect(output).toHaveBeenCalledTimes(3);
-  expect(data.index).toBe(3);
+    expect(data).toHaveProperty('node.name', 'subscription');
+  });
 });
diff --git a/exchanges/graphcache/src/store/data.test.ts b/exchanges/graphcache/src/store/data.test.ts
index 394e01c5..db75fbad 100644
--- a/exchanges/graphcache/src/store/data.test.ts
+++ b/exchanges/graphcache/src/store/data.test.ts
@@ -218,11 +218,10 @@ describe('commutative changes', () => {
     expect(data.optimisticOrder).toEqual([]);
   });
 
-  it('creates optimistic layers that may be removed using clearLayer', () => {
+  it('creates optimistic layers that may be removed later', () => {
     InMemoryData.reserveLayer(data, 1);
-    InMemoryData.reserveLayer(data, 2);
 
-    InMemoryData.initDataState(data, 2);
+    InMemoryData.initDataState(data, 2, true);
     InMemoryData.writeRecord('Query', 'index', 2);
     InMemoryData.clearDataState();
 
@@ -230,7 +229,7 @@ describe('commutative changes', () => {
     expect(InMemoryData.readRecord('Query', 'index')).toBe(2);
 
     // Actively clearing out layer 2
-    InMemoryData.clearLayer(data, 2);
+    InMemoryData.noopDataState(data, 2);
 
     InMemoryData.initDataState(data, null);
     expect(InMemoryData.readRecord('Query', 'index')).toBe(undefined);
@@ -334,4 +333,77 @@ describe('commutative changes', () => {
     InMemoryData.noopDataState(data, 1);
     expect(data.optimisticOrder).toEqual([]);
   });
+
+  it('respects non-reserved optimistic layers', () => {
+    InMemoryData.reserveLayer(data, 1);
+
+    InMemoryData.initDataState(data, 2, true);
+    InMemoryData.writeRecord('Query', 'index', 2);
+    InMemoryData.clearDataState();
+
+    InMemoryData.reserveLayer(data, 3);
+
+    expect(data.optimisticOrder).toEqual([3, 2, 1]);
+    expect([...data.commutativeKeys]).toEqual([1, 3]);
+
+    InMemoryData.initDataState(data, 1);
+    InMemoryData.writeRecord('Query', 'index', 1);
+    InMemoryData.clearDataState();
+    expect(data.optimisticOrder).toEqual([3, 2]);
+
+    InMemoryData.initDataState(data, null);
+    expect(InMemoryData.readRecord('Query', 'index')).toBe(2);
+
+    InMemoryData.initDataState(data, 3);
+    InMemoryData.writeRecord('Query', 'index', 3);
+    InMemoryData.clearDataState();
+    expect(data.optimisticOrder).toEqual([3, 2]);
+
+    InMemoryData.initDataState(data, null);
+    expect(InMemoryData.readRecord('Query', 'index')).toBe(3);
+  });
+
+  it('squashes when optimistic layers are completed', () => {
+    InMemoryData.reserveLayer(data, 1);
+
+    InMemoryData.initDataState(data, 2, true);
+    InMemoryData.writeRecord('Query', 'index', 2);
+    InMemoryData.clearDataState();
+    expect(data.optimisticOrder).toEqual([2, 1]);
+
+    InMemoryData.initDataState(data, 1);
+    InMemoryData.writeRecord('Query', 'index', 1);
+    InMemoryData.clearDataState();
+    expect(data.optimisticOrder).toEqual([2]);
+
+    // Delete optimistic layer
+    InMemoryData.noopDataState(data, 2);
+    expect(data.optimisticOrder).toEqual([]);
+
+    InMemoryData.initDataState(data, null);
+    expect(InMemoryData.readRecord('Query', 'index')).toBe(1);
+  });
+
+  it('squashes when optimistic layers are replaced with actual data', () => {
+    InMemoryData.reserveLayer(data, 1);
+
+    InMemoryData.initDataState(data, 2, true);
+    InMemoryData.writeRecord('Query', 'index', 2);
+    InMemoryData.clearDataState();
+    expect(data.optimisticOrder).toEqual([2, 1]);
+
+    InMemoryData.initDataState(data, 1);
+    InMemoryData.writeRecord('Query', 'index', 1);
+    InMemoryData.clearDataState();
+    expect(data.optimisticOrder).toEqual([2]);
+
+    // Convert optimistic layer to commutative layer
+    InMemoryData.initDataState(data, 2);
+    InMemoryData.writeRecord('Query', 'index', 2);
+    InMemoryData.clearDataState();
+    expect(data.optimisticOrder).toEqual([]);
+
+    InMemoryData.initDataState(data, null);
+    expect(InMemoryData.readRecord('Query', 'index')).toBe(2);
+  });
 });
diff --git a/packages/core/src/exchanges/ssr.test.ts b/packages/core/src/exchanges/ssr.test.ts
index 7c4f81c1..70a88ee7 100644
--- a/packages/core/src/exchanges/ssr.test.ts
+++ b/packages/core/src/exchanges/ssr.test.ts
@@ -71,6 +71,35 @@ it('caches errored query results correctly', () => {
   });
 });
 
+it('caches complex GraphQLErrors in query results correctly', () => {
+  output.mockReturnValueOnce({
+    ...queryResponse,
+    data: null,
+    error: new CombinedError({
+      graphQLErrors: [
+        {
+          message: 'Oh no!',
+          path: ['Query'],
+          extensions: { test: true },
+        },
+      ],
+    }),
+  });
+
+  const ssr = ssrExchange();
+  const { source: ops$, next } = input;
+  const exchange = ssr(exchangeInput)(ops$);
+
+  publish(exchange);
+  next(queryOperation);
+
+  const error = ssr.extractData()[queryOperation.key].error;
+
+  expect(error).toHaveProperty('graphQLErrors.0.message', 'Oh no!');
+  expect(error).toHaveProperty('graphQLErrors.0.path', ['Query']);
+  expect(error).toHaveProperty('graphQLErrors.0.extensions.test', true);
+});
+
 it('resolves cached query results correctly', () => {
   const onPush = jest.fn();