Skip to content

Commit

Permalink
fix(graphcache): Fix defer field state becoming sticky and affecting …
Browse files Browse the repository at this point in the history
…future fields (#3167)
  • Loading branch information
kitten authored Apr 20, 2023
1 parent 06189b5 commit a2bb8ea
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 51 deletions.
5 changes: 5 additions & 0 deletions .changeset/real-queens-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@urql/exchange-graphcache': patch
---

Fix regression which caused `@defer` directives from becoming “sticky” and causing every subsequent cache read to be treated as if the field was deferred.
16 changes: 14 additions & 2 deletions exchanges/graphcache/src/operations/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,13 @@ const readRoot = (
return input;
}

const iterate = makeSelectionIterator(entityKey, entityKey, select, ctx);
const iterate = makeSelectionIterator(
entityKey,
entityKey,
deferRef,
select,
ctx
);

let node: FieldNode | void;
let hasChanged = InMemoryData.currentForeignData;
Expand Down Expand Up @@ -334,7 +340,13 @@ const readSelection = (
}

const resolvers = store.resolvers[typename];
const iterate = makeSelectionIterator(typename, entityKey, select, ctx);
const iterate = makeSelectionIterator(
typename,
entityKey,
deferRef,
select,
ctx
);

let hasFields = false;
let hasPartials = false;
Expand Down
249 changes: 249 additions & 0 deletions exchanges/graphcache/src/operations/shared.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import { describe, it, expect } from 'vitest';
import { TypedDocumentNode, gql } from '@urql/core';
import { FieldNode } from '@0no-co/graphql.web';

import { makeSelectionIterator, deferRef } from './shared';
import { SelectionSet } from '../ast';

const selectionOfDocument = (doc: TypedDocumentNode): SelectionSet => {
for (const definition of doc.definitions)
if (definition.kind === 'OperationDefinition')
return definition.selectionSet.selections;
return [];
};

const ctx = {} as any;

describe('makeSelectionIterator', () => {
it('emits all fields', () => {
const selection = selectionOfDocument(
gql`
{
a
b
c
}
`
);
const iterate = makeSelectionIterator(
'Query',
'Query',
false,
selection,
ctx
);
const result: FieldNode[] = [];

let node: FieldNode | void;
while ((node = iterate())) result.push(node);

expect(result).toMatchInlineSnapshot(`
[
{
"alias": undefined,
"arguments": [],
"directives": [],
"kind": "Field",
"name": {
"kind": "Name",
"value": "a",
},
"selectionSet": undefined,
},
{
"alias": undefined,
"arguments": [],
"directives": [],
"kind": "Field",
"name": {
"kind": "Name",
"value": "b",
},
"selectionSet": undefined,
},
{
"alias": undefined,
"arguments": [],
"directives": [],
"kind": "Field",
"name": {
"kind": "Name",
"value": "c",
},
"selectionSet": undefined,
},
]
`);
});

it('skips fields that are skipped or not included', () => {
const selection = selectionOfDocument(gql`
{
a @skip(if: true)
b @include(if: false)
}
`);

const iterate = makeSelectionIterator(
'Query',
'Query',
false,
selection,
ctx
);
const result: FieldNode[] = [];

let node: FieldNode | void;
while ((node = iterate())) result.push(node);

expect(result).toMatchInlineSnapshot('[]');
});

it('processes fragments', () => {
const selection = selectionOfDocument(gql`
{
a
... {
b
}
... {
... {
c
}
}
}
`);

const iterate = makeSelectionIterator(
'Query',
'Query',
false,
selection,
ctx
);
const result: FieldNode[] = [];

let node: FieldNode | void;
while ((node = iterate())) result.push(node);

expect(result).toMatchInlineSnapshot(`
[
{
"alias": undefined,
"arguments": [],
"directives": [],
"kind": "Field",
"name": {
"kind": "Name",
"value": "a",
},
"selectionSet": undefined,
},
{
"alias": undefined,
"arguments": [],
"directives": [],
"kind": "Field",
"name": {
"kind": "Name",
"value": "b",
},
"selectionSet": undefined,
},
{
"alias": undefined,
"arguments": [],
"directives": [],
"kind": "Field",
"name": {
"kind": "Name",
"value": "c",
},
"selectionSet": undefined,
},
]
`);
});

it('updates deferred state as needed', () => {
const selection = selectionOfDocument(gql`
{
a
... @defer {
b
}
... {
... @defer {
c
}
}
... {
... {
d
}
}
... @defer {
... {
e
}
}
... {
... {
f
}
}
... {
g
}
h
}
`);

const iterate = makeSelectionIterator(
'Query',
'Query',
false,
selection,
ctx
);

const deferred: boolean[] = [];
while (iterate()) deferred.push(deferRef);
expect(deferred).toEqual([
false, // a
true, // b
true, // c
false, // d
true, // e
false, // f
false, // g
false, // h
]);
});

it('applies the parent’s defer state if needed', () => {
const selection = selectionOfDocument(gql`
{
a
... @defer {
b
}
... {
c
}
}
`);

const iterate = makeSelectionIterator(
'Query',
'Query',
true,
selection,
ctx
);

const deferred: boolean[] = [];
while (iterate()) deferred.push(deferRef);
expect(deferred).toEqual([true, true, true]);
});
});
92 changes: 43 additions & 49 deletions exchanges/graphcache/src/operations/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,66 +160,60 @@ interface SelectionIterator {
export const makeSelectionIterator = (
typename: void | string,
entityKey: string,
select: SelectionSet,
defer: boolean,
selectionSet: SelectionSet,
ctx: Context
): SelectionIterator => {
let childDeferred = false;
let childIterator: SelectionIterator | void;
let child: SelectionIterator | void;
let index = 0;

return function next() {
if (!deferRef && childDeferred) deferRef = childDeferred;

if (childIterator) {
const node = childIterator();
if (node != null) {
return node;
}

childIterator = undefined;
childDeferred = false;
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,
let node: FieldNode | undefined;
while (child || index < selectionSet.length) {
node = undefined;
deferRef = defer;
if (child) {
if ((node = child())) {
return node;
} else {
child = undefined;
if (process.env.NODE_ENV !== 'production') popDebugNode();
}
} else {
const select = selectionSet[index++];
if (!shouldInclude(select, ctx.variables)) {
/*noop*/
} else if (!isFieldNode(select)) {
// A fragment is either referred to by FragmentSpread or inline
const fragment = !isInlineFragment(select)
? ctx.fragments[getName(select)]
: select;
if (fragment) {
const isMatching =
!fragment.typeCondition ||
(ctx.store.schema
? isInterfaceOfType(ctx.store.schema, fragment, typename)
: isFragmentHeuristicallyMatching(
fragment,
typename,
entityKey,
ctx.variables
));
if (isMatching) {
if (process.env.NODE_ENV !== 'production')
pushDebugNode(typename, fragment);
child = makeSelectionIterator(
typename,
entityKey,
ctx.variables
defer || isDeferred(select, ctx.variables),
getSelectionSet(fragment),
ctx
);
if (isMatching) {
if (process.env.NODE_ENV !== 'production') {
pushDebugNode(typename, fragmentNode);
}

childDeferred = !!isDeferred(node, ctx.variables);
if (!deferRef && childDeferred) deferRef = childDeferred;

return (childIterator = makeSelectionIterator(
typename,
entityKey,
getSelectionSet(fragmentNode)!,
ctx
))();
}
} else {
return select;
}
} else {
return node;
}
}
};
Expand Down
Loading

0 comments on commit a2bb8ea

Please sign in to comment.