Skip to content

Commit

Permalink
(graphcache) - Refactor SelectionIterator and clean up exports (#1060)
Browse files Browse the repository at this point in the history
* Remove private Graphcache exports

In theory these undocumented APIs could be used to write a custom
normalized cache based on Graphcache's internal logic. However, we
forgot to expose `initDataState` anyway and additionally these APIs
were not actually in use by anyone. So for now we'll remove them.

* Simplify getFragments utility in traversal helpers

* Reduce iteration cost of SelectionIterator

- Replace object with just an iterate function
- Reduce cost of iteration by adding recursive iterators

* Add changeset
  • Loading branch information
kitten authored Oct 14, 2020
1 parent 53f748a commit 2c26cd5
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 71 deletions.
5 changes: 5 additions & 0 deletions .changeset/kind-radios-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@urql/exchange-graphcache': patch
---

Changes some internals of how selections are iterated over and remove some private exports. This will have no effect or fixes on how Graphcache functions, but may improve some minor performance characteristics of large queries.
21 changes: 11 additions & 10 deletions exchanges/graphcache/src/ast/traversal.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {
SelectionNode,
DefinitionNode,
DocumentNode,
FragmentDefinitionNode,
OperationDefinitionNode,
valueFromASTUntyped,
Kind,
Expand All @@ -13,9 +11,6 @@ import { getName } from './node';
import { invariant } from '../helpers/help';
import { Fragments, Variables } from '../types';

const isFragmentNode = (node: DefinitionNode): node is FragmentDefinitionNode =>
node.kind === Kind.FRAGMENT_DEFINITION;

/** Returns the main operation's definition */
export const getMainOperation = (
doc: DocumentNode
Expand All @@ -35,11 +30,17 @@ export const getMainOperation = (
};

/** Returns a mapping from fragment names to their selections */
export const getFragments = (doc: DocumentNode): Fragments =>
doc.definitions.filter(isFragmentNode).reduce((map: Fragments, node) => {
map[getName(node)] = node;
return map;
}, {});
export const getFragments = (doc: DocumentNode): Fragments => {
const fragments: Fragments = {};
for (let i = 0; i < doc.definitions.length; i++) {
const node = doc.definitions[i];
if (node.kind === Kind.FRAGMENT_DEFINITION) {
fragments[getName(node)] = node;
}
}

return fragments;
};

export const shouldInclude = (
node: SelectionNode,
Expand Down
4 changes: 2 additions & 2 deletions exchanges/graphcache/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from './types';
export { query, write, writeOptimistic } from './operations';
export { Store, noopDataState, reserveLayer } from './store';
export { query, write } from './operations';
export { Store } from './store';
export { cacheExchange } from './cacheExchange';
export { offlineExchange } from './offlineExchange';
8 changes: 4 additions & 4 deletions exchanges/graphcache/src/operations/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,12 @@ const readRoot = (
return originalData;
}

const iter = makeSelectionIterator(entityKey, entityKey, select, ctx);
const iterate = makeSelectionIterator(entityKey, entityKey, select, ctx);
const data = {} as Data;
data.__typename = originalData.__typename;

let node: FieldNode | void;
while ((node = iter.next()) !== undefined) {
while ((node = iterate()) !== undefined) {
const fieldAlias = getFieldAlias(node);
const fieldValue = originalData[fieldAlias];
if (node.selectionSet !== undefined && fieldValue !== null) {
Expand Down Expand Up @@ -274,12 +274,12 @@ const readSelection = (
// The following closely mirrors readSelection, but differs only slightly for the
// sake of resolving from an existing resolver result
data.__typename = typename;
const iter = makeSelectionIterator(typename, entityKey, select, ctx);
const iterate = makeSelectionIterator(typename, entityKey, select, ctx);

let node: FieldNode | void;
let hasFields = false;
let hasPartials = false;
while ((node = iter.next()) !== undefined) {
while ((node = iterate()) !== undefined) {
// Derive the needed data from our node.
const fieldName = getName(node);
const fieldArgs = getFieldArguments(node, ctx.variables);
Expand Down
107 changes: 54 additions & 53 deletions exchanges/graphcache/src/operations/shared.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InlineFragmentNode, FragmentDefinitionNode } from 'graphql';
import { FieldNode, InlineFragmentNode, FragmentDefinitionNode } from 'graphql';

import {
isInlineFragment,
Expand Down Expand Up @@ -90,68 +90,69 @@ const isFragmentHeuristicallyMatching = (
});
};

interface SelectionIterator {
(): FieldNode | undefined;
}

export const makeSelectionIterator = (
typename: void | string,
entityKey: string,
select: SelectionSet,
ctx: Context
) => {
const indexStack: number[] = [0];
const selectionStack: SelectionSet[] = [select];

return {
next() {
while (indexStack.length !== 0) {
const index = indexStack[indexStack.length - 1]++;
const select = selectionStack[selectionStack.length - 1];
if (index >= select.length) {
indexStack.pop();
selectionStack.pop();
if (process.env.NODE_ENV !== 'production') {
popDebugNode();
}
continue;
} else {
const node = select[index];
if (!shouldInclude(node, ctx.variables)) {
continue;
} else if (!isFieldNode(node)) {
// A fragment is either referred to by FragmentSpread or inline
const fragmentNode = !isInlineFragment(node)
? ctx.fragments[getName(node)]
: node;

if (fragmentNode !== undefined) {
if (process.env.NODE_ENV !== 'production') {
pushDebugNode(typename, fragmentNode);
}

const isMatching = ctx.store.schema
? isInterfaceOfType(ctx.store.schema, fragmentNode, typename)
: isFragmentHeuristicallyMatching(
fragmentNode,
typename,
entityKey,
ctx.variables
);

if (isMatching) {
indexStack.push(0);
selectionStack.push(getSelectionSet(fragmentNode));
}
): SelectionIterator => {
let childIterator: SelectionIterator | void;
let index = 0;

return function next() {
if (childIterator !== undefined) {
const node = childIterator();
if (node !== undefined) {
return node;
}

childIterator = undefined;
if (process.env.NODE_ENV !== 'production') {
popDebugNode();
}
}

while (index < select.length) {
const node = select[index++];
if (!shouldInclude(node, ctx.variables)) {
continue;
} else if (!isFieldNode(node)) {
// A fragment is either referred to by FragmentSpread or inline
const fragmentNode = !isInlineFragment(node)
? ctx.fragments[getName(node)]
: node;

if (fragmentNode !== undefined) {
const isMatching = ctx.store.schema
? isInterfaceOfType(ctx.store.schema, fragmentNode, typename)
: isFragmentHeuristicallyMatching(
fragmentNode,
typename,
entityKey,
ctx.variables
);

if (isMatching) {
if (process.env.NODE_ENV !== 'production') {
pushDebugNode(typename, fragmentNode);
}

continue;
} else if (getName(node) === '__typename') {
continue;
} else {
return node;
return (childIterator = makeSelectionIterator(
typename,
entityKey,
getSelectionSet(fragmentNode),
ctx
))();
}
}
} else if (getName(node) !== '__typename') {
return node;
}

return undefined;
},
}
};
};

Expand Down
4 changes: 2 additions & 2 deletions exchanges/graphcache/src/operations/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,15 +199,15 @@ const writeSelection = (
InMemoryData.writeRecord(entityKey, '__typename', typename);
}

const iter = makeSelectionIterator(
const iterate = makeSelectionIterator(
typename,
entityKey || typename,
select,
ctx
);

let node: FieldNode | void;
while ((node = iter.next())) {
while ((node = iterate())) {
const fieldName = getName(node);
const fieldArgs = getFieldArguments(node, ctx.variables);
const fieldKey = keyOfField(fieldName, fieldArgs);
Expand Down

0 comments on commit 2c26cd5

Please sign in to comment.