Repository: graphql/graphql-js
Pull request #3568 (2022-05-10 16:16:18)

Refactor out `AccumulatorMap` from `groupBy`

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *compassionate-pike-271cb3* processing.


|  Name | Link |
|---------------------------------|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 334ad59e860351d2381c0ec0dd1819722874d314 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/compassionate-pike-271cb3/deploys/627a8fd33bed4a00081666e8 |
github-actions[bot]: Hi @IvanGoncharov, I'm @github-actions bot happy to help you with this PR 👋

<details>
<summary> Supported commands </summary>

Please post this commands in separate comments and only one per comment:
* `@github-actions run-benchmark` - Run benchmark comparing base and merge commits for this PR
* `@github-actions publish-pr-on-npm` - Build package from this PR and publish it on NPM
</details>


Patch:
diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts
index d0961bfa..83a78051 100644
--- a/src/execution/collectFields.ts
+++ b/src/execution/collectFields.ts
@@ -1,3 +1,4 @@
+import { AccumulatorMap } from '../jsutils/AccumulatorMap';
 import type { ObjMap } from '../jsutils/ObjMap';
 
 import type {
@@ -37,7 +38,7 @@ export function collectFields(
   runtimeType: GraphQLObjectType,
   selectionSet: SelectionSetNode,
 ): Map<string, ReadonlyArray<FieldNode>> {
-  const fields = new Map();
+  const fields = new AccumulatorMap<string, FieldNode>();
   collectFieldsImpl(
     schema,
     fragments,
@@ -67,7 +68,7 @@ export function collectSubfields(
   returnType: GraphQLObjectType,
   fieldNodes: ReadonlyArray<FieldNode>,
 ): Map<string, ReadonlyArray<FieldNode>> {
-  const subFieldNodes = new Map();
+  const subFieldNodes = new AccumulatorMap<string, FieldNode>();
   const visitedFragmentNames = new Set<string>();
   for (const node of fieldNodes) {
     if (node.selectionSet) {
@@ -91,7 +92,7 @@ function collectFieldsImpl(
   variableValues: { [variable: string]: unknown },
   runtimeType: GraphQLObjectType,
   selectionSet: SelectionSetNode,
-  fields: Map<string, Array<FieldNode>>,
+  fields: AccumulatorMap<string, FieldNode>,
   visitedFragmentNames: Set<string>,
 ): void {
   for (const selection of selectionSet.selections) {
@@ -100,13 +101,7 @@ function collectFieldsImpl(
         if (!shouldIncludeNode(variableValues, selection)) {
           continue;
         }
-        const name = getFieldEntryKey(selection);
-        const fieldList = fields.get(name);
-        if (fieldList !== undefined) {
-          fieldList.push(selection);
-        } else {
-          fields.set(name, [selection]);
-        }
+        fields.add(getFieldEntryKey(selection), selection);
         break;
       }
       case Kind.INLINE_FRAGMENT: {
diff --git a/src/jsutils/AccumulatorMap.ts b/src/jsutils/AccumulatorMap.ts
new file mode 100644
index 00000000..156fe71c
--- /dev/null
+++ b/src/jsutils/AccumulatorMap.ts
@@ -0,0 +1,17 @@
+/**
+ * ES6 Map with additional `add` method to accumulate items.
+ */
+export class AccumulatorMap<K, T> extends Map<K, Array<T>> {
+  get [Symbol.toStringTag]() {
+    return 'AccumulatorMap';
+  }
+
+  add(key: K, item: T): void {
+    const group = this.get(key);
+    if (group === undefined) {
+      this.set(key, [item]);
+    } else {
+      group.push(item);
+    }
+  }
+}
diff --git a/src/jsutils/groupBy.ts b/src/jsutils/groupBy.ts
index f3b0c076..60e02ea3 100644
--- a/src/jsutils/groupBy.ts
+++ b/src/jsutils/groupBy.ts
@@ -1,3 +1,5 @@
+import { AccumulatorMap } from './AccumulatorMap';
+
 /**
  * Groups array items into a Map, given a function to produce grouping key.
  */
@@ -5,15 +7,9 @@ export function groupBy<K, T>(
   list: ReadonlyArray<T>,
   keyFn: (item: T) => K,
 ): Map<K, ReadonlyArray<T>> {
-  const result = new Map<K, Array<T>>();
+  const result = new AccumulatorMap<K, T>();
   for (const item of list) {
-    const key = keyFn(item);
-    const group = result.get(key);
-    if (group === undefined) {
-      result.set(key, [item]);
-    } else {
-      group.push(item);
-    }
+    result.add(keyFn(item), item);
   }
   return result;
 }


Test patch:
diff --git a/src/jsutils/__tests__/AccumulatorMap-test.ts b/src/jsutils/__tests__/AccumulatorMap-test.ts
new file mode 100644
index 00000000..0fb34c64
--- /dev/null
+++ b/src/jsutils/__tests__/AccumulatorMap-test.ts
@@ -0,0 +1,36 @@
+import { expect } from 'chai';
+import { describe, it } from 'mocha';
+
+import { AccumulatorMap } from '../AccumulatorMap';
+
+function expectMap<K, V>(map: Map<K, V>) {
+  return expect(Object.fromEntries(map.entries()));
+}
+
+describe('AccumulatorMap', () => {
+  it('can be Object.toStringified', () => {
+    const accumulatorMap = new AccumulatorMap();
+
+    expect(Object.prototype.toString.call(accumulatorMap)).to.equal(
+      '[object AccumulatorMap]',
+    );
+  });
+
+  it('accumulate items', () => {
+    const accumulatorMap = new AccumulatorMap<string, number>();
+
+    expectMap(accumulatorMap).to.deep.equal({});
+
+    accumulatorMap.add('a', 1);
+    accumulatorMap.add('b', 2);
+    accumulatorMap.add('c', 3);
+    accumulatorMap.add('b', 4);
+    accumulatorMap.add('c', 5);
+    accumulatorMap.add('c', 6);
+    expectMap(accumulatorMap).to.deep.equal({
+      a: [1],
+      b: [2, 4],
+      c: [3, 5, 6],
+    });
+  });
+});