Repository: FormidableLabs/urql (ts)

Issue:
mutations with different files in parallel, results lost
codesandbox links:
server: https://codesandbox.io/s/graphql-server-1-l6ry3
client: https://codesandbox.io/s/graphql-client-1-dtnkx

i have a schema with a single mutation:
```
type Mutation {
   createMessage:String!
```
the server always returns a random string (it contains a random uuid).

then i have an urql-client, where if you press a button,
i execute two `createMessage` mutations at the same time,
and display their result. 

expected behavior:
- i get two different messages

actual behavior:
- i get the same message two times

i looked into the code,and what seems to be happening is this:
- two ajax-requests are sent out (the two mutations), and both ajax-requests return with different messages. so far so good
- but, both ajax-requests contain the same operation-key, so (it seems) as soon as one finishes, URQL finishes both mutations with the same result.
- i tried to use a `fetchPolicy: 'network-only'`, but that did not help
- it seems the operation-key is a hash from the mutation and variables. so, when the mutation and variables are the same, the operation-key is the same.

am i doing something wrong here? for  me it seems mutations should not be deduplicated this way. 

i understand that i can just send some mutation-variable that always changes and it willl solve the problem, but that seems ugly :(

p.s: this is just a minimal example, the place where i found the issue was with using the new multipart-form-data-exchange, i was  uploading pictures, and only the picture changed in the mutations. i assume the key-hash does not take the separately traveling blobs into account.

Pull request description:
(core) - Fix stringifyVariables (and thus operation keys) for Files
Resolve #649

## Summary

When two operations with a `File` in the variables are sent the operation key would likely be the same since we never take any `File` attributes into account.

We'd hence like to have special handling for objects without any enumerable keys that also aren't plain objects.

## Set of changes

- Add random (but stable) keying for objects without enumerable keys that aren't plain objects


Patch:
diff --git a/.changeset/wicked-cows-heal.md b/.changeset/wicked-cows-heal.md
new file mode 100644
index 00000000..9f4e6190
--- /dev/null
+++ b/.changeset/wicked-cows-heal.md
@@ -0,0 +1,5 @@
+---
+'@urql/core': patch
+---
+
+Add support for variables that contain non-plain objects without any enumerable keys, e.g. `File` or `Blob`. In this case `stringifyVariables` will now use a stable (but random) key, which means that mutations containing `File`s — or other objects like this — will now be distinct, as they should be.
diff --git a/packages/core/src/utils/stringifyVariables.ts b/packages/core/src/utils/stringifyVariables.ts
index e8a2f3f1..2ddab6c4 100644
--- a/packages/core/src/utils/stringifyVariables.ts
+++ b/packages/core/src/utils/stringifyVariables.ts
@@ -1,4 +1,5 @@
 const seen = new Set();
+const cache = new WeakMap();
 
 const stringify = (x: any): string => {
   if (x === undefined) {
@@ -29,6 +30,15 @@ const stringify = (x: any): string => {
   }
 
   const keys = Object.keys(x).sort();
+  if (!keys.length && x.constructor && x.constructor !== Object) {
+    const key =
+      cache.get(x) ||
+      Math.random()
+        .toString(36)
+        .slice(2);
+    cache.set(x, key);
+    return `{"__key":"${key}"}`;
+  }
 
   seen.add(x);
   out = '{';


Test patch:
diff --git a/packages/core/src/utils/stringifyVariables.test.ts b/packages/core/src/utils/stringifyVariables.test.ts
index 784ea552..3489fb93 100644
--- a/packages/core/src/utils/stringifyVariables.test.ts
+++ b/packages/core/src/utils/stringifyVariables.test.ts
@@ -29,7 +29,22 @@ it('throws for circular structures', () => {
   }).toThrow();
 });
 
-it('stringifies date correctly', () => {
+it('stringifies dates correctly', () => {
   const date = new Date('2019-12-11T04:20:00');
   expect(stringifyVariables(date)).toBe(date.toJSON());
 });
+
+it('stringifies dictionaries (Object.create(null)) correctly', () => {
+  expect(stringifyVariables(Object.create(null))).toBe('{}');
+});
+
+it('stringifies files correctly', () => {
+  const file = new File([0] as any, 'test.js');
+  Object.defineProperty(file, 'lastModified', { value: 123 });
+  const str = stringifyVariables(file);
+  expect(str).toBe(stringifyVariables(file));
+
+  const otherFile = new File([0] as any, 'otherFile.js');
+  Object.defineProperty(otherFile, 'lastModified', { value: 234 });
+  expect(str).not.toBe(stringifyVariables(otherFile));
+});