Repository: blitz-js/superjson
Pull request #247 (2023-07-14 14:24:31)

feat: add `dedupe` flag
add dedupe flag that will make sure that complex objects only appear once in the output

KATT: You decide everything and me / @juliusmarminge will just follow suit. :)

Feel free to just push to my PR or make another stab at it, I gotta go for today now! 
KATT: FWIW, changing the circular to anything else but `null` could be seen as a breaking change - so maybe it's better to have _both_ as `null` for now

Patch:
diff --git a/src/index.ts b/src/index.ts
index 5103c66..320caa8 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,4 @@
-import { SuperJSONResult, SuperJSONValue, Class, JSONValue } from './types';
+import { Class, JSONValue, SuperJSONResult, SuperJSONValue } from './types';
 import { ClassRegistry, RegisterOptions } from './class-registry';
 import { Registry } from './registry';
 import {
@@ -6,17 +6,33 @@ import {
   CustomTransformerRegistry,
 } from './custom-transformer-registry';
 import {
-  walker,
   applyReferentialEqualityAnnotations,
   applyValueAnnotations,
   generateReferentialEqualityAnnotations,
+  walker,
 } from './plainer';
 import { copy } from 'copy-anything';
 
 export default class SuperJSON {
+  /**
+   * If true, SuperJSON will make sure only one instance of referentially equal objects are serialized and the rest are replaced with `null`.
+   */
+  private readonly dedupe: boolean;
+
+  /**
+   * @param dedupeReferentialEqualities  If true, SuperJSON will make sure only one instance of referentially equal objects are serialized and the rest are replaced with `null`.
+   */
+  constructor({
+    dedupe = false,
+  }: {
+    dedupe?: boolean;
+  } = {}) {
+    this.dedupe = dedupe;
+  }
+
   serialize(object: SuperJSONValue): SuperJSONResult {
     const identities = new Map<any, any[][]>();
-    const output = walker(object, identities, this);
+    const output = walker(object, identities, this, this.dedupe);
     const res: SuperJSONResult = {
       json: output.transformedValue,
     };
diff --git a/src/plainer.spec.ts b/src/plainer.spec.ts
index be11cd3..47db3b2 100644
--- a/src/plainer.spec.ts
+++ b/src/plainer.spec.ts
@@ -9,7 +9,8 @@ test('walker', () => {
         b: /test/g,
       },
       new Map(),
-      new SuperJSON()
+      new SuperJSON(),
+      false
     )
   ).toEqual({
     transformedValue: {
diff --git a/src/plainer.ts b/src/plainer.ts
index 4b4cc42..1a568df 100644
--- a/src/plainer.ts
+++ b/src/plainer.ts
@@ -154,6 +154,7 @@ export const walker = (
   object: any,
   identities: Map<any, any[][]>,
   superJson: SuperJSON,
+  dedupe: boolean,
   path: any[] = [],
   objectsInThisPath: any[] = [],
   seenObjects = new Map<unknown, Result>()
@@ -166,7 +167,11 @@ export const walker = (
     const seen = seenObjects.get(object);
     if (seen) {
       // short-circuit result if we've seen this object before
-      return seen;
+      return dedupe
+        ? {
+            transformedValue: null,
+          }
+        : seen;
     }
   }
 
@@ -205,6 +210,7 @@ export const walker = (
       value,
       identities,
       superJson,
+      dedupe,
       [...path, index],
       [...objectsInThisPath, object],
       seenObjects


Test patch:
diff --git a/src/index.test.ts b/src/index.test.ts
index fa5341f..566d80f 100644
--- a/src/index.test.ts
+++ b/src/index.test.ts
@@ -1157,3 +1157,39 @@ test('regression #245: superjson referential equalities only use the top-most pa
   const parsed = SuperJSON.deserialize(res);
   expect(parsed).toEqual(input);
 });
+
+test('dedupe=true', () => {
+  const instance = new SuperJSON({
+    dedupe: true,
+  });
+
+  type Node = {
+    children: Node[];
+  };
+  const root: Node = {
+    children: [],
+  };
+  const input = {
+    a: root,
+    b: root,
+  };
+  const output = instance.serialize(input);
+
+  const json = output.json as any;
+
+  expect(json.a);
+
+  // This has already been seen and should be deduped
+  expect(json.b).toBeNull();
+
+  expect(json).toMatchInlineSnapshot(`
+    Object {
+      "a": Object {
+        "children": Array [],
+      },
+      "b": null,
+    }
+  `);
+
+  expect(instance.deserialize(output)).toEqual(input);
+});