Repository: blitz-js/superjson (ts)

Issue:
referential equalities are unnecessarily noisy
given object

```ts
const obj = {
  children: [],
}
const input = {
  a: obj,
  b: obj,
};
```

It's key to say that path `b === a`, but it's redundant to say that `a.children === b.children`. This is currently the case


The above currently gives `refentialEqualities` of: 

```json
{
  "a": [
    "b"
  ],
  "a.children": [
    "b.children"
  ]
}
```

Causes big complex payloads to be reallllyyyy noisy.

Pull request description:
fix: referential equalities should ignore child nodes
Closes #245


- Short-circuit result of the walker on already-seen objects
- Will make outputs less noisy
- Should make superjson faster too

---

```sh
yarn test --watch --testNamePattern "#245" --testPathPattern index
```



Patch:
diff --git a/src/plainer.ts b/src/plainer.ts
index a762056..4b4cc42 100644
--- a/src/plainer.ts
+++ b/src/plainer.ts
@@ -155,27 +155,40 @@ export const walker = (
   identities: Map<any, any[][]>,
   superJson: SuperJSON,
   path: any[] = [],
-  objectsInThisPath: any[] = []
+  objectsInThisPath: any[] = [],
+  seenObjects = new Map<unknown, Result>()
 ): Result => {
-  if (!isPrimitive(object)) {
+  const primitive = isPrimitive(object);
+
+  if (!primitive) {
     addIdentity(object, path, identities);
+
+    const seen = seenObjects.get(object);
+    if (seen) {
+      // short-circuit result if we've seen this object before
+      return seen;
+    }
   }
 
   if (!isDeep(object, superJson)) {
     const transformed = transformValue(object, superJson);
-    if (transformed) {
-      return {
-        transformedValue: transformed.value,
-        annotations: [transformed.type],
-      };
-    } else {
-      return {
-        transformedValue: object,
-      };
+
+    const result: Result = transformed
+      ? {
+          transformedValue: transformed.value,
+          annotations: [transformed.type],
+        }
+      : {
+          transformedValue: object,
+        };
+    if (!primitive) {
+      seenObjects.set(object, result);
     }
+    return result;
   }
 
   if (includes(objectsInThisPath, object)) {
+    // prevent circular references
     return {
       transformedValue: null,
     };
@@ -184,10 +197,6 @@ export const walker = (
   const transformationResult = transformValue(object, superJson);
   const transformed = transformationResult?.value ?? object;
 
-  if (!isPrimitive(object)) {
-    objectsInThisPath = [...objectsInThisPath, object];
-  }
-
   const transformedValue: any = isArray(transformed) ? [] : {};
   const innerAnnotations: Record<string, Tree<TypeAnnotation>> = {};
 
@@ -197,7 +206,8 @@ export const walker = (
       identities,
       superJson,
       [...path, index],
-      objectsInThisPath
+      [...objectsInThisPath, object],
+      seenObjects
     );
 
     transformedValue[index] = recursiveResult.transformedValue;
@@ -211,19 +221,22 @@ export const walker = (
     }
   });
 
-  if (isEmptyObject(innerAnnotations)) {
-    return {
-      transformedValue,
-      annotations: !!transformationResult
-        ? [transformationResult.type]
-        : undefined,
-    };
-  } else {
-    return {
-      transformedValue,
-      annotations: !!transformationResult
-        ? [transformationResult.type, innerAnnotations]
-        : innerAnnotations,
-    };
+  const result: Result = isEmptyObject(innerAnnotations)
+    ? {
+        transformedValue,
+        annotations: !!transformationResult
+          ? [transformationResult.type]
+          : undefined,
+      }
+    : {
+        transformedValue,
+        annotations: !!transformationResult
+          ? [transformationResult.type, innerAnnotations]
+          : innerAnnotations,
+      };
+  if (!primitive) {
+    seenObjects.set(object, result);
   }
+
+  return result;
 };


Test patch:
diff --git a/src/index.test.ts b/src/index.test.ts
index 14e21e2..fa5341f 100644
--- a/src/index.test.ts
+++ b/src/index.test.ts
@@ -1126,3 +1126,34 @@ test('superjson instances are independent of one another', () => {
   const res2 = s2.serialize(value);
   expect(res2.json).toEqual(value);
 });
+
+test('regression #245: superjson referential equalities only use the top-most parent node', () => {
+  type Node = {
+    children: Node[];
+  };
+  const root: Node = {
+    children: [],
+  };
+  const input = {
+    a: root,
+    b: root,
+  };
+  const res = SuperJSON.serialize(input);
+
+  expect(res.meta?.referentialEqualities).toHaveProperty(['a']);
+
+  // saying that a.children is equal to b.children is redundant since its already know that a === b
+  expect(res.meta?.referentialEqualities).not.toHaveProperty(['a.children']);
+  expect(res.meta).toMatchInlineSnapshot(`
+    Object {
+      "referentialEqualities": Object {
+        "a": Array [
+          "b",
+        ],
+      },
+    }
+  `);
+
+  const parsed = SuperJSON.deserialize(res);
+  expect(parsed).toEqual(input);
+});