Skip to content

Commit

Permalink
memoize field node lists when collecting fields (#96)
Browse files Browse the repository at this point in the history
* memoize field node lists when collecting fields

...so that functions operating on field lists can in turn be memoized.

* add changeset

* prettier

* fix
  • Loading branch information
yaacovCR authored Nov 24, 2021
1 parent f9ce865 commit 797ee21
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 2 deletions.
7 changes: 7 additions & 0 deletions .changeset/grumpy-toys-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'graphql-executor': patch
---

Memoize field lists created by the collectFields utility function.

This allows functions that operate on these field lists to be memoized.
129 changes: 129 additions & 0 deletions src/execution/__tests__/collectFields-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';

import type { FragmentDefinitionNode, OperationDefinitionNode } from 'graphql';
import {
GraphQLID,
GraphQLList,
GraphQLObjectType,
GraphQLSchema,
GraphQLString,
parse,
} from 'graphql';

import { collectFields } from '../collectFields';

const friendType = new GraphQLObjectType({
fields: {
id: { type: GraphQLID },
name: { type: GraphQLString },
},
name: 'Friend',
});

const heroType = new GraphQLObjectType({
fields: {
id: { type: GraphQLID },
name: { type: GraphQLString },
friends: {
type: new GraphQLList(friendType),
},
},
name: 'Hero',
});

const query = new GraphQLObjectType({
fields: {
hero: {
type: heroType,
},
},
name: 'Query',
});

const schema = new GraphQLSchema({ query });

const document = parse(`
query HeroQuery($skipFirst: Boolean, $skipSecond: Boolean) {
hero {
name
}
...HeroFragment1 @skip(if: $skipFirst)
...HeroFragment2 @skip(if: $skipSecond)
}
fragment HeroFragment1 on Query {
hero {
name
}
}
fragment HeroFragment2 on Query {
hero {
name
}
}
`);

const selectionSet = (document.definitions[0] as OperationDefinitionNode)
.selectionSet;
const fragments = {
HeroFragment1: document.definitions[1] as FragmentDefinitionNode,
HeroFragment2: document.definitions[2] as FragmentDefinitionNode,
};

describe('collectFields', () => {
it('memoizes', () => {
const { fields: fields1 } = collectFields(
schema,
fragments,
{
skipFirst: false,
skipSecond: false,
},
query,
selectionSet,
);
const { fields: fields2 } = collectFields(
schema,
fragments,
{
skipFirst: false,
skipSecond: false,
},
query,
selectionSet,
);

const heroFieldNodes1 = fields1.get('hero');
const heroFieldNodes2 = fields2.get('hero');

expect(heroFieldNodes1).to.equal(heroFieldNodes2);
});

it('does not yet (?) memoize everything', () => {
const { fields: fields1 } = collectFields(
schema,
fragments,
{
skipFirst: true,
skipSecond: false,
},
query,
selectionSet,
);
const { fields: fields2 } = collectFields(
schema,
fragments,
{
skipFirst: false,
skipSecond: true,
},
query,
selectionSet,
);

const heroFieldNodes1 = fields1.get('hero');
const heroFieldNodes2 = fields2.get('hero');

expect(heroFieldNodes1).to.not.equal(heroFieldNodes2);
});
});
23 changes: 21 additions & 2 deletions src/execution/collectFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {

import type { Maybe } from '../jsutils/Maybe';
import type { ObjMap } from '../jsutils/ObjMap';
import { memoize1 } from '../jsutils/memoize1';
import { memoize2 } from '../jsutils/memoize2';

import { GraphQLDeferDirective } from '../type/directives';

Expand Down Expand Up @@ -129,9 +131,9 @@ function collectFieldsImpl(
const name = getFieldEntryKey(selection);
const fieldList = fields.get(name);
if (fieldList !== undefined) {
fieldList.push(selection);
fields.set(name, updateFieldList(fieldList, selection));
} else {
fields.set(name, [selection]);
fields.set(name, createFieldList(selection));
}
break;
}
Expand Down Expand Up @@ -310,3 +312,20 @@ function doesFragmentConditionMatch(
function getFieldEntryKey(node: FieldNode): string {
return node.alias ? node.alias.value : node.name.value;
}

/**
* Creates a field list, memoizing so that functions operating on the
* field list can be memoized.
*/
const createFieldList = memoize1((node: FieldNode): Array<FieldNode> => [node]);

/**
* Appends to a field list, memoizing so that functions operating on the
* field list can be memoized.
*/
const updateFieldList = memoize2(
(fieldList: Array<FieldNode>, node: FieldNode): Array<FieldNode> => [
...fieldList,
node,
],
);
22 changes: 22 additions & 0 deletions src/jsutils/memoize1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Memoizes the provided one-argument function.
*/
export function memoize1<A1 extends object, R>(
fn: (a1: A1) => R,
): (a1: A1) => R {
let cache0: WeakMap<A1, R>;

return function memoized(a1) {
if (cache0 === undefined) {
cache0 = new WeakMap();
}

let fnResult = cache0.get(a1);
if (fnResult === undefined) {
fnResult = fn(a1);
cache0.set(a1, fnResult);
}

return fnResult;
};
}
28 changes: 28 additions & 0 deletions src/jsutils/memoize2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Memoizes the provided two-argument function.
*/
export function memoize2<A1 extends object, A2 extends object, R>(
fn: (a1: A1, a2: A2) => R,
): (a1: A1, a2: A2) => R {
let cache0: WeakMap<A1, WeakMap<A2, R>>;

return function memoized(a1, a2) {
if (cache0 === undefined) {
cache0 = new WeakMap();
}

let cache1 = cache0.get(a1);
if (cache1 === undefined) {
cache1 = new WeakMap();
cache0.set(a1, cache1);
}

let fnResult = cache1.get(a2);
if (fnResult === undefined) {
fnResult = fn(a1, a2);
cache1.set(a2, fnResult);
}

return fnResult;
};
}

0 comments on commit 797ee21

Please sign in to comment.