Repository: FormidableLabs/urql (ts)

Issue:
Under certain conditions a different key is generated for the same DocumentNode
Under certain conditions (all of which are a bit confusing) a different key is generated (by [`keyDocument()`](https://github.com/FormidableLabs/urql/blob/ed45f960122e5e3848dfbb4382304f774417b5e9/packages/core/src/utils/request.ts#L56-L87)) given the same DocumentNode.

This is noticeable in a next-urql app when we see unexpected cache misses during either final server-side render (`Error: ReactDOMServer does not yet support Suspense.`) or client-side rehydration (since cached data isn't found, React warns about unexpected dom nodes, and data is fetched over network again).

versions are the latest as of writing this:
- urql @ v2.0.2
- next-urql @ v3.0.1

Uses next-urql's default exchanges

**Steps to reproduce**

1. Clone & install the reproduction project: https://github.com/zenflow/urql-documentnode-key-generation-issue
2. Start dev server with `yarn next dev`
3. Open http://localhost:3000 & open Chrome devtools
4. `touch pages/index.tsx`, or make a trivial change to it's contents like `<h1>` to `<h2>`
5. Wait for recompile & then refresh the browser
6. Observe error rendering server-side: `Error: ReactDOMServer does not yet support Suspense.`
7. Refresh browser (or allow it to automatically refresh) until a page loads without the above error
8. Observe console warning & network request to graphql server

**Analysis**

To debug I built `packages/core` with [`hash()`](https://github.com/FormidableLabs/urql/blob/ed45f960122e5e3848dfbb4382304f774417b5e9/packages/core/src/utils/hash.ts#L14) modified to `console.log` it's input (unique graphql query string) & output (hash number). 

I found that, for my single query, either of two different query strings would be hashed: `"query MyQuery { ... }"` (bare) or `"# MyQuery\nquery MyQuery { ... }"` (prefixed), resulting in a different hash number.

See in [`stringifyDocument()`](https://github.com/FormidableLabs/urql/blob/ed45f960122e5e3848dfbb4382304f774417b5e9/packages/core/src/utils/request.ts#L24-L52) where the query string gets prefixed.

I see a problem that `stringifyDocument()` is not idempotent. When first called with a DocumentNode that has no `.loc` property, it adds the `.loc` property and returns only the bare query string. The next time it's called with the exact same DocumentNode object, since the `.loc` property is now present, the result will be prefixed with operation name if available. 

TBH It was pretty confusing, because the code looks ok at first glance - you would think `stringifyDocument()` wouldn't be called twice with the same object anyway, due to short-circuiting in [`keyDocument()`](https://github.com/FormidableLabs/urql/blob/ed45f960122e5e3848dfbb4382304f774417b5e9/packages/core/src/utils/request.ts#L56-L73) when that object has a `.__key` property defined. 
But I looked closer and noticed that this `.__key` property is only added to the document objects that are being kept in the [`docs` Map](https://github.com/FormidableLabs/urql/blob/ed45f960122e5e3848dfbb4382304f774417b5e9/packages/core/src/utils/request.ts#L54). Document objects that never get added to the `docs` Map (because another identical document object was "keyed" first) never have their `.__key` property defined, and are therefore stringified & hashed on each call to `keyDocument()`.

Pull request:
(core) - Make DocumentNode serialization idempotent
## Summary

Fixes #1508

## Set of changes

This makes DocumentNode serialization idempotent.
In other words, this makes it so we get the same serial representation (and therefore hash key) for a DocumentNode each time it is computed.


Patch:
diff --git a/.changeset/fair-beds-happen.md b/.changeset/fair-beds-happen.md
new file mode 100644
index 00000000..b0381fbb
--- /dev/null
+++ b/.changeset/fair-beds-happen.md
@@ -0,0 +1,5 @@
+---
+'@urql/core': patch
+---
+
+Fix inconsistency in generating keys for `DocumentNode`s, especially when using GraphQL Code Generator, which could cause SSR serialization to fail.
diff --git a/packages/core/src/utils/request.ts b/packages/core/src/utils/request.ts
index a8050965..f4905494 100644
--- a/packages/core/src/utils/request.ts
+++ b/packages/core/src/utils/request.ts
@@ -32,10 +32,12 @@ export const stringifyDocument = (
     .trim();
 
   if (typeof node !== 'string') {
-    if (node.loc) {
-      const operationName = 'definitions' in node && getOperationName(node);
-      if (operationName) str = `# ${operationName}\n${str}`;
-    } else {
+    const operationName = 'definitions' in node && getOperationName(node);
+    if (operationName) {
+      str = `# ${operationName}\n${str}`;
+    }
+
+    if (!node.loc) {
       (node as WritableLocation).loc = {
         start: 0,
         end: str.length,


Test patch:
diff --git a/exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap b/exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap
index 34b8d666..032bfc68 100644
--- a/exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap
+++ b/exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap
@@ -16,7 +16,7 @@ Object {
     "key": 2,
     "kind": "query",
     "query": Object {
-      "__key": 3044551916,
+      "__key": 3521976120,
       "definitions": Array [
         Object {
           "directives": Array [],
@@ -140,9 +140,10 @@ Object {
       ],
       "kind": "Document",
       "loc": Object {
-        "end": 76,
+        "end": 86,
         "source": Object {
-          "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }",
+          "body": "# getUser
+query getUser($name: String) { user(name: $name) { id firstName lastName } }",
           "locationOffset": Object {
             "column": 1,
             "line": 1,
@@ -185,7 +186,7 @@ Object {
     "key": 2,
     "kind": "query",
     "query": Object {
-      "__key": 3044551916,
+      "__key": 3521976120,
       "definitions": Array [
         Object {
           "directives": Array [],
@@ -309,9 +310,10 @@ Object {
       ],
       "kind": "Document",
       "loc": Object {
-        "end": 76,
+        "end": 86,
         "source": Object {
-          "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }",
+          "body": "# getUser
+query getUser($name: String) { user(name: $name) { id firstName lastName } }",
           "locationOffset": Object {
             "column": 1,
             "line": 1,
@@ -356,7 +358,7 @@ Object {
     "key": 2,
     "kind": "query",
     "query": Object {
-      "__key": 3044551916,
+      "__key": 3521976120,
       "definitions": Array [
         Object {
           "directives": Array [],
@@ -480,9 +482,10 @@ Object {
       ],
       "kind": "Document",
       "loc": Object {
-        "end": 76,
+        "end": 86,
         "source": Object {
-          "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }",
+          "body": "# getUser
+query getUser($name: String) { user(name: $name) { id firstName lastName } }",
           "locationOffset": Object {
             "column": 1,
             "line": 1,
@@ -529,7 +532,7 @@ Object {
     "key": 3,
     "kind": "mutation",
     "query": Object {
-      "__key": 3781860981,
+      "__key": 4034972436,
       "definitions": Array [
         Object {
           "directives": Array [],
@@ -627,9 +630,10 @@ Object {
       ],
       "kind": "Document",
       "loc": Object {
-        "end": 102,
+        "end": 125,
         "source": Object {
-          "body": "mutation uploadProfilePicture($picture: File) { uploadProfilePicture(picture: $picture) { location } }",
+          "body": "# uploadProfilePicture
+mutation uploadProfilePicture($picture: File) { uploadProfilePicture(picture: $picture) { location } }",
           "locationOffset": Object {
             "column": 1,
             "line": 1,
@@ -678,7 +682,7 @@ Object {
     "key": 3,
     "kind": "mutation",
     "query": Object {
-      "__key": 1193185401,
+      "__key": 2033658603,
       "definitions": Array [
         Object {
           "directives": Array [],
@@ -780,9 +784,10 @@ Object {
       ],
       "kind": "Document",
       "loc": Object {
-        "end": 108,
+        "end": 132,
         "source": Object {
-          "body": "mutation uploadProfilePictures($pictures: [File]) { uploadProfilePicture(pictures: $pictures) { location } }",
+          "body": "# uploadProfilePictures
+mutation uploadProfilePictures($pictures: [File]) { uploadProfilePicture(pictures: $pictures) { location } }",
           "locationOffset": Object {
             "column": 1,
             "line": 1,
diff --git a/packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap b/packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap
index 6d1d342c..69d4494f 100644
--- a/packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap
+++ b/packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap
@@ -16,7 +16,7 @@ Object {
     "key": 2,
     "kind": "query",
     "query": Object {
-      "__key": 3044551916,
+      "__key": 3521976120,
       "definitions": Array [
         Object {
           "directives": Array [],
@@ -140,9 +140,10 @@ Object {
       ],
       "kind": "Document",
       "loc": Object {
-        "end": 76,
+        "end": 86,
         "source": Object {
-          "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }",
+          "body": "# getUser
+query getUser($name: String) { user(name: $name) { id firstName lastName } }",
           "locationOffset": Object {
             "column": 1,
             "line": 1,
@@ -185,7 +186,7 @@ Object {
     "key": 2,
     "kind": "query",
     "query": Object {
-      "__key": 3044551916,
+      "__key": 3521976120,
       "definitions": Array [
         Object {
           "directives": Array [],
@@ -309,9 +310,10 @@ Object {
       ],
       "kind": "Document",
       "loc": Object {
-        "end": 76,
+        "end": 86,
         "source": Object {
-          "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }",
+          "body": "# getUser
+query getUser($name: String) { user(name: $name) { id firstName lastName } }",
           "locationOffset": Object {
             "column": 1,
             "line": 1,
@@ -356,7 +358,7 @@ Object {
     "key": 2,
     "kind": "query",
     "query": Object {
-      "__key": 3044551916,
+      "__key": 3521976120,
       "definitions": Array [
         Object {
           "directives": Array [],
@@ -480,9 +482,10 @@ Object {
       ],
       "kind": "Document",
       "loc": Object {
-        "end": 76,
+        "end": 86,
         "source": Object {
-          "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }",
+          "body": "# getUser
+query getUser($name: String) { user(name: $name) { id firstName lastName } }",
           "locationOffset": Object {
             "column": 1,
             "line": 1,
diff --git a/packages/core/src/exchanges/__snapshots__/subscription.test.ts.snap b/packages/core/src/exchanges/__snapshots__/subscription.test.ts.snap
index 864ba6fc..3961537e 100644
--- a/packages/core/src/exchanges/__snapshots__/subscription.test.ts.snap
+++ b/packages/core/src/exchanges/__snapshots__/subscription.test.ts.snap
@@ -16,7 +16,7 @@ Object {
     "key": 4,
     "kind": "subscription",
     "query": Object {
-      "__key": 2616620656,
+      "__key": 2088253569,
       "definitions": Array [
         Object {
           "directives": Array [],
@@ -114,9 +114,10 @@ Object {
       ],
       "kind": "Document",
       "loc": Object {
-        "end": 74,
+        "end": 92,
         "source": Object {
-          "body": "subscription subscribeToUser($user: String) { user(user: $user) { name } }",
+          "body": "# subscribeToUser
+subscription subscribeToUser($user: String) { user(user: $user) { name } }",
           "locationOffset": Object {
             "column": 1,
             "line": 1,
diff --git a/packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap b/packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap
index 2dbf72ae..d69ab87a 100644
--- a/packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap
+++ b/packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap
@@ -16,7 +16,7 @@ Object {
     "key": 2,
     "kind": "query",
     "query": Object {
-      "__key": 3044551916,
+      "__key": 3521976120,
       "definitions": Array [
         Object {
           "directives": Array [],
@@ -140,9 +140,10 @@ Object {
       ],
       "kind": "Document",
       "loc": Object {
-        "end": 76,
+        "end": 86,
         "source": Object {
-          "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }",
+          "body": "# getUser
+query getUser($name: String) { user(name: $name) { id firstName lastName } }",
           "locationOffset": Object {
             "column": 1,
             "line": 1,
@@ -175,7 +176,7 @@ Object {
     "key": 2,
     "kind": "query",
     "query": Object {
-      "__key": 3044551916,
+      "__key": 3521976120,
       "definitions": Array [
         Object {
           "directives": Array [],
@@ -299,9 +300,10 @@ Object {
       ],
       "kind": "Document",
       "loc": Object {
-        "end": 76,
+        "end": 86,
         "source": Object {
-          "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }",
+          "body": "# getUser
+query getUser($name: String) { user(name: $name) { id firstName lastName } }",
           "locationOffset": Object {
             "column": 1,
             "line": 1,
@@ -334,7 +336,7 @@ Object {
     "key": 2,
     "kind": "query",
     "query": Object {
-      "__key": 3044551916,
+      "__key": 3521976120,
       "definitions": Array [
         Object {
           "directives": Array [],
@@ -458,9 +460,10 @@ Object {
       ],
       "kind": "Document",
       "loc": Object {
-        "end": 76,
+        "end": 86,
         "source": Object {
-          "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }",
+          "body": "# getUser
+query getUser($name: String) { user(name: $name) { id firstName lastName } }",
           "locationOffset": Object {
             "column": 1,
             "line": 1,
@@ -497,7 +500,7 @@ Object {
     "key": 2,
     "kind": "query",
     "query": Object {
-      "__key": 3044551916,
+      "__key": 3521976120,
       "definitions": Array [
         Object {
           "directives": Array [],
@@ -621,9 +624,10 @@ Object {
       ],
       "kind": "Document",
       "loc": Object {
-        "end": 76,
+        "end": 86,
         "source": Object {
-          "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }",
+          "body": "# getUser
+query getUser($name: String) { user(name: $name) { id firstName lastName } }",
           "locationOffset": Object {
             "column": 1,
             "line": 1,
@@ -676,7 +680,7 @@ Object {
     "key": 2,
     "kind": "query",
     "query": Object {
-      "__key": 3044551916,
+      "__key": 3521976120,
       "definitions": Array [
         Object {
           "directives": Array [],
@@ -800,9 +804,10 @@ Object {
       ],
       "kind": "Document",
       "loc": Object {
-        "end": 76,
+        "end": 86,
         "source": Object {
-          "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }",
+          "body": "# getUser
+query getUser($name: String) { user(name: $name) { id firstName lastName } }",
           "locationOffset": Object {
             "column": 1,
             "line": 1,