diff --git a/.changeset/real-queens-crash.md b/.changeset/real-queens-crash.md new file mode 100644 index 0000000000..9f21a5bfcb --- /dev/null +++ b/.changeset/real-queens-crash.md @@ -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. diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts index 80dcb2ca14..83f3cf6900 100644 --- a/exchanges/graphcache/src/operations/query.ts +++ b/exchanges/graphcache/src/operations/query.ts @@ -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; @@ -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; diff --git a/exchanges/graphcache/src/operations/shared.test.ts b/exchanges/graphcache/src/operations/shared.test.ts new file mode 100644 index 0000000000..fdfb7111f8 --- /dev/null +++ b/exchanges/graphcache/src/operations/shared.test.ts @@ -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]); + }); +}); diff --git a/exchanges/graphcache/src/operations/shared.ts b/exchanges/graphcache/src/operations/shared.ts index 28f9dd15a7..9768195657 100644 --- a/exchanges/graphcache/src/operations/shared.ts +++ b/exchanges/graphcache/src/operations/shared.ts @@ -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; } } }; diff --git a/exchanges/graphcache/src/operations/write.ts b/exchanges/graphcache/src/operations/write.ts index e2871fca58..33b552e100 100644 --- a/exchanges/graphcache/src/operations/write.ts +++ b/exchanges/graphcache/src/operations/write.ts @@ -226,6 +226,7 @@ const writeSelection = ( const iterate = makeSelectionIterator( typename, entityKey || typename, + deferRef, select, ctx );