Repository: FormidableLabs/urql (ts)

Issue:
RFC: Update refocusExchange to utilize Page Visibility API
See: https://github.com/FormidableLabs/urql/discussions/1055

## Summary

There are known issues that occur with the `focus` event:

- `iframe` can cause the `focus` event to fire numerous times if the user interacts with the `iframe` and then back with the current document
- DevTools can cause the `focus` event to fire numerous times if the user/developer interacts with the DevTools and then back with the document. (A very common case for developers)
- A developer fires a `focus` event (ie. `node.dispatchEvent(new CustomEvent('focus', { bubbles: true}))`). This case seems less likely but still a side-effect of the current implementation.

We may also want to check what other libraries, like `swr`, are doing.

## Proposed Solution

- Check whether the Page Visiblity API can help with this
- Reimplement `refocusExchange` with the new API
- Preserve IE11 support with fallbacks, polyfills, or workarounds (or an alternative approach entirely?)


Pull request description:
(refocus) - Switch to visibilitychange api
Resolves #1063 


## Summary

Use the `visibilitychange` api.

## Set of changes

* Switch from `focus` to `visibilitychange` api


I looked at [SWR](https://github.com/vercel/swr/blob/4d6be3df441f925d235a4b26914efcc85ae335a3/src/use-swr.ts#L61) and [react-query](https://github.com/tannerlinsley/react-query/blob/master/src/core/setFocusHandler.ts#L14) code and noticed that they refetch on both `focus` and `visibilitychange` events. As you can see in the video below, `visibilitychange` does not fire when we switch from one window to another (while `focus` does). It actually makes sense since the visibility of the document does not change. According to #1063, `focus` has some issues so I guess it's fine to only refetch on `visibilitychange`?

![ezgif com-gif-maker (1)](https://user-images.githubusercontent.com/5595092/96684965-bf392d80-137c-11eb-8c3b-4b3b8e91834c.gif)









Patch:
diff --git a/.changeset/dirty-dragons-try.md b/.changeset/dirty-dragons-try.md
new file mode 100644
index 00000000..781615a8
--- /dev/null
+++ b/.changeset/dirty-dragons-try.md
@@ -0,0 +1,5 @@
+---
+'@urql/exchange-refocus': minor
+---
+
+Switch from a `focus-event` triggering the refetch to a change in [`page-visbility`](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API). This means that interacting with an `iframe` and then going back to the page won't trigger a refetch, interacting with Devtools won't cause refetches and a bubbled `focusEvent` won't trigger a refetch.
diff --git a/.changeset/gorgeous-queens-fetch.md b/.changeset/gorgeous-queens-fetch.md
new file mode 100644
index 00000000..5cbafe82
--- /dev/null
+++ b/.changeset/gorgeous-queens-fetch.md
@@ -0,0 +1,5 @@
+---
+'@urql/exchange-graphcache': patch
+---
+
+Enforce atomic optimistic updates so that optimistic layers are cleared before they're reapplied. This is important for instance when an optimistic update is performed while offline and then reapplied while online, which would previously repeat the optimistic update on top of its past data changes.
diff --git a/exchanges/graphcache/src/offlineExchange.ts b/exchanges/graphcache/src/offlineExchange.ts
index 147ce5c6..181a79f3 100644
--- a/exchanges/graphcache/src/offlineExchange.ts
+++ b/exchanges/graphcache/src/offlineExchange.ts
@@ -73,32 +73,42 @@ export const offlineExchange = (opts: CacheExchangeOpts): Exchange => input => {
     storage.writeMetadata
   ) {
     const { forward: outerForward, client, dispatchDebug } = input;
+    const { source: reboundOps$, next } = makeSubject<Operation>();
     const optimisticMutations = opts.optimistic || {};
     const failedQueue: Operation[] = [];
 
     const updateMetadata = () => {
       const requests: SerializedRequest[] = [];
       for (let i = 0; i < failedQueue.length; i++) {
-        const op = failedQueue[i];
-        if (op.operationName === 'mutation') {
+        const operation = failedQueue[i];
+        if (operation.operationName === 'mutation') {
           requests.push({
-            query: print(op.query),
-            variables: op.variables,
+            query: print(operation.query),
+            variables: operation.variables,
           });
         }
       }
       storage.writeMetadata!(requests);
     };
 
-    let _flushing = false;
+    let isFlushingQueue = false;
     const flushQueue = () => {
-      if (!_flushing) {
-        _flushing = true;
-        let operation: void | Operation;
-        while ((operation = failedQueue.shift()))
-          client.reexecuteOperation(operation);
+      if (!isFlushingQueue) {
+        isFlushingQueue = true;
+
+        for (let i = 0; i < failedQueue.length; i++) {
+          const operation = failedQueue[i];
+          if (operation.operationName === 'mutation') {
+            next({ ...operation, operationName: 'teardown' });
+          }
+        }
+
+        for (let i = 0; i < failedQueue.length; i++)
+          client.reexecuteOperation(failedQueue[i]);
+
+        failedQueue.length = 0;
+        isFlushingQueue = false;
         updateMetadata();
-        _flushing = false;
       }
     };
 
@@ -145,7 +155,6 @@ export const offlineExchange = (opts: CacheExchangeOpts): Exchange => input => {
 
     return ops$ => {
       const sharedOps$ = share(ops$);
-      const { source: reboundOps$, next } = makeSubject<Operation>();
       const opsAndRebound$ = merge([reboundOps$, sharedOps$]);
 
       return pipe(
diff --git a/exchanges/refocus/src/refocusExchange.ts b/exchanges/refocus/src/refocusExchange.ts
index ecf65987..653cf055 100644
--- a/exchanges/refocus/src/refocusExchange.ts
+++ b/exchanges/refocus/src/refocusExchange.ts
@@ -6,14 +6,19 @@ export const refocusExchange = (): Exchange => {
     const watchedOperations = new Map<number, Operation>();
     const observedOperations = new Map<number, number>();
 
-    window.addEventListener('focus', () => {
-      watchedOperations.forEach(op => {
-        client.reexecuteOperation(
-          client.createRequestOperation('query', op, {
-            requestPolicy: 'cache-and-network',
-          })
-        );
-      });
+    window.addEventListener('visibilitychange', () => {
+      if (
+        typeof document !== 'object' ||
+        document.visibilityState === 'visible'
+      ) {
+        watchedOperations.forEach(op => {
+          client.reexecuteOperation(
+            client.createRequestOperation('query', op, {
+              requestPolicy: 'cache-and-network',
+            })
+          );
+        });
+      }
     });
 
     const processIncomingOperation = (op: Operation) => {


Test patch:
diff --git a/exchanges/graphcache/src/test-utils/examples-1.test.ts b/exchanges/graphcache/src/test-utils/examples-1.test.ts
index b3a7031f..0dff0159 100644
--- a/exchanges/graphcache/src/test-utils/examples-1.test.ts
+++ b/exchanges/graphcache/src/test-utils/examples-1.test.ts
@@ -552,3 +552,71 @@ it('allows cumulative optimistic updates', () => {
     ],
   });
 });
+
+it('supports clearing a layer then reapplying optimistic updates', () => {
+  let counter = 1;
+
+  const store = new Store({
+    updates: {
+      Mutation: {
+        addTodo: (result, _, cache) => {
+          cache.updateQuery({ query: Todos }, data => {
+            (data as any).todos.push(result.addTodo);
+            return data as Data;
+          });
+        },
+      },
+    },
+    optimistic: {
+      addTodo: () => ({
+        __typename: 'Todo',
+        id: 'optimistic_' + ++counter,
+        text: '',
+        complete: false,
+      }),
+    },
+  });
+
+  const todosData = {
+    __typename: 'Query',
+    todos: [
+      { id: '0', complete: true, text: '0', __typename: 'Todo' },
+      { id: '1', complete: true, text: '1', __typename: 'Todo' },
+    ],
+  };
+
+  write(store, { query: Todos }, todosData);
+
+  const AddTodo = gql`
+    mutation {
+      __typename
+      addTodo {
+        __typename
+        complete
+        text
+        id
+      }
+    }
+  `;
+
+  writeOptimistic(store, { query: AddTodo }, 1);
+  writeOptimistic(store, { query: AddTodo }, 1);
+
+  InMemoryData.noopDataState(store.data, 1);
+
+  writeOptimistic(store, { query: AddTodo }, 1);
+  writeOptimistic(store, { query: AddTodo }, 1);
+
+  const queryRes = query(store, { query: Todos });
+
+  expect(queryRes.partial).toBe(false);
+  expect(queryRes.data).toEqual({
+    ...todosData,
+    todos: [
+      todosData.todos[0],
+      todosData.todos[1],
+      { __typename: 'Todo', text: '', complete: false, id: 'optimistic_4' },
+      { __typename: 'Todo', text: '', complete: false, id: 'optimistic_5' },
+    ],
+  });
+});
diff --git a/exchanges/refocus/src/refocusExchange.test.ts b/exchanges/refocus/src/refocusExchange.test.ts
index b6a988cd..c4de8c9b 100644
--- a/exchanges/refocus/src/refocusExchange.test.ts
+++ b/exchanges/refocus/src/refocusExchange.test.ts
@@ -77,7 +77,7 @@ it(`attaches a listener and redispatches queries on call`, () => {
   );
 
   expect(spy).toBeCalledTimes(1);
-  expect(spy).toBeCalledWith('focus', expect.anything());
+  expect(spy).toBeCalledWith('visibilitychange', expect.anything());
 
   next(op);