Repository: FormidableLabs/urql (ts)

Issue:
svelte: variables reactivity
urql version & exchanges:

```json
"@urql/exchange-graphcache": "3.1.3",
"@urql/svelte": "https://pkg.csb.dev/FormidableLabs/urql/commit/4550be11/@urql/svelte",
"graphql": "15.3.0",
"graphql-tag": "2.11.0",
"svelte": "3.29.0",
```

**Steps to reproduce**

1. Start the server opening this page: https://codesandbox.io/s/apollo-server-urql-svelte-crud-h1pcl
1. Open Incognito browser session
1. Go to reproduction: https://77lev.csb.app/
1. Click on "Todos list"
1. Change `first` value using the only input available

**Expected behavior**

New API call on each change

**Actual behavior**

No API call at all

---

If you go [here](https://codesandbox.io/s/urql-svelte-100-variables-reactivity-77lev?file=/List.svelte) you can find this:

```js
// THIS DOESN'T WORK
$: {
  console.log("todos.variables BEFORE:", todos.variables);
  todos.variables.input.first = first;
  console.log("todos.variables AFTER:", todos.variables);
}

// THIS WORKS
// $: {
//   console.log("todos.variables BEFORE:", todos.variables);
//   todos.variables = { ...todos.variables, input: { first } };
//   console.log("todos.variables AFTER:", todos.variables);
// }
```

The second works but is too complicated. **You can imagine when there are so many variables to consider.**

Is this the correct way? Am I wrong?

Can you suggest something alternative?

**Additional**

[CodeSandBox client project](https://codesandbox.io/s/urql-svelte-100-variables-reactivity-77lev?file=/List.svelte)
[CodeSandBox server project](https://codesandbox.io/s/apollo-server-urql-svelte-crud-h1pcl)

Pull request description:
(svelte) - Add pause support and fix update edge case
- Add support for `context.pause` to restore functionality like other bindings' to pause executing
- Fix an edge case where Svelte wouldn't correctly update the store, which lead to a faulty error (Fix #1036)
- Update `context` comparison code to deeply compare context changes (because Svelte lacks primitives for easy memoisation)

Patch:
diff --git a/.changeset/lovely-gorillas-change.md b/.changeset/lovely-gorillas-change.md
new file mode 100644
index 00000000..cd22af7a
--- /dev/null
+++ b/.changeset/lovely-gorillas-change.md
@@ -0,0 +1,5 @@
+---
+'@urql/svelte': patch
+---
+
+Fix an issue where updated `context` options wouldn't cause a new query to be executed, or updates to the store would erroneously throw a debug error.
diff --git a/.changeset/yellow-toes-decide.md b/.changeset/yellow-toes-decide.md
new file mode 100644
index 00000000..b4ee798b
--- /dev/null
+++ b/.changeset/yellow-toes-decide.md
@@ -0,0 +1,5 @@
+---
+'@urql/svelte': minor
+---
+
+Support passing `pause` to stop executing queries or subscriptions.
diff --git a/docs/basics/queries.md b/docs/basics/queries.md
index 50e956da..d8ca4622 100644
--- a/docs/basics/queries.md
+++ b/docs/basics/queries.md
@@ -210,7 +210,7 @@ the query's eventual result, which we can then observe.
 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
+```jsx
 <script>
   import { operationStore, query } from '@urql/svelte';
 
@@ -257,7 +257,7 @@ Typically we'll also need to pass variables to our queries, for instance, if we
 pagination. For this purpose the `operationStore` also accepts a `variables` argument, which we can
 use to supply variables to our query.
 
-```html
+```jsx
 <script>
   import { operationStore, query } from '@urql/svelte';
 
@@ -283,7 +283,7 @@ As when we're sending GraphQL queries manually using `fetch`, the variables will
 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
+```jsx
 <script>
   import { operationStore, query } from '@urql/svelte';
 
@@ -304,7 +304,7 @@ model as well and cause the `query` utility to start a new operation.
   }
 </script>
 
-<button on:click="{nextPage}">Next page<button></button></button>
+<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
@@ -312,6 +312,41 @@ update `todos.variables` or `todos.query` directly. Both, updating `todos.variab
 `$todos.variables` in a component for instance, will cause `query` to pick up the update and execute
 our changes.
 
+### Pausing Queries
+
+In some cases we may want our queries to not execute until a pre-condition has been met. Since the
+`query` operation exists for the entire component lifecycle however, it can't just be stopped and
+started at will. Instead, the `query`'s third argument, the `context`, may have an added `pause`
+option that can be set to `true` to temporarily _freeze_ all changes and stop requests.
+
+For instance we may start out with a paused store and then unpause it once a callback is invoked:
+
+```html
+<script>
+  import { operationStore, query } from '@urql/svelte';
+
+  const todo = operationStore(
+    `
+    query {
+      todo {
+        id
+        title
+      }
+    }`,
+    undefined,
+    { pause: true }
+  );
+
+  query(todo);
+
+  function nextPage() {
+    $todo.context.pause = false;
+  }
+</script>
+
+<button on:click="{unpause}">Unpause</button>
+```
+
 ### Request Policies
 
 The `operationStore` also accepts another argument apart from `query` and `variables`. Optionally
@@ -339,7 +374,7 @@ 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
+```jsx
 <script>
   import { operationStore, query } from '@urql/svelte';
 
@@ -376,7 +411,7 @@ couple of cases. It can for instance be used to refresh data that is currently b
 
 We can trigger a new query update by changing out the `context` of our `operationStore`.
 
-```html
+```jsx
 <script>
   import { operationStore, query } from '@urql/svelte';
 
diff --git a/packages/svelte-urql/example/src/App.svelte b/packages/svelte-urql/example/src/App.svelte
index a5dc570e..54b0fc1c 100644
--- a/packages/svelte-urql/example/src/App.svelte
+++ b/packages/svelte-urql/example/src/App.svelte
@@ -3,6 +3,8 @@
 
   initClient({ url: "https://0ufyz.sse.codesandbox.io" });
 
+  let pause = false;
+
   const todos = operationStore(`
     query {
       todos {
@@ -11,21 +13,28 @@
         complete
       }
     }
-  `);
+  `, undefined, { pause: false });
 
   query(todos);
+
+  function clickPause() {
+    $todos.context = { pause: !todos.context.pause }
+  }
 </script>
 
-{#if $todos.fetching}
-  Loading...
-{:else if $todos.error}
-  Oh no! {$todos.error.message}
-{:else if !$todos.data}
-  No data
-{:else}
-  <ul>
-    {#each $todos.data.todos as todo}
-      <li>{todo.text}</li>
-    {/each}
-  </ul>
-{/if}
+<div>
+  {#if $todos.fetching}
+    Loading...
+  {:else if $todos.error}
+    Oh no! {$todos.error.message}
+  {:else if !$todos.data}
+    No data
+  {:else}
+    <ul>
+      {#each $todos.data.todos as todo}
+        <li>{todo.text}</li>
+      {/each}
+    </ul>
+  {/if}
+  <button on:click={clickPause}>Pause</button>
+</div>
diff --git a/packages/svelte-urql/src/operationStore.ts b/packages/svelte-urql/src/operationStore.ts
index 9e117382..12d87f5d 100644
--- a/packages/svelte-urql/src/operationStore.ts
+++ b/packages/svelte-urql/src/operationStore.ts
@@ -32,7 +32,7 @@ export interface OperationStore<Data = any, Vars = any>
 export function operationStore<Data = any, Vars = object>(
   query: string | DocumentNode,
   variables?: Vars | null,
-  context?: Partial<OperationContext>
+  context?: Partial<OperationContext & { pause: boolean }>
 ): OperationStore<Data, Vars> {
   const internal = {
     query,
@@ -52,7 +52,7 @@ export function operationStore<Data = any, Vars = object>(
   let _internalUpdate = false;
 
   state.set = function set(value?: Partial<typeof state>) {
-    if (!value) value = emptyUpdate;
+    if (!value || value === state) value = emptyUpdate;
 
     _internalUpdate = true;
     if (process.env.NODE_ENV !== 'production') {
diff --git a/packages/svelte-urql/src/operations.ts b/packages/svelte-urql/src/operations.ts
index 688c1872..53781ef2 100644
--- a/packages/svelte-urql/src/operations.ts
+++ b/packages/svelte-urql/src/operations.ts
@@ -1,7 +1,14 @@
-import { createRequest, OperationContext, GraphQLRequest } from '@urql/core';
 import { onDestroy } from 'svelte';
 
 import {
+  createRequest,
+  stringifyVariables,
+  OperationContext,
+  GraphQLRequest,
+} from '@urql/core';
+
+import {
+  Source,
   pipe,
   map,
   make,
@@ -16,6 +23,12 @@ import { OperationStore, operationStore } from './operationStore';
 import { getClient } from './context';
 import { _markStoreUpdate } from './internal';
 
+interface SourceRequest extends GraphQLRequest {
+  context?: Partial<OperationContext> & {
+    pause?: boolean;
+  };
+}
+
 const baseState: Partial<OperationStore> = {
   fetching: false,
   stale: false,
@@ -25,18 +38,26 @@ const baseState: Partial<OperationStore> = {
 };
 
 const toSource = (store: OperationStore) => {
-  return make<GraphQLRequest>(observer => {
+  return make<SourceRequest>(observer => {
     let $request: void | GraphQLRequest;
-    let $context: void | Partial<OperationContext>;
+    let $contextKey: void | string;
     return store.subscribe(state => {
-      const request = createRequest(state.query, state.variables as any);
+      const request = createRequest(
+        state.query,
+        state.variables as any
+      ) as SourceRequest;
+
+      const contextKey = stringifyVariables((request.context = state.context));
+
       if (
-        $context !== state.context ||
-        !$request ||
-        request.key !== $request.key
+        $request === undefined ||
+        request.key !== $request.key ||
+        $contextKey === undefined ||
+        contextKey !== $contextKey
       ) {
-        $context = state.context;
-        observer.next(($request = request));
+        $contextKey = contextKey;
+        $request = request;
+        observer.next(request);
       }
     });
   });
@@ -48,16 +69,22 @@ export function query<T = any, V = object>(
   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 }),
-      ]);
-    }),
+    switchMap(
+      (request): Source<Partial<OperationStore>> => {
+        if (request.context && request.context.pause) {
+          return fromValue({ fetching: false, stale: false });
+        }
+
+        return concat<Partial<OperationStore>>([
+          fromValue({ fetching: true, stale: false }),
+          pipe(
+            client.executeQuery(request, request.context!),
+            map(result => ({ fetching: false, ...result }))
+          ),
+          fromValue({ fetching: false, stale: false }),
+        ]);
+      }
+    ),
     scan(
       (result, partial) => ({
         ...result,
@@ -84,13 +111,19 @@ export function subscription<T = any, R = T, V = object>(
   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 }),
-      ]);
-    }),
+    switchMap(
+      (request): Source<Partial<OperationStore>> => {
+        if (request.context && request.context.pause) {
+          return fromValue({ fetching: false, stale: false });
+        }
+
+        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


Test patch:
diff --git a/packages/svelte-urql/src/operationStore.test.ts b/packages/svelte-urql/src/operationStore.test.ts
index 73b74c1a..966bbb4c 100644
--- a/packages/svelte-urql/src/operationStore.test.ts
+++ b/packages/svelte-urql/src/operationStore.test.ts
@@ -10,7 +10,11 @@ it('instantiates an operation container acting as a store', () => {
   expect(store.variables).toBe(variables);
   expect(store.context).toBe(context);
 
-  const subscriber = jest.fn();
+  let state: any;
+  const subscriber = jest.fn($state => {
+    state = $state;
+  });
+
   store.subscribe(subscriber);
 
   expect(subscriber).toHaveBeenCalledWith(store);
@@ -19,8 +23,11 @@ it('instantiates an operation container acting as a store', () => {
   store.set({ query: '{ test2 }' });
   expect(subscriber).toHaveBeenCalledWith(store);
   expect(subscriber).toHaveBeenCalledTimes(2);
-
   expect(store.query).toBe('{ test2 }');
+
+  // Svelte's reactive updates will call set with an identity state value
+  store.set(state);
+  expect(subscriber).toHaveBeenCalledTimes(3);
 });
 
 it('adds getters and setters for known values', () => {
diff --git a/packages/svelte-urql/src/operations.test.ts b/packages/svelte-urql/src/operations.test.ts
index 5cbd3634..473eaab9 100644
--- a/packages/svelte-urql/src/operations.test.ts
+++ b/packages/svelte-urql/src/operations.test.ts
@@ -3,7 +3,7 @@ import { createClient } from '@urql/core';
 import { operationStore } from './operationStore';
 import { query, subscription, mutation } from './operations';
 
-const client = createClient({ url: '/graphql' });
+const client = createClient({ url: '/graphql', exchanges: [] });
 
 jest.mock('./context', () => ({ getClient: () => client }));
 jest.mock('svelte', () => ({ onDestroy: () => undefined }));
@@ -30,6 +30,7 @@ describe('query', () => {
         key: expect.any(Number),
         query: expect.any(Object),
         variables: {},
+        context: undefined,
       },
       undefined
     );
@@ -46,6 +47,29 @@ describe('query', () => {
     expect(store.fetching).toBe(false);
   });
 
+  it('pauses the query when requested to do so', () => {
+    const subscriber = jest.fn();
+    const subject = makeSubject<any>();
+    const executeQuery = jest
+      .spyOn(client, 'executeQuery')
+      .mockImplementation(() => subject.source);
+
+    const store = operationStore('{ test }', undefined, { pause: true });
+    store.subscribe(subscriber);
+
+    query(store);
+
+    expect(executeQuery).not.toHaveBeenCalled();
+    expect(subscriber).toHaveBeenCalledTimes(2);
+    expect(store.fetching).toBe(false);
+    expect(store.stale).toBe(false);
+
+    store.set({ context: { pause: false } });
+    expect(executeQuery).toHaveBeenCalled();
+    expect(subscriber).toHaveBeenCalledTimes(4);
+    expect(store.fetching).toBe(true);
+  });
+
   it('updates the executed query when inputs change', () => {
     const subscriber = jest.fn();
     const subject = makeSubject<any>();
@@ -63,6 +87,7 @@ describe('query', () => {
         key: expect.any(Number),
         query: expect.any(Object),
         variables: {},
+        context: undefined,
       },
       undefined
     );
@@ -78,6 +103,7 @@ describe('query', () => {
         key: expect.any(Number),
         query: expect.any(Object),
         variables: { test: true },
+        context: undefined,
       },
       undefined
     );
@@ -93,6 +119,7 @@ describe('query', () => {
         key: expect.any(Number),
         query: expect.any(Object),
         variables: { test: true },
+        context: { requestPolicy: 'cache-and-network' },
       },
       {
         requestPolicy: 'cache-and-network',