Repository: FormidableLabs/urql
Pull request #1018 (2020-09-30 10:43:43)

(next) - Move urql to peerDependencies and remove fetch polyfill
## Summary

Having `urql` as a dependency seemed to confuse some package managers about what version of `@urql/core` to take and would often result in duplicates.

Removes the `fetch` polyfill since Next handles this [as of 9.4](https://nextjs.org/blog/next-9-4#improved-built-in-fetch-support)

## Set of changes

- Move `urql` to the peerDependencies (breaking)
- Remove `fetch` polyfill (breaking pre Next 9.4)


changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: 13e61b781c1f5e0d1947a999e4b3cd95c98954ea

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name      | Type  |
| --------- | ----- |
| next-urql | Major |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/FormidableLabs/urql/new/fix-next-dep-issue?filename=.changeset/tricky-candles-drum.md&value=---%0A%22next-urql%22%3A%20patch%0A---%0A%0A(next)%20-%20fix%20dependency%20issue%0A)



Patch:
diff --git a/.changeset/ninety-plants-peel.md b/.changeset/ninety-plants-peel.md
new file mode 100644
index 00000000..37b434b0
--- /dev/null
+++ b/.changeset/ninety-plants-peel.md
@@ -0,0 +1,5 @@
+---
+'@urql/exchange-graphcache': patch
+---
+
+Fix a case where the `offlineExchange` would not start processing operations after hydrating persisted data when no operations arrived in time by the time the persisted data was restored. This would be more evident in Preact and Svelte due to their internal short timings.
diff --git a/.changeset/real-horses-cough.md b/.changeset/real-horses-cough.md
new file mode 100644
index 00000000..ab3ab784
--- /dev/null
+++ b/.changeset/real-horses-cough.md
@@ -0,0 +1,12 @@
+---
+'@urql/svelte': major
+---
+
+Reimplement the `@urql/svelte` API, which is now marked as stable.
+The new `@urql/svelte` API features the `query`, `mutation`, and `subscription` utilities, which are
+called as part of a component's normal lifecycle and accept `operationStore` stores. These are
+writable stores that encapsulate both a GraphQL operation's inputs and outputs (the result)!
+
+Learn more about how to use `@urql/svelte` [in our new API
+docs](https://formidable.com/open-source/urql/docs/api/svelte/) or starting from the [Basics
+pages.](https://formidable.com/open-source/urql/docs/basics/)
diff --git a/.changeset/shy-lemons-hug.md b/.changeset/shy-lemons-hug.md
new file mode 100644
index 00000000..7b530fb4
--- /dev/null
+++ b/.changeset/shy-lemons-hug.md
@@ -0,0 +1,11 @@
+---
+'next-urql': major
+---
+
+Move the `urql` dependency to a peerDependency, migrating can be done by performing:
+
+```sh
+npm i --save urql
+## OR
+yarn add urql
+```
diff --git a/.changeset/weak-seas-reflect.md b/.changeset/weak-seas-reflect.md
new file mode 100644
index 00000000..7341141e
--- /dev/null
+++ b/.changeset/weak-seas-reflect.md
@@ -0,0 +1,9 @@
+---
+'next-urql': major
+---
+
+Remove the automatic polyfilling of `fetch` since this is done automatically starting at
+[`Next v9.4`](https://nextjs.org/blog/next-9-4#improved-built-in-fetch-support)
+
+If you are using a version before 9.4 you can upgrade by installing [`isomorphic-unfetch`](https://www.npmjs.com/package/isomorphic-unfetch)
+and importing it to polyfill the behavior.
diff --git a/docs/advanced/server-side-rendering.md b/docs/advanced/server-side-rendering.md
index b8e72373..ccc33b44 100644
--- a/docs/advanced/server-side-rendering.md
+++ b/docs/advanced/server-side-rendering.md
@@ -181,18 +181,19 @@ We have a custom integration with [`Next.js`](https://nextjs.org/), being [`next
 this integration contains convenience methods specifically for `Next.js`.
 These will simplify the above setup for SSR.
 
-To setup `next-urql`, first we'll install `next-urql` with `react-is` and `isomorphic-unfetch` as
+To setup `next-urql`, first we'll install `next-urql` with `react-is` and `urql` as
 peer dependencies:
 
 ```sh
-yarn add next-urql react-is isomorphic-unfetch
+yarn add next-urql react-is urql
 # or
-npm install --save next-urql react-is isomorphic-unfetch
+npm install --save next-urql react-is urql
 ```
 
-The peer dependency on `react-is` is inherited from `react-ssr-prepass` requiring it, and the peer
-dependency on `isomorphic-unfetch` exists, since `next-urql` automatically injects it as a `fetch`
-polyfill.
+The peer dependency on `react-is` is inherited from `react-ssr-prepass` requiring it.
+
+Note that if you are using Next before v9.4 you'll need to polyfill fetch, this can be
+done through [`isomorphic-unfetch`](https://www.npmjs.com/package/isomorphic-unfetch).
 
 We're now able to wrap any page or `_app.js` using the `withUrqlClient` higher-order component. If
 we wrap `_app.js` we won't have to wrap any individual page, but we also won't be able to make use
@@ -241,8 +242,7 @@ export default withUrqlClient(ssrExchange => ({
 Unless the component that is being wrapped already has a `getInitialProps` method, `next-urql` won't add its own SSR logic, which automatically fetches queries during
 server-side rendering. This can be explicitly enabled by passing the `{ ssr: true }` option as a second argument to `withUrqlClient`.
 
-When you are using `getStaticProps`, `getServerSideProps`, or `getStaticPaths`,  you should opt-out of `Suspense` by setting the `neverSuspend` option to `true` in your `withUrqlClient` configuration.
-your `withUrqlClient`.
+When you are using `getStaticProps`, `getServerSideProps`, or `getStaticPaths`, you should opt-out of `Suspense` by setting the `neverSuspend` option to `true` in your `withUrqlClient` configuration.
 During the prepass of your component tree `next-urql` can't know how these functions will alter the props passed to your page component. This injection
 could change the `variables` used in your `useQuery`. This will lead to error being thrown during the subsequent `toString` pass, which isn't supported in React 16.
 
diff --git a/docs/advanced/subscriptions.md b/docs/advanced/subscriptions.md
index 918eb6c1..e54d7c31 100644
--- a/docs/advanced/subscriptions.md
+++ b/docs/advanced/subscriptions.md
@@ -72,7 +72,7 @@ we return to the `subscriptionExchange` inside `forwardSubscription`.
 ## React & Preact
 
 The `useSubscription` hooks comes with a similar API to `useQuery`, which [we've learned about in
-the "Queries" page in the "Basics" section.](../basics/queries.md)
+the "Queries" page in the "Basics" section.](../basics/queries.md#react--preact)
 
 Its usage is extremely similar in that it accepts options, which may contain `query` and
 `variables`. However, it also accepts a second argument, which is a reducer function, similar to
@@ -130,6 +130,62 @@ the `handleSubscription` function. This works over time, so as
 new messages come in, we will append them to the list of previous
 messages.
 
+## Svelte
+
+The `subscription` function in `@urql/svelte` comes with a similar API to `query`, which [we've
+learned about in the "Queries" page in the "Basics" section.](../basics/queries.md#svelte)
+
+Its usage is extremely similar in that it accepts an `operationStore`, which will typically contain
+our GraphQL subscription query. However, `subscription` also accepts a second argument, which is
+a reducer function, similar to what you would pass to `Array.prototype.reduce`.
+
+It receives the previous set of data that this function has returned or `undefined`.
+As the second argument, it receives the event that has come in from the subscription.
+You can use this to accumulate the data over time, which is useful for a
+list for example.
+
+In the following example, we create a subscription that informs us of
+new messages. We will concatenate the incoming messages so that we
+can display all messages that have come in over the subscription across
+events.
+
+```js
+<script>
+  import { operationStore, subscription } from '@urql/svelte';
+
+  const messages = operationStore(`
+    subscription MessageSub {
+      newMessages {
+        id
+        from
+        text
+      }
+    }
+  `);
+
+  const handleSubscription = (messages = [], data) => {
+    return [data.newMessages, ...messages];
+  };
+
+  subsription(messages, handleSubscription);
+</script>
+
+{#if !$result.data}
+  <p>No new messages</p>
+{:else}
+  <ul>
+    {#each $messages.data as message}
+      <li>{message.from}: "{message.text}"</li>
+    {/each}
+  </ul>
+{/if}
+
+```
+
+As we can see, the `$result.data` is being updated and transformed by the `handleSubscription`
+function. This works over time, so as new messages come in, we will append them to
+the list of previous messages.
+
 ## One-off Subscriptions
 
 When you're using subscriptions directly without `urql`'s framework bindings, you can use the `Client`'s `subscription` method for one-off subscriptions. This method is similar to the ones for mutations and subscriptions [that we've seen before on the "Core Package" page.](../concepts/core-package.md#one-off-queries-and-mutations)
@@ -155,3 +211,4 @@ const { unsubscribe } = pipe(
     console.log(result); // { data: ... }
   })
 );
+```
diff --git a/docs/api/README.md b/docs/api/README.md
index eea0220f..2f8d1ad7 100644
--- a/docs/api/README.md
+++ b/docs/api/README.md
@@ -14,6 +14,7 @@ more about the core package on the "Core Package" page.](../concepts/core-packag
 - [`@urql/core` API docs](./core.md)
 - [`urql` React API docs](./urql.md)
 - [`@urql/preact` Preact API docs](./preact.md)
+- [`@urql/svelte` Svelte API docs](./svelte.md)
 - [`@urql/exchange-graphcache` API docs](./graphcache.md)
 - [`@urql/exchange-retry` API docs](./retry-exchange.md)
 - [`@urql/exchange-execute` API docs](./execute-exchange.md)
diff --git a/docs/api/auth-exchange.md b/docs/api/auth-exchange.md
index ee3cb8f1..5b69d5e1 100644
--- a/docs/api/auth-exchange.md
+++ b/docs/api/auth-exchange.md
@@ -1,6 +1,6 @@
 ---
 title: '@urql/exchange-auth'
-order: 9
+order: 10
 ---
 
 # Authentication Exchange
diff --git a/docs/api/execute-exchange.md b/docs/api/execute-exchange.md
index b3635a24..e014ce93 100644
--- a/docs/api/execute-exchange.md
+++ b/docs/api/execute-exchange.md
@@ -1,6 +1,6 @@
 ---
 title: '@urql/exchange-execute'
-order: 5
+order: 6
 ---
 
 # Execute Exchange
diff --git a/docs/api/graphcache.md b/docs/api/graphcache.md
index 56560e38..4b492e94 100644
--- a/docs/api/graphcache.md
+++ b/docs/api/graphcache.md
@@ -1,6 +1,6 @@
 ---
 title: '@urql/exchange-graphcache'
-order: 3
+order: 4
 ---
 
 # @urql/exchange-graphcache
diff --git a/docs/api/multipart-fetch-exchange.md b/docs/api/multipart-fetch-exchange.md
index 074f254a..e54e7961 100644
--- a/docs/api/multipart-fetch-exchange.md
+++ b/docs/api/multipart-fetch-exchange.md
@@ -1,6 +1,6 @@
 ---
 title: '@urql/exchange-multipart-fetch'
-order: 6
+order: 7
 ---
 
 # Multipart Fetch Exchange
diff --git a/docs/api/persisted-fetch-exchange.md b/docs/api/persisted-fetch-exchange.md
index cd86b5c5..28c33ef6 100644
--- a/docs/api/persisted-fetch-exchange.md
+++ b/docs/api/persisted-fetch-exchange.md
@@ -1,6 +1,6 @@
 ---
 title: '@urql/exchange-persisted-fetch'
-order: 7
+order: 8
 ---
 
 # Persisted Fetch Exchange
diff --git a/docs/api/refocus-exchange.md b/docs/api/refocus-exchange.md
index 268f0b37..2323cd88 100644
--- a/docs/api/refocus-exchange.md
+++ b/docs/api/refocus-exchange.md
@@ -1,6 +1,6 @@
 ---
 title: '@urql/exchange-refocus'
-order: 10
+order: 11
 ---
 
 # Refocus exchange
diff --git a/docs/api/request-policy-exchange.md b/docs/api/request-policy-exchange.md
index a086f321..5f666375 100644
--- a/docs/api/request-policy-exchange.md
+++ b/docs/api/request-policy-exchange.md
@@ -1,6 +1,6 @@
 ---
 title: '@urql/exchange-request-policy'
-order: 8
+order: 9
 ---
 
 # Request Policy Exchange
diff --git a/docs/api/retry-exchange.md b/docs/api/retry-exchange.md
index dc594ee4..82cb5fb0 100644
--- a/docs/api/retry-exchange.md
+++ b/docs/api/retry-exchange.md
@@ -1,6 +1,6 @@
 ---
 title: '@urql/exchange-retry'
-order: 4
+order: 5
 ---
 
 # Retry Exchange
diff --git a/docs/api/svelte.md b/docs/api/svelte.md
new file mode 100644
index 00000000..029da6eb
--- /dev/null
+++ b/docs/api/svelte.md
@@ -0,0 +1,106 @@
+---
+title: '@urql/svelte'
+order: 3
+---
+
+# Svelte API
+
+## operationStore
+
+Accepts three arguments as inputs, where only the first one — `query` — is required.
+
+| Argument  | Type                     | Description                                                                        |
+| --------- | ------------------------ | ---------------------------------------------------------------------------------- |
+| query     | `string \| DocumentNode` | The query to be executed. Accepts as a plain string query or GraphQL DocumentNode. |
+| variables | `?object`                | The variables to be used with the GraphQL request.                                 |
+| context   | `?object`                | Holds the contextual information for the query.                                    |
+
+This is a [Svelte Writable Store](https://svelte.dev/docs#writable) that is used by other utilities
+listed in these docs to read [`Operation` inputs](./core.md#operation) from and write
+[`OperationResult` outputs](./core.md#operationresult) to.
+
+The store has several properties on its value. The **writable properties** of it are inputs that are
+used by either [`query`](#query), [`mutation`](#mutation), or [`subscription`](#subscription) to
+create an [`Operation`](./core.md#operation) to execute. These are `query`, `variables`, and
+`context`; the same properties that the `operationStore` accepts as arguments on creation.
+
+Furthermore the store exposes some **readonly properties** which represent the operation's progress
+and [result](./core.md#operationresult).
+
+| Prop                                                                                      | Type                   | Description                                                                                                                                       |
+| ----------------------------------------------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
+| data                                                                                      | `?any`                 | Data returned by the specified query                                                                                                              |
+| error                                                                                     | `?CombinedError`       | A [`CombinedError`](./core.md#combinederror) instances that wraps network or `GraphQLError`s (if any)                                             |
+| extensions                                                                                | `?Record<string, any>` | Extensions that the GraphQL server may have returned.                                                                                             |
+| stale                                                                                     | `boolean`              | A flag that may be set to `true` by exchanges to indicate that the `data` is incomplete or out-of-date, and that the result will be updated soon. |
+| fetching                                                                                  | `boolean`              | A flag that indicates whether the operation is currently                                                                                          |
+| in progress, which means that the `data` and `error` is out-of-date for the given inputs. |
+
+All of the writable properties are updatable either via the common Svelte Writable's `set` or
+`update` methods or directly. The `operationStore` exposes setters for the writable properties which
+will automatically update the store and notify reactive subscribers.
+
+In development, trying to update the _readonly_ properties directly or via the `set` or `update`
+method will result in a `TypeError` being thrown.
+
+[Read more about `writable` stores on the Svelte API docs.](https://svelte.dev/docs#writable)
+
+## query
+
+The `query` utility function only accepts an `operationStore` as its only argument. Per
+`operationStore` it should only be called once per component as it lives alongside the component and
+hooks into its `onDestroy` lifecycle method. This means that we must avoid passing a reactive
+variable to it, and instead must pass the raw `operationStore`.
+
+This function will return the `operationStore` itself that has been passed.
+
+[Read more about how to use the `query` API on the "Queries" page.](../basics/queries.md#svelte)
+
+## subscription
+
+The `subscription` utility function accepts an `operationStore` as its first argument, like the
+[`query` function](#query). It should also per `operationStore` be called once per component.
+
+The function also optionally accepts a second argument, a `handler` function. This function has the
+following type signature:
+
+```js
+type SubscriptionHandler<T, R> = (previousData: R | undefined, data: T) => R;
+```
+
+This function will be called with the previous data (or `undefined`) and the new data that's
+incoming from a subscription event, and may be used to "reduce" the data over time, altering the
+value of `result.data`.
+
+`subscription` itself will return the `operationStore` that has been passed when called.
+
+[Read more about how to use the `subscription` API on the "Subscriptions"
+page.](../advanced/subscriptions.md#svelte)
+
+## mutation
+
+The `mutation` utility function either accepts an `operationStore` as its only argument or an object
+containing `query`, `variables`, and `context` properties. When it receives the latter it will
+create an `operationStore` automatically.
+
+The function will return an `executeMutation` callback, which can be used to trigger the mutation.
+This callback optionally accepts a `variables` argument and a `context` argument of type
+[`Partial<OperationContext>`](./core.md#operationcontext). If these arguments are passed, they will
+automatically update the `operationStore` before starting the mutation.
+
+The `executeMutation` callback will return a promise which resolves to the `operationStore` once the
+mutation has been completed.
+
+[Read more about how to use the `mutation` API on the "Mutations"
+page.](../basics/mutations.md#svelte)
+
+## Context API
+
+In Svelte the [`Client`](./core.md#client) is passed around using [Svelte's Context
+API](https://svelte.dev/tutorial/context-api). `@urql/svelte` wraps around Svelte's
+[`setContext`](https://svelte.dev/docs#setContext) and
+[`getContext`](https://svelte.dev/docs#getContext) functions and exposes:
+
+- `setClient`
+- `getClient`
+- `initClient` (a shortcut for `createClient` + `setClient`)
diff --git a/docs/basics/getting-started.md b/docs/basics/getting-started.md
index d3ee1bda..67342faf 100644
--- a/docs/basics/getting-started.md
+++ b/docs/basics/getting-started.md
@@ -95,3 +95,104 @@ const App = () => (
 
 Now every component and element inside and under the `Provider` are able to use GraphQL queries that
 will be sent to our API.
+
+[On the next page we'll learn about executing "Queries".](./queries.md#react--preact)
+
+## Svelte
+
+This "Getting Started" guide covers how to install and set up `urql` and provide a `Client` for
+Svelte. The `@urql/svelte` package, which provides bindings for Svelte, doesn't fundamentally
+function differently from `@urql/preact` or `urql` and uses the same [Core Package and
+`Client`](../concepts/core-package.md).
+
+### Installation
+
+Installing `@urql/svelte` is quick and no other packages are immediately necessary.
+
+```sh
+yarn add @urql/svelte graphql
+# or
+npm install --save @urql/svelte graphql
+```
+
+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. That's why we'll need
+to install `graphql` alongside `@urql/svelte`.
+
+Both the `@urql/svelte` and `graphql` packages follow [semantic versioning](https://semver.org) and
+all `@urql/svelte` 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`
+
+The `@urql/svelte` package exports a method called `createClient` which we can use to create
+the GraphQL client. This central `Client` manages all of our GraphQL requests and results.
+
+```js
+import { createClient } from '@urql/svelte';
+
+const client = createClient({
+  url: 'http://localhost:3000/graphql',
+});
+```
+
+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 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.
+
+```js
+const client = createClient({
+  url: 'http://localhost:3000/graphql',
+  fetchOptions: () => {
+    const token = getToken();
+    return {
+      headers: { authorization: token ? `Bearer ${token}` : '' },
+    };
+  },
+});
+```
+
+### Providing the `Client`
+
+To make use of the `Client` in Svelte we will have to provide it via the
+[Context API](https://svelte.dev/tutorial/context-api). From a parent component to its child
+components. This will share one `Client` with the rest of our app, if we for instance provide the
+`Client`
+
+```html
+<script>
+  import { createClient, setClient } from '@urql/svelte';
+
+  const client = createClient({
+    url: 'http://localhost:3000/graphql',
+  });
+
+  setClient(client);
+</script>
+```
+
+The `setClient` method internally calls [Svelte's `setContext`
+function](https://svelte.dev/docs#setContext). The `@urql/svelte` package also exposes a `getClient`
+function that uses [`getContext`](https://svelte.dev/docs#getContext) to retrieve the `Client` in
+child components. This is used throughout `@urql/svelte`'s API.
+
+We can also use a convenience function, `initClient`. This function combines the `createClient` and
+`setClient` calls into one.
+
+```html
+<script>
+  import { initClient } from '@urql/svelte';
+
+  initClient({
+    url: 'http://localhost:3000/graphql',
+  });
+</script>
+```
+
+[On the next page we'll learn about executing "Queries".](./queries.md#svelte)
diff --git a/docs/basics/mutations.md b/docs/basics/mutations.md
index a84de89d..42f65832 100644
--- a/docs/basics/mutations.md
+++ b/docs/basics/mutations.md
@@ -100,3 +100,129 @@ it.](../api/urql.md#usemutation)
 
 [On the next page we'll learn about "Document Caching", `urql`'s default caching
 mechanism.](./document-caching.md)
+
+## Svelte
+
+This guide covers how to send mutations in Svelte using `@urql/svelte`'s `mutation` utility.
+The `mutation` function isn't dissimilar from the `query` function but is triggered manually and
+can accept a [`GraphQLRequest` object](../api/core.md#graphqlrequest) too while also supporting our
+trusty `operationStore`.
+
+### Sending a mutation
+
+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.
+
+```html
+<script>
+  import { mutation } from '@urql/svelte';
+
+  export let id;
+
+  const mutateTodo = mutation({
+    query: `
+      mutation ($id: ID!, $title: String!) {
+        updateTodo (id: $id, title: $title) {
+          id
+          title
+        }
+      }
+    `,
+  });
+
+  function updateTodo(newTitle) {
+    mutateTodo({ id, title: newTitle });
+  }
+</script>
+```
+
+This small call to `mutation` accepts a `query` property (besides the `variables` property) and
+returns an execute function. We've wrapped it in an `updateTodo` function to illustrate its usage.
+
+Unlike the `query` function, the `mutation` function doesn't start our mutation automatically.
+Instead, mutations are started programmatically by calling the function they return. This function
+also returns a promise so that we can use the mutation's result.
+
+### Using the mutation result
+
+When calling `mutateTodo` in our previous example, we start the mutation. To use the mutation's
+result we actually have two options instead of one.
+
+The first option is to use the promise that the `mutation`'s execute function returns. This promise
+will resolve to an `operationStore`, which is what we're used to from sending queries. Using this
+store we can then read the mutation's `data` or `error`.
+
+```html
+<script>
+  import { mutation } from '@urql/svelte';
+
+  export let id;
+
+  const mutateTodo = mutation({
+    query: `
+      mutation ($id: ID!, $title: String!) {
+        updateTodo (id: $id, title: $title) {
+          id
+          title
+        }
+      }
+    `,
+  });
+
+  function updateTodo(newTitle) {
+    mutateTodo({ id, title: newTitle }).then(result => {
+      // The result is an operationStore again, which will already carry the mutation's result
+      console.log(result.data, result.error);
+    });
+  }
+</script>
+```
+
+Alternatively, we can pass `mutation` an `operationStore` directly. This allows us to use a
+mutation's result in our component's UI more easily, without storing it ourselves.
+
+```html
+<script>
+  import { operationStore, mutation } from '@urql/svelte';
+
+  export let id;
+
+  const updateTodoStore = operationStore(`
+    mutation ($id: ID!, $title: String!) {
+      updateTodo (id: $id, title: $title) {
+        id
+        title
+      }
+    }
+  `);
+
+  const updateTodoMutation = mutation(updateTodoStore);
+
+  function updateTodo(newTitle) {
+    updateTodoMutation({ id, title: newTitle });
+  }
+</script>
+
+{#if $updateTodoStore.data} Todo was updated! {/if}
+```
+
+### Handling mutation errors
+
+It's worth noting that the promise we receive when calling the execute function will never
+reject. Instead it will always return a promise that resolves to an `operationStore`, even if the
+mutation has failed.
+
+If you're checking for errors, you should use `operationStore.error` instead, which will be set
+to a `CombinedError` when any kind of errors occurred while executing your mutation.
+[Read more about errors on our "Errors" page.](./errors.md)
+
+```jsx
+mutateTodo({ id, title: newTitle }).then(result => {
+  if (result.error) {
+    console.error('Oh no!', result.error);
+  }
+});
+```
+
+[On the next page we'll learn about "Document Caching", `urql`'s default caching
+mechanism.](./document-caching.md)
diff --git a/docs/basics/queries.md b/docs/basics/queries.md
index aa0cecc5..e23931f7 100644
--- a/docs/basics/queries.md
+++ b/docs/basics/queries.md
@@ -192,4 +192,219 @@ when `pause` is set to `true`, which would usually stop all automatic queries.
 There are some more tricks we can use with `useQuery`. [Read more about its API in the API docs for
 it.](../api/urql.md#usequery)
 
-[On the next page we'll learn about "Mutations" rather than Queries.](./mutations.md)
+[On the next page we'll learn about "Mutations" rather than Queries.](./mutations.md#react--preact)
+
+## Svelte
+
+This guide covers how to query data with Svelte with our `Client` now fully set up and provided via
+the Context API. We'll implement queries using the `operationStore` and the `query` function from
+`@urql/svelte`.
+
+The `operationStore` function creates a [Svelte Writable store](https://svelte.dev/docs#writable).
+You can use it to initialise a data container in `urql`. This store holds on to our query inputs,
+like the GraphQL query and variables, which we can change to launch new queries, and also exposes
+the query's eventual result, which we can then observe.
+
+### Run a first query
+
+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!
+
+```html
+<script>
+  import { operationStore, query } from '@urql/svelte';
+
+  const todos = operationStore(`
+    query {
+      todos {
+        id
+        title
+      }
+    }
+  `);
+
+  query(todos);
+</script>
+
+{#if $todos.fetching}
+<p>Loading...</p>
+{:else if $todos.error}
+<p>Oh no... {$todos.error.message}</p>
+{:else}
+<ul>
+  {#each $todos.data.todos as todo}
+  <li>{todo.title}</li>
+  {/each}
+</ul>
+{/if}
+```
+
+Here we have implemented our first GraphQL query to fetch todos. We're first creating an
+`operationStore` which holds on to our `query` and are then passing the store to the `query`
+function, which starts the GraphQL query.
+
+The `todos` store can now be used like any other Svelte store using a
+[reactive auto-subscription](https://svelte.dev/tutorial/auto-subscriptions) in Svelte. This means
+that we prefix `$todos` with a dollar symbol, which automatically subscribes us to its changes.
+
+The `query` function accepts our store and starts using the `Client` to execute our query. It may
+only be called once for a store and lives alongside the component's lifecycle. It will automatically
+read changes on the `operationStore` and will update our query and results accordingly.
+
+### Variables
+
+Typically we'll also need to pass variables to our queries, for instance, if we are dealing with
+pagination. For this purpose the `operationStore` also accepts a `variables` argument, which we can
+use to supply variables to our query.
+
+```html
+<script>
+  import { operationStore, query } from '@urql/svelte';
+
+  const todos = operationStore(`
+    query ($from: Int!, $limit: Int!) {
+      todos(from: $from, limit: $limit) {
+        id
+        title
+      }
+    },
+    { from, limit }
+  `);
+
+  query(todos);
+</script>
+
+...
+```
+
+As when we're sending GraphQL queries manually using `fetch`, the variables will be attached to the
+`POST` request body that is sent to our GraphQL API.
+
+The `operationStore` also supports being actively changed. This will hook into Svelte's reactivity
+model as well and cause the `query` utility to start a new operation.
+
+```html
+<script>
+  import { operationStore, query } from '@urql/svelte';
+
+  const todos = operationStore(`
+    query ($from: Int!, $limit: Int!) {
+      todos(from: $from, limit: $limit) {
+        id
+        title
+      }
+    },
+    { from, limit }
+  `);
+
+  query(todos);
+
+  function nextPage() {
+    $todos.variables.from += $todos.variables.limit;
+  }
+</script>
+
+<button on:click="{nextPage}">Next page<button></button></button>
+```
+
+The `operationStore` provides getters too so it's also possible for us to pass `todos` around and
+update `todos.variables` or `todos.query` directly. Both, updating `todos.variables` and
+`$todos.variables` in a component for instance, will cause `query` to pick up the update and execute
+our changes.
+
+### Request Policies
+
+The `operationStore` also accepts another argument apart from `query` and `variables`. Optionally
+you may pass a third argument, [the `context` object](../api/core.md#operationcontext). The arguably
+most interesting option the `context` may contain is `requestPolicy`.
+
+The `requestPolicy` option determines how results are retrieved from our `Client`'s cache. By
+default this is set to `cache-first`, which means that we prefer to get results from our cache, but
+are falling back to sending an API request.
+
+In total there are four different policies that we can use:
+
+- `cache-first` (the default) prefers cached results and falls back to sending an API request when
+  no prior result is cached.
+- `cache-and-network` returns cached results but also always sends an API request, which is perfect
+  for displaying data quickly while keeping it up-to-date.
+- `network-only` will always send an API request and will ignore cached results.
+- `cache-only` will always return cached results or `null`.
+
+The `cache-and-network` policy is particularly useful, since it allows us to display data instantly
+if it has been cached, but also refreshes data in our cache in the background. This means though
+that `fetching` will be `false` for cached results although an API request may still be ongoing in
+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.
+
+```html
+<script>
+  import { operationStore, query } from '@urql/svelte';
+
+  const todos = operationStore(`
+    query ($from: Int!, $limit: Int!) {
+      todos(from: $from, limit: $limit) {
+        id
+        title
+      }
+    },
+    { from, limit },
+    { requestPolicy: 'cache-and-network' }
+  `);
+
+  query(todos);
+</script>
+
+...
+```
+
+As we can see, the `requestPolicy` is easily changed here and we can read our `context` option back
+from `todos.context`, just as we can check `todos.query` and `todos.variables`. Updating
+`operationStore.context` can be very useful to also refetch queries, as we'll see in the next
+section.
+
+[You can learn more about request policies on the API docs.](../api/core.md#requestpolicy)
+
+### Reexecuting Queries
+
+The default caching approach in `@urql/svelte` typically takes care of updating queries on the fly
+quite well and does so automatically. Sometimes it may be necessary though to refetch data and to
+execute a query with a different `context`. Triggering a query programmatically may be useful in a
+couple of cases. It can for instance be used to refresh data that is currently being displayed.
+
+We can trigger a new query update by changing out the `context` of our `operationStore`.
+
+```html
+<script>
+  import { operationStore, query } from '@urql/svelte';
+
+  const todos = operationStore(`
+    query ($from: Int!, $limit: Int!) {
+      todos(from: $from, limit: $limit) {
+        id
+        title
+      }
+    },
+    { from, limit },
+    { requestPolicy: 'cache-first' }
+  `);
+
+  query(todos);
+
+  function refresh() {
+    $todos.context = { requestPolicy: 'network-only' };
+  }
+</script>
+```
+
+Calling `refresh` in the above example will execute the query again forcefully, and will skip the
+cache, since we're passing `requestPolicy: 'network-only'`.
+
+### Reading on
+
+There are some more tricks we can use with `operationStore`.
+[Read more about its API in the API docs for it.](../api/svelte.md#operationStore)
+
+[On the next page we'll learn about "Mutations" rather than Queries.](./mutations.md#svelte)
diff --git a/docs/concepts/exchanges.md b/docs/concepts/exchanges.md
index ffd481e6..9e21501c 100644
--- a/docs/concepts/exchanges.md
+++ b/docs/concepts/exchanges.md
@@ -22,7 +22,7 @@ The default set of exchanges that `@urql/core` contains and applies to a `Client
 
 Other available exchanges:
 
-- [`errorExchange`](../api/core.md#errorexchange): Allows a global callback to be called when any error occursz
+- [`errorExchange`](../api/core.md#errorexchange): Allows a global callback to be called when any error occurs
 - [`ssrExchange`](../advanced/server-side-rendering.md): Allows for a server-side renderer to
   collect results for client-side rehydration.
 - [`retryExchange`](../api/retry-exchange.md): Allows operations to be retried
diff --git a/exchanges/graphcache/src/cacheExchange.ts b/exchanges/graphcache/src/cacheExchange.ts
index 9c3bc995..598ea059 100644
--- a/exchanges/graphcache/src/cacheExchange.ts
+++ b/exchanges/graphcache/src/cacheExchange.ts
@@ -11,13 +11,14 @@ import {
 
 import {
   filter,
+  combine,
+  scan,
   map,
   merge,
   pipe,
   share,
   fromPromise,
   fromArray,
-  buffer,
   take,
   mergeMap,
   concat,
@@ -249,10 +250,15 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({
     // If no hydration takes place we replace this stream with an empty one
     const bufferedOps$ = hydration
       ? pipe(
-          sharedOps$,
-          buffer(fromPromise(hydration)),
+          combine(
+            pipe(
+              sharedOps$,
+              scan((acc: Operation[], x) => (acc.push(x), acc), [])
+            ),
+            fromPromise(hydration)
+          ),
           take(1),
-          mergeMap(fromArray)
+          mergeMap(zip => fromArray(zip[0]))
         )
       : (empty as Source<Operation>);
 
diff --git a/packages/next-urql/README.md b/packages/next-urql/README.md
index 2ea48d25..61dc6ed6 100644
--- a/packages/next-urql/README.md
+++ b/packages/next-urql/README.md
@@ -25,11 +25,16 @@ Using GraphQL with server-side rendering in React is a challenging problem. Curr
 Install `next-urql` along with its `peerDependencies`.
 
 ```sh
-npm install --save next-urql urql react-is isomorphic-unfetch
+yarn add next-urql react-is urql
+# or
+npm install --save next-urql react-is urql
 ```
 
 `react-is` helps to support server-side `Suspense` with `react-ssr-prepass`. This assumes you have followed the basic installation steps for `urql` [here](https://github.com/FormidableLabs/urql#installation).
 
+Note that if you are using Next before v9.4 you'll need to polyfill fetch, this can be
+done through [`isomorphic-unfetch`](https://www.npmjs.com/package/isomorphic-unfetch).
+
 ### Usage
 
 To use `next-urql`, first `import` the `withUrqlClient` higher order component.
@@ -247,7 +252,7 @@ component has children using `getInitialProps` but that component itself is not
 `withUrqlClient`, this means `withUrqlClient(() => clientOptiosn, { ssr:true })`.
 This measure is available so we can support `getStaticProps`, ...
 
-When you are using `getStaticProps`, `getServerSideProps`, or `getStaticPaths`,  you should opt-out of `Suspense` by setting the `neverSuspend` option to `true` in your `withUrqlClient` configuration.
+When you are using `getStaticProps`, `getServerSideProps`, or `getStaticPaths`, you should opt-out of `Suspense` by setting the `neverSuspend` option to `true` in your `withUrqlClient` configuration.
 your `withUrqlClient`.
 During the prepass of your component tree `next-urql` can't know how these functions will alter the props passed to your page component. This injection
 could change the `variables` used in your `useQuery`. This will lead to error being thrown during the subsequent `toString` pass, which isn't supported in React 16.
diff --git a/packages/next-urql/package.json b/packages/next-urql/package.json
index a9366b28..3f7075a5 100644
--- a/packages/next-urql/package.json
+++ b/packages/next-urql/package.json
@@ -43,18 +43,16 @@
     "enzyme-adapter-react-16": "^1.15.2",
     "graphql": "^15.1.0",
     "graphql-tag": "^2.10.3",
-    "isomorphic-unfetch": "^3.0.0",
     "next": "^9.2.1",
     "react": "^16.13.0",
     "react-dom": "^16.13.0",
     "react-is": "^16.12.0"
   },
   "dependencies": {
-    "react-ssr-prepass": "^1.2.1",
-    "urql": ">=1.10.0"
+    "react-ssr-prepass": "^1.2.1"
   },
   "peerDependencies": {
-    "isomorphic-unfetch": "^3.0.0",
-    "react": ">=16.8.0"
+    "react": ">=16.8.0",
+    "urql": "^1.10.0"
   }
 }
diff --git a/packages/next-urql/src/init-urql-client.ts b/packages/next-urql/src/init-urql-client.ts
index c37fdaac..8b978919 100644
--- a/packages/next-urql/src/init-urql-client.ts
+++ b/packages/next-urql/src/init-urql-client.ts
@@ -1,5 +1,4 @@
 import { createClient, Client, ClientOptions } from 'urql';
-import 'isomorphic-unfetch';
 
 let urqlClient: Client | null = null;
 
diff --git a/packages/svelte-urql/package.json b/packages/svelte-urql/package.json
index 828ecf9d..56ecde04 100644
--- a/packages/svelte-urql/package.json
+++ b/packages/svelte-urql/package.json
@@ -55,7 +55,7 @@
     "svelte": "^3.0.0"
   },
   "dependencies": {
-    "@urql/core": "^1.12.3",
+    "@urql/core": "^1.13.1",
     "wonka": "^4.0.14"
   },
   "devDependencies": {
diff --git a/packages/svelte-urql/src/context.ts b/packages/svelte-urql/src/context.ts
index 96e8da2e..f9443b55 100644
--- a/packages/svelte-urql/src/context.ts
+++ b/packages/svelte-urql/src/context.ts
@@ -1,12 +1,11 @@
 import { setContext, getContext } from 'svelte';
 import { Client, ClientOptions } from '@urql/core';
+import { _contextKey } from './internal';
 
-const CLIENT = '$$_URQL';
-
-export const getClient = (): Client => getContext(CLIENT);
+export const getClient = (): Client => getContext(_contextKey);
 
 export const setClient = (client: Client): void => {
-  setContext(CLIENT, client);
+  setContext(_contextKey, client);
 };
 
 export const initClient = (args: ClientOptions): Client => {
diff --git a/packages/svelte-urql/src/index.ts b/packages/svelte-urql/src/index.ts
index 662f1395..f8acafa6 100644
--- a/packages/svelte-urql/src/index.ts
+++ b/packages/svelte-urql/src/index.ts
@@ -1,3 +1,4 @@
 export * from '@urql/core';
+export * from './operationStore';
 export * from './context';
 export * from './operations';
diff --git a/packages/svelte-urql/src/internal.ts b/packages/svelte-urql/src/internal.ts
new file mode 100644
index 00000000..58ab8e1f
--- /dev/null
+++ b/packages/svelte-urql/src/internal.ts
@@ -0,0 +1,6 @@
+export const _contextKey = '$$_urql';
+export const _storeUpdate = new Set<object>();
+export const _markStoreUpdate =
+  process.env.NODE_ENV !== 'production'
+    ? (value: object) => _storeUpdate.add(value)
+    : () => undefined;
diff --git a/packages/svelte-urql/src/operationStore.ts b/packages/svelte-urql/src/operationStore.ts
new file mode 100644
index 00000000..e2009580
--- /dev/null
+++ b/packages/svelte-urql/src/operationStore.ts
@@ -0,0 +1,137 @@
+import { Readable, writable } from 'svelte/store';
+import { DocumentNode } from 'graphql';
+import { OperationContext, CombinedError } from '@urql/core';
+
+import { _storeUpdate } from './internal';
+
+const emptyUpdate = Object.create(null);
+
+type Updater<T> = (value: T) => T;
+
+/**
+ * This Svelte store wraps both a `GraphQLRequest` and an `OperationResult`.
+ * It can be used to update the query and read the subsequent result back.
+ */
+export interface OperationStore<Data = any, Vars = any>
+  extends Readable<OperationStore<Data, Vars>> {
+  // Input properties
+  query: DocumentNode | string;
+  variables: Vars | undefined | null;
+  context: Partial<OperationContext> | undefined;
+  // Output properties
+  readonly stale: boolean;
+  readonly fetching: boolean;
+  readonly data: Data | void;
+  readonly error?: CombinedError | void;
+  readonly extensions?: Record<string, any> | void;
+  // Writable properties
+  set(value: Partial<OperationStore<Data, Vars>>): void;
+  update(updater: Updater<Partial<OperationStore<Data, Vars>>>): void;
+}
+
+export function operationStore<Data = any, Vars = object>(
+  query: string | DocumentNode,
+  variables?: Vars | null,
+  context?: Partial<OperationContext>
+): OperationStore<Data, Vars> {
+  const internal = {
+    query,
+    variables,
+    context,
+  };
+
+  const state = {
+    stale: false,
+    fetching: true,
+    data: undefined,
+    error: undefined,
+    extensions: undefined,
+  } as OperationStore<Data, Vars>;
+
+  const svelteStore = writable(state);
+  let _internalUpdate = false;
+
+  state.set = function set(value?: Partial<typeof state>) {
+    if (!value) value = emptyUpdate;
+
+    _internalUpdate = true;
+    if (process.env.NODE_ENV !== 'production') {
+      if (!_storeUpdate.has(value!)) {
+        for (const key in value) {
+          if (!(key in internal)) {
+            throw new TypeError(
+              'It is not allowed to update result properties on an OperationStore.'
+            );
+          }
+        }
+      }
+
+      _storeUpdate.delete(value!);
+    }
+
+    for (const key in value) {
+      if (key === 'query' || key === 'variables' || key === 'context') {
+        (internal as any)[key] = value[key];
+      } else if (key === 'stale' || key === 'fetching') {
+        (state as any)[key] = !!value[key];
+      } else if (key in state) {
+        state[key] = value[key];
+      }
+    }
+
+    _internalUpdate = false;
+    svelteStore.set(state);
+  };
+
+  state.update = function update(fn: Updater<typeof state>): void {
+    state.set(fn(state));
+  };
+
+  state.subscribe = function subscribe(run, invalidate) {
+    return svelteStore.subscribe(run, invalidate);
+  };
+
+  Object.keys(internal).forEach(prop => {
+    Object.defineProperty(state, prop, {
+      configurable: false,
+      get: () => internal[prop],
+      set(value) {
+        internal[prop] = value;
+        if (!_internalUpdate) svelteStore.set(state);
+      },
+    });
+  });
+
+  if (process.env.NODE_ENV !== 'production') {
+    const result = { ...state };
+
+    Object.keys(state).forEach(prop => {
+      Object.defineProperty(result, prop, {
+        configurable: false,
+        get() {
+          return state[prop];
+        },
+        set() {
+          throw new TypeError(
+            'It is not allowed to update result properties on an OperationStore.'
+          );
+        },
+      });
+    });
+
+    Object.keys(internal).forEach(prop => {
+      Object.defineProperty(result, prop, {
+        configurable: false,
+        get: () => internal[prop],
+        set(value) {
+          internal[prop] = value;
+          if (!_internalUpdate) svelteStore.set(state);
+        },
+      });
+    });
+
+    return result;
+  }
+
+  return state;
+}
diff --git a/packages/svelte-urql/src/operations.ts b/packages/svelte-urql/src/operations.ts
new file mode 100644
index 00000000..688c1872
--- /dev/null
+++ b/packages/svelte-urql/src/operations.ts
@@ -0,0 +1,150 @@
+import { createRequest, OperationContext, GraphQLRequest } from '@urql/core';
+import { onDestroy } from 'svelte';
+
+import {
+  pipe,
+  map,
+  make,
+  scan,
+  concat,
+  fromValue,
+  switchMap,
+  subscribe,
+} from 'wonka';
+
+import { OperationStore, operationStore } from './operationStore';
+import { getClient } from './context';
+import { _markStoreUpdate } from './internal';
+
+const baseState: Partial<OperationStore> = {
+  fetching: false,
+  stale: false,
+  error: undefined,
+  data: undefined,
+  extensions: undefined,
+};
+
+const toSource = (store: OperationStore) => {
+  return make<GraphQLRequest>(observer => {
+    let $request: void | GraphQLRequest;
+    let $context: void | Partial<OperationContext>;
+    return store.subscribe(state => {
+      const request = createRequest(state.query, state.variables as any);
+      if (
+        $context !== state.context ||
+        !$request ||
+        request.key !== $request.key
+      ) {
+        $context = state.context;
+        observer.next(($request = request));
+      }
+    });
+  });
+};
+
+export function query<T = any, V = object>(
+  store: OperationStore<T, V>
+): OperationStore<T, V> {
+  const client = getClient();
+  const subscription = pipe(
+    toSource(store),
+    switchMap(request => {
+      return concat<Partial<OperationStore>>([
+        fromValue({ fetching: true, stale: false }),
+        pipe(
+          client.executeQuery(request, store.context!),
+          map(result => ({ fetching: false, ...result }))
+        ),
+        fromValue({ fetching: false, stale: false }),
+      ]);
+    }),
+    scan(
+      (result, partial) => ({
+        ...result,
+        ...partial,
+      }),
+      baseState
+    ),
+    subscribe(update => {
+      _markStoreUpdate(update);
+      store.set(update as OperationStore);
+    })
+  );
+
+  onDestroy(subscription.unsubscribe);
+  return store;
+}
+
+export type SubscriptionHandler<T, R> = (prev: R | undefined, data: T) => R;
+
+export function subscription<T = any, R = T, V = object>(
+  store: OperationStore<T, V>,
+  handler?: SubscriptionHandler<T, R>
+): OperationStore<T, V> {
+  const client = getClient();
+  const subscription = pipe(
+    toSource(store),
+    switchMap(request => {
+      return concat<Partial<OperationStore>>([
+        fromValue({ fetching: true, stale: false }),
+        client.executeSubscription(request, store.context),
+        fromValue({ fetching: false, stale: false }),
+      ]);
+    }),
+    scan((result, partial: any) => {
+      const data =
+        partial.data !== undefined
+          ? typeof handler === 'function'
+            ? handler(result.data, partial.data)
+            : partial.data
+          : result.data;
+      return { ...result, ...partial, data };
+    }, baseState),
+    subscribe(update => {
+      _markStoreUpdate(update);
+      store.set(update as OperationStore);
+    })
+  );
+
+  onDestroy(subscription.unsubscribe);
+  return store;
+}
+
+export type ExecuteMutation<T = any, V = object> = (
+  variables?: V,
+  context?: Partial<OperationContext>
+) => Promise<OperationStore<T, V>>;
+
+export function mutation<T = any, V = object>(
+  input: GraphQLRequest | OperationStore<T, V>
+): ExecuteMutation<V> {
+  const client = getClient();
+
+  const store =
+    typeof (input as any).subscribe !== 'function'
+      ? operationStore(input.query, input.variables)
+      : (input as OperationStore);
+
+  return (vars, context) => {
+    const update = {
+      fetching: true,
+      variables: vars || store.variables,
+      context: context || store.context,
+    };
+
+    _markStoreUpdate(update);
+    store.set(update);
+
+    return new Promise(resolve => {
+      client
+        .mutation(store.query, store.variables, store.context)
+        .toPromise()
+        .then(result => {
+          const update = { fetching: false, ...result };
+          _markStoreUpdate(update);
+          store.set(update);
+          resolve(store);
+        });
+    });
+  };
+}
diff --git a/packages/svelte-urql/src/operations/constants.ts b/packages/svelte-urql/src/operations/constants.ts
deleted file mode 100644
index 2a3c57f4..00000000
--- a/packages/svelte-urql/src/operations/constants.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export const initialState = {
-  fetching: false,
-  stale: false,
-  error: undefined,
-  data: undefined,
-  operation: undefined,
-  extensions: undefined,
-};
diff --git a/packages/svelte-urql/src/operations/index.ts b/packages/svelte-urql/src/operations/index.ts
deleted file mode 100644
index 48447ffd..00000000
--- a/packages/svelte-urql/src/operations/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export * from './mutate';
-export * from './query';
-export * from './subscription';
diff --git a/packages/svelte-urql/src/operations/mutate.ts b/packages/svelte-urql/src/operations/mutate.ts
deleted file mode 100644
index 7f2686e7..00000000
--- a/packages/svelte-urql/src/operations/mutate.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { pipe, subscribe } from 'wonka';
-import { OperationResult, OperationContext } from '@urql/core';
-import { Readable } from 'svelte/store';
-import { DocumentNode } from 'graphql';
-
-import { getClient } from '../context';
-
-export interface MutationArguments<V> {
-  query: string | DocumentNode;
-  variables?: V;
-  context?: Partial<OperationContext>;
-}
-
-export interface MutationStore<T = any, V = object>
-  extends Readable<OperationResult<T>>,
-    PromiseLike<OperationResult<T>> {
-  (additionalArgs?: Partial<MutationArguments<V>>): Promise<OperationResult<T>>;
-}
-
-export const mutate = <T = any, V = object>(
-  args: MutationArguments<V>
-): MutationStore<T, V> => {
-  const client = getClient();
-
-  function mutate$(additionalArgs?: Partial<MutationArguments<V>>) {
-    const mergedArgs = { ...args, ...additionalArgs };
-    return client
-      .mutation(
-        mergedArgs.query,
-        mergedArgs.variables as any,
-        mergedArgs.context
-      )
-      .toPromise();
-  }
-
-  mutate$.subscribe = (onValue: (result: OperationResult<T>) => void) => {
-    return pipe(
-      client.mutation(args.query, args.variables as any, args.context),
-      subscribe(onValue)
-    ).unsubscribe;
-  };
-
-  mutate$.then = (
-    onValue: (result: OperationResult<T>) => any
-  ): Promise<any> => {
-    return mutate$().then(onValue);
-  };
-
-  return mutate$ as any;
-};
diff --git a/packages/svelte-urql/src/operations/query.ts b/packages/svelte-urql/src/operations/query.ts
deleted file mode 100644
index 47a01dda..00000000
--- a/packages/svelte-urql/src/operations/query.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-import {
-  pipe,
-  makeSubject,
-  fromValue,
-  switchMap,
-  onStart,
-  concat,
-  scan,
-  map,
-  take,
-  share,
-  subscribe,
-  publish,
-  toPromise,
-} from 'wonka';
-
-import {
-  RequestPolicy,
-  OperationContext,
-  CombinedError,
-  Operation,
-} from '@urql/core';
-
-import { Readable } from 'svelte/store';
-import { DocumentNode } from 'graphql';
-
-import { getClient } from '../context';
-import { initialState } from './constants';
-
-export interface QueryArguments<V> {
-  query: string | DocumentNode;
-  variables?: V;
-  requestPolicy?: RequestPolicy;
-  pollInterval?: number;
-  pause?: boolean;
-  context?: Partial<OperationContext>;
-}
-
-export interface QueryResult<T> {
-  fetching: boolean;
-  stale: boolean;
-  data?: T;
-  error?: CombinedError;
-  extensions?: Record<string, any>;
-  operation?: Operation;
-}
-
-export interface QueryStore<T = any, V = object>
-  extends Readable<QueryResult<T>>,
-    PromiseLike<QueryResult<T>> {
-  (args?: Partial<QueryArguments<V>>): QueryStore<T>;
-}
-
-export const query = <T = any, V = object>(
-  args: QueryArguments<V>
-): QueryStore<T, V> => {
-  const client = getClient();
-  const { source: args$, next: nextArgs } = makeSubject<QueryArguments<V>>();
-
-  const queryResult$ = pipe(
-    args$,
-    switchMap(args => {
-      if (args.pause) {
-        return fromValue({ fetching: false, stale: false });
-      }
-
-      return concat([
-        // Initially set fetching to true
-        fromValue({ fetching: true, stale: false }),
-        pipe(
-          client.query<T>(args.query, args.variables, {
-            requestPolicy: args.requestPolicy,
-            pollInterval: args.pollInterval,
-            ...args.context,
-          }),
-          map(({ stale, data, error, extensions, operation }) => ({
-            fetching: false,
-            stale: !!stale,
-            data,
-            error,
-            extensions,
-            operation,
-          }))
-        ),
-        // When the source proactively closes, fetching is set to false
-        fromValue({ fetching: false, stale: false }),
-      ]);
-    }),
-    // The individual partial results are merged into each previous result
-    scan(
-      (result, partial) => ({
-        ...result,
-        ...partial,
-      }),
-      initialState
-    ),
-    share
-  );
-
-  publish(queryResult$);
-
-  const queryStore = (baseArgs: QueryArguments<V>): QueryStore<T, V> => {
-    const result$ = pipe(
-      queryResult$,
-      onStart(() => {
-        nextArgs({ ...baseArgs, ...args });
-      })
-    );
-
-    function query$(args?: Partial<QueryArguments<V>>) {
-      return queryStore({
-        ...baseArgs,
-        ...args,
-      });
-    }
-
-    query$.subscribe = (onValue: (result: QueryResult<T>) => void) => {
-      return pipe(result$, subscribe(onValue)).unsubscribe;
-    };
-
-    query$.then = (onValue: (result: QueryResult<T>) => any): Promise<any> => {
-      return pipe(result$, take(1), toPromise).then(onValue);
-    };
-
-    return query$ as any;
-  };
-
-  return queryStore(args);
-};
diff --git a/packages/svelte-urql/src/operations/subscription.ts b/packages/svelte-urql/src/operations/subscription.ts
deleted file mode 100644
index 972e2ea8..00000000
--- a/packages/svelte-urql/src/operations/subscription.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-import {
-  pipe,
-  makeSubject,
-  fromValue,
-  switchMap,
-  onStart,
-  concat,
-  scan,
-  map,
-  share,
-  subscribe,
-  publish,
-} from 'wonka';
-
-import { OperationContext, CombinedError, Operation } from '@urql/core';
-import { Readable } from 'svelte/store';
-import { DocumentNode } from 'graphql';
-
-import { getClient } from '../context';
-import { initialState } from './constants';
-
-export interface SubscriptionArguments<V> {
-  query: string | DocumentNode;
-  variables?: V;
-  pause?: boolean;
-  context?: Partial<OperationContext>;
-  operation?: Operation;
-}
-
-export type SubscriptionHandler<T, R> = (prev: R | undefined, data: T) => R;
-
-export interface SubscriptionResult<T> {
-  fetching: boolean;
-  stale: boolean;
-  data?: T;
-  error?: CombinedError;
-  extensions?: Record<string, any>;
-}
-
-export interface SubscriptionStore<T = any, R = T, V = object>
-  extends Readable<SubscriptionResult<T>> {
-  (args?: Partial<SubscriptionArguments<V>>): SubscriptionStore<T, R, V>;
-}
-
-export const subscription = <T = any, R = T, V = object>(
-  args: SubscriptionArguments<V>,
-  handler?: SubscriptionHandler<T, R>
-): SubscriptionStore<T, R, V> => {
-  const client = getClient();
-  const { source: args$, next: nextArgs } = makeSubject<
-    SubscriptionArguments<V>
-  >();
-
-  const subscriptionResult$ = pipe(
-    args$,
-    switchMap(args => {
-      if (args.pause) {
-        return fromValue({ fetching: false, stale: false });
-      }
-
-      return concat([
-        // Initially set fetching to true
-        fromValue({ fetching: true, stale: false }),
-        pipe(
-          client.subscription<T>(args.query, args.variables, args.context),
-          map(({ stale, data, error, extensions, operation }) => ({
-            fetching: false,
-            stale: !!stale,
-            data,
-            error,
-            extensions,
-            operation,
-          }))
-        ),
-        // When the source proactively closes, fetching is set to false
-        fromValue({ fetching: false, stale: false }),
-      ]);
-    }),
-    // The individual partial results are merged into each previous result
-    scan((result, partial: any) => {
-      const data =
-        partial.data !== undefined
-          ? typeof handler === 'function'
-            ? handler(result.data, partial.data)
-            : partial.data
-          : result.data;
-      return { ...result, ...partial, data };
-    }, initialState),
-    share
-  );
-
-  publish(subscriptionResult$);
-
-  const subscriptionStore = (
-    baseArgs: SubscriptionArguments<V>
-  ): SubscriptionStore<T, R, V> => {
-    function subscription$(args?: Partial<SubscriptionArguments<V>>) {
-      return subscriptionStore({
-        ...baseArgs,
-        ...args,
-      });
-    }
-
-    subscription$.subscribe = (
-      onValue: (result: SubscriptionResult<T>) => void
-    ) => {
-      return pipe(
-        subscriptionResult$,
-        onStart(() => {
-          nextArgs({ ...baseArgs, ...args });
-        }),
-        subscribe(onValue)
-      ).unsubscribe;
-    };
-
-    return subscription$ as any;
-  };
-
-  return subscriptionStore(args);
-};
diff --git a/yarn.lock b/yarn.lock
index aefb65b0..f28919e3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9802,14 +9802,6 @@ isobject@^4.0.0:
   resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
   integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
 
-isomorphic-unfetch@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f"
-  integrity sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==
-  dependencies:
-    node-fetch "^2.6.1"
-    unfetch "^4.2.0"
-
 isstream@0.1.x, isstream@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@@ -11665,7 +11657,7 @@ node-fetch@2.6.0:
   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
   integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
 
-node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1:
+node-fetch@^2.5.0, node-fetch@^2.6.0:
   version "2.6.1"
   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
   integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
@@ -16506,7 +16498,7 @@ unbzip2-stream@^1.0.9:
     buffer "^5.2.1"
     through "^2.3.8"
 
-unfetch@^4.1.0, unfetch@^4.2.0:
+unfetch@^4.1.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be"
   integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==


Test patch:
diff --git a/packages/svelte-urql/src/operationStore.test.ts b/packages/svelte-urql/src/operationStore.test.ts
new file mode 100644
index 00000000..8c263cba
--- /dev/null
+++ b/packages/svelte-urql/src/operationStore.test.ts
@@ -0,0 +1,81 @@
+import { operationStore } from './operationStore';
+import { _markStoreUpdate } from './internal';
+
+it('instantiates an operation container acting as a store', () => {
+  const variables = {};
+  const context = {};
+  const store = operationStore('{ test }', variables, context);
+
+  expect(store.query).toBe('{ test }');
+  expect(store.variables).toBe(variables);
+  expect(store.context).toBe(context);
+
+  const subscriber = jest.fn();
+  store.subscribe(subscriber);
+
+  expect(subscriber).toHaveBeenCalledWith(store);
+  expect(subscriber).toHaveBeenCalledTimes(1);
+
+  store.set({ query: '{ test2 }' });
+  expect(subscriber).toHaveBeenCalledWith(store);
+  expect(subscriber).toHaveBeenCalledTimes(2);
+
+  expect(store.query).toBe('{ test2 }');
+});
+
+it('adds getters and setters for known values', () => {
+  const variables = {};
+  const context = {};
+  const store = operationStore('{ test }', variables, context);
+
+  const update = {
+    query: '{ update }',
+    variables: undefined,
+    context: { requestPolicy: 'cache-and-network' },
+    stale: true,
+    fetching: true,
+    data: { update: true },
+    error: undefined,
+    extensions: undefined,
+  };
+
+  _markStoreUpdate(update);
+  store.set(update as any);
+
+  expect(store.query).toBe(update.query);
+  expect(store.variables).toBe(update.variables);
+  expect(store.context).toBe(update.context);
+  expect(store.stale).toBe(update.stale);
+  expect(store.fetching).toBe(update.fetching);
+  expect(store.data).toBe(update.data);
+  expect(store.error).toBe(update.error);
+  expect(store.extensions).toBe(update.extensions);
+
+  const subscriber = jest.fn();
+  store.subscribe(subscriber);
+  expect(subscriber).toHaveBeenCalledTimes(1);
+
+  const state = subscriber.mock.calls[0][0];
+  expect(state.stale).toBe(true);
+  expect(state.query).toBe('{ update }');
+
+  store.query = '{ imperative }';
+  expect(subscriber).toHaveBeenCalledTimes(2);
+  expect(store.query).toBe('{ imperative }');
+});
+
+it('throws when illegal values are set', () => {
+  const store = operationStore('{ test }');
+
+  expect(() => {
+    (store as any).variables = {};
+  }).not.toThrow();
+
+  expect(() => {
+    (store as any).data = {};
+  }).toThrow();
+
+  expect(() => {
+    (store as any).set({ error: null });
+  }).toThrow();
+});
diff --git a/packages/svelte-urql/src/operations.test.ts b/packages/svelte-urql/src/operations.test.ts
new file mode 100644
index 00000000..5cbd3634
--- /dev/null
+++ b/packages/svelte-urql/src/operations.test.ts
@@ -0,0 +1,243 @@
+import { makeSubject, pipe, take, toPromise } from 'wonka';
+import { createClient } from '@urql/core';
+import { operationStore } from './operationStore';
+import { query, subscription, mutation } from './operations';
+
+const client = createClient({ url: '/graphql' });
+
+jest.mock('./context', () => ({ getClient: () => client }));
+jest.mock('svelte', () => ({ onDestroy: () => undefined }));
+
+beforeEach(() => {
+  jest.resetAllMocks();
+});
+
+describe('query', () => {
+  it('subscribes to a query and updates data', () => {
+    const subscriber = jest.fn();
+    const subject = makeSubject<any>();
+    const executeQuery = jest
+      .spyOn(client, 'executeQuery')
+      .mockImplementation(() => subject.source);
+
+    const store = operationStore('{ test }');
+    store.subscribe(subscriber);
+
+    query(store);
+
+    expect(executeQuery).toHaveBeenCalledWith(
+      {
+        key: expect.any(Number),
+        query: expect.any(Object),
+        variables: {},
+      },
+      undefined
+    );
+
+    expect(subscriber).toHaveBeenCalledTimes(2);
+    expect(store.fetching).toBe(true);
+
+    subject.next({ data: { test: true } });
+    expect(subscriber).toHaveBeenCalledTimes(3);
+    expect(store.data).toEqual({ test: true });
+
+    subject.complete();
+    expect(subscriber).toHaveBeenCalledTimes(4);
+    expect(store.fetching).toBe(false);
+  });
+
+  it('updates the executed query when inputs change', () => {
+    const subscriber = jest.fn();
+    const subject = makeSubject<any>();
+    const executeQuery = jest
+      .spyOn(client, 'executeQuery')
+      .mockImplementation(() => subject.source);
+
+    const store = operationStore('{ test }');
+    store.subscribe(subscriber);
+
+    query(store);
+
+    expect(executeQuery).toHaveBeenCalledWith(
+      {
+        key: expect.any(Number),
+        query: expect.any(Object),
+        variables: {},
+      },
+      undefined
+    );
+
+    subject.next({ data: { test: true } });
+    expect(subscriber).toHaveBeenCalledTimes(3);
+    expect(store.data).toEqual({ test: true });
+
+    store.variables = { test: true };
+    expect(executeQuery).toHaveBeenCalledTimes(2);
+    expect(executeQuery).toHaveBeenCalledWith(
+      {
+        key: expect.any(Number),
+        query: expect.any(Object),
+        variables: { test: true },
+      },
+      undefined
+    );
+
+    expect(subscriber).toHaveBeenCalledTimes(5);
+    expect(store.fetching).toBe(true);
+    expect(store.data).toEqual({ test: true });
+
+    store.context = { requestPolicy: 'cache-and-network' };
+    expect(executeQuery).toHaveBeenCalledTimes(3);
+    expect(executeQuery).toHaveBeenCalledWith(
+      {
+        key: expect.any(Number),
+        query: expect.any(Object),
+        variables: { test: true },
+      },
+      {
+        requestPolicy: 'cache-and-network',
+      }
+    );
+  });
+});
+
+describe('subscription', () => {
+  it('subscribes to a subscription and updates data', () => {
+    const subscriber = jest.fn();
+    const subject = makeSubject<any>();
+    const executeQuery = jest
+      .spyOn(client, 'executeSubscription')
+      .mockImplementation(() => subject.source);
+
+    const store = operationStore('subscription { test }');
+    store.subscribe(subscriber);
+
+    subscription(store);
+
+    expect(executeQuery).toHaveBeenCalledWith(
+      {
+        key: expect.any(Number),
+        query: expect.any(Object),
+        variables: {},
+      },
+      undefined
+    );
+
+    expect(subscriber).toHaveBeenCalledTimes(2);
+    expect(store.fetching).toBe(true);
+
+    subject.next({ data: { test: true } });
+    expect(subscriber).toHaveBeenCalledTimes(3);
+    expect(store.data).toEqual({ test: true });
+
+    subject.complete();
+    expect(subscriber).toHaveBeenCalledTimes(4);
+    expect(store.fetching).toBe(false);
+  });
+
+  it('updates the executed subscription when inputs change', () => {
+    const subscriber = jest.fn();
+    const subject = makeSubject<any>();
+    const executeSubscription = jest
+      .spyOn(client, 'executeSubscription')
+      .mockImplementation(() => subject.source);
+
+    const store = operationStore('{ test }');
+    store.subscribe(subscriber);
+
+    subscription(store);
+
+    expect(executeSubscription).toHaveBeenCalledWith(
+      {
+        key: expect.any(Number),
+        query: expect.any(Object),
+        variables: {},
+      },
+      undefined
+    );
+
+    subject.next({ data: { test: true } });
+    expect(subscriber).toHaveBeenCalledTimes(3);
+    expect(store.data).toEqual({ test: true });
+
+    store.variables = { test: true };
+    expect(executeSubscription).toHaveBeenCalledTimes(2);
+    expect(executeSubscription).toHaveBeenCalledWith(
+      {
+        key: expect.any(Number),
+        query: expect.any(Object),
+        variables: { test: true },
+      },
+      undefined
+    );
+
+    expect(subscriber).toHaveBeenCalledTimes(5);
+    expect(store.fetching).toBe(true);
+    expect(store.data).toEqual({ test: true });
+  });
+
+  it('supports a custom scanning handler', () => {
+    const subscriber = jest.fn();
+    const subject = makeSubject<any>();
+    const executeSubscription = jest
+      .spyOn(client, 'executeSubscription')
+      .mockImplementation(() => subject.source);
+
+    const store = operationStore('subscription { counter }');
+    store.subscribe(subscriber);
+
+    subscription(store, (prev, current) => ({
+      counter: (prev ? prev.counter : 0) + current.counter,
+    }));
+
+    expect(executeSubscription).toHaveBeenCalledWith(
+      {
+        key: expect.any(Number),
+        query: expect.any(Object),
+        variables: {},
+      },
+      undefined
+    );
+
+    subject.next({ data: { counter: 1 } });
+    expect(subscriber).toHaveBeenCalledTimes(3);
+    expect(store.data).toEqual({ counter: 1 });
+
+    subject.next({ data: { counter: 2 } });
+    expect(subscriber).toHaveBeenCalledTimes(4);
+    expect(store.data).toEqual({ counter: 3 });
+  });
+});
+
+describe('mutation', () => {
+  it('provides an execute method that resolves a promise', async () => {
+    const subscriber = jest.fn();
+    const subject = makeSubject<any>();
+    const clientMutation = jest
+      .spyOn(client, 'mutation')
+      .mockImplementation((): any => ({
+        toPromise() {
+          return pipe(subject.source, take(1), toPromise);
+        },
+      }));
+
+    const store = operationStore('mutation { test }', { test: false });
+    store.subscribe(subscriber);
+
+    const start = mutation(store);
+    expect(subscriber).toHaveBeenCalledTimes(1);
+    expect(clientMutation).not.toHaveBeenCalled();
+
+    const result$ = start({ test: true });
+    expect(subscriber).toHaveBeenCalledTimes(2);
+    expect(store.fetching).toBe(true);
+    expect(store.variables).toEqual({ test: true });
+
+    subject.next({ data: { test: true } });
+    expect(await result$).toEqual(store);
+
+    expect(subscriber).toHaveBeenCalledTimes(3);
+    expect(store.fetching).toBe(false);
+    expect(store.data).toEqual({ test: true });
+  });
+});