Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(graphcache): Fix defer field state becoming sticky and affecting future fields (#3167)

authored by

Phil Pluckthun and committed by
GitHub
a2bb8eac 06189b5b

+312 -51
+5
.changeset/real-queens-crash.md
··· 1 + --- 2 + '@urql/exchange-graphcache': patch 3 + --- 4 + 5 + Fix regression which caused `@defer` directives from becoming “sticky” and causing every subsequent cache read to be treated as if the field was deferred.
+14 -2
exchanges/graphcache/src/operations/query.ts
··· 137 137 return input; 138 138 } 139 139 140 - const iterate = makeSelectionIterator(entityKey, entityKey, select, ctx); 140 + const iterate = makeSelectionIterator( 141 + entityKey, 142 + entityKey, 143 + deferRef, 144 + select, 145 + ctx 146 + ); 141 147 142 148 let node: FieldNode | void; 143 149 let hasChanged = InMemoryData.currentForeignData; ··· 334 340 } 335 341 336 342 const resolvers = store.resolvers[typename]; 337 - const iterate = makeSelectionIterator(typename, entityKey, select, ctx); 343 + const iterate = makeSelectionIterator( 344 + typename, 345 + entityKey, 346 + deferRef, 347 + select, 348 + ctx 349 + ); 338 350 339 351 let hasFields = false; 340 352 let hasPartials = false;
+249
exchanges/graphcache/src/operations/shared.test.ts
··· 1 + import { describe, it, expect } from 'vitest'; 2 + import { TypedDocumentNode, gql } from '@urql/core'; 3 + import { FieldNode } from '@0no-co/graphql.web'; 4 + 5 + import { makeSelectionIterator, deferRef } from './shared'; 6 + import { SelectionSet } from '../ast'; 7 + 8 + const selectionOfDocument = (doc: TypedDocumentNode): SelectionSet => { 9 + for (const definition of doc.definitions) 10 + if (definition.kind === 'OperationDefinition') 11 + return definition.selectionSet.selections; 12 + return []; 13 + }; 14 + 15 + const ctx = {} as any; 16 + 17 + describe('makeSelectionIterator', () => { 18 + it('emits all fields', () => { 19 + const selection = selectionOfDocument( 20 + gql` 21 + { 22 + a 23 + b 24 + c 25 + } 26 + ` 27 + ); 28 + const iterate = makeSelectionIterator( 29 + 'Query', 30 + 'Query', 31 + false, 32 + selection, 33 + ctx 34 + ); 35 + const result: FieldNode[] = []; 36 + 37 + let node: FieldNode | void; 38 + while ((node = iterate())) result.push(node); 39 + 40 + expect(result).toMatchInlineSnapshot(` 41 + [ 42 + { 43 + "alias": undefined, 44 + "arguments": [], 45 + "directives": [], 46 + "kind": "Field", 47 + "name": { 48 + "kind": "Name", 49 + "value": "a", 50 + }, 51 + "selectionSet": undefined, 52 + }, 53 + { 54 + "alias": undefined, 55 + "arguments": [], 56 + "directives": [], 57 + "kind": "Field", 58 + "name": { 59 + "kind": "Name", 60 + "value": "b", 61 + }, 62 + "selectionSet": undefined, 63 + }, 64 + { 65 + "alias": undefined, 66 + "arguments": [], 67 + "directives": [], 68 + "kind": "Field", 69 + "name": { 70 + "kind": "Name", 71 + "value": "c", 72 + }, 73 + "selectionSet": undefined, 74 + }, 75 + ] 76 + `); 77 + }); 78 + 79 + it('skips fields that are skipped or not included', () => { 80 + const selection = selectionOfDocument(gql` 81 + { 82 + a @skip(if: true) 83 + b @include(if: false) 84 + } 85 + `); 86 + 87 + const iterate = makeSelectionIterator( 88 + 'Query', 89 + 'Query', 90 + false, 91 + selection, 92 + ctx 93 + ); 94 + const result: FieldNode[] = []; 95 + 96 + let node: FieldNode | void; 97 + while ((node = iterate())) result.push(node); 98 + 99 + expect(result).toMatchInlineSnapshot('[]'); 100 + }); 101 + 102 + it('processes fragments', () => { 103 + const selection = selectionOfDocument(gql` 104 + { 105 + a 106 + ... { 107 + b 108 + } 109 + ... { 110 + ... { 111 + c 112 + } 113 + } 114 + } 115 + `); 116 + 117 + const iterate = makeSelectionIterator( 118 + 'Query', 119 + 'Query', 120 + false, 121 + selection, 122 + ctx 123 + ); 124 + const result: FieldNode[] = []; 125 + 126 + let node: FieldNode | void; 127 + while ((node = iterate())) result.push(node); 128 + 129 + expect(result).toMatchInlineSnapshot(` 130 + [ 131 + { 132 + "alias": undefined, 133 + "arguments": [], 134 + "directives": [], 135 + "kind": "Field", 136 + "name": { 137 + "kind": "Name", 138 + "value": "a", 139 + }, 140 + "selectionSet": undefined, 141 + }, 142 + { 143 + "alias": undefined, 144 + "arguments": [], 145 + "directives": [], 146 + "kind": "Field", 147 + "name": { 148 + "kind": "Name", 149 + "value": "b", 150 + }, 151 + "selectionSet": undefined, 152 + }, 153 + { 154 + "alias": undefined, 155 + "arguments": [], 156 + "directives": [], 157 + "kind": "Field", 158 + "name": { 159 + "kind": "Name", 160 + "value": "c", 161 + }, 162 + "selectionSet": undefined, 163 + }, 164 + ] 165 + `); 166 + }); 167 + 168 + it('updates deferred state as needed', () => { 169 + const selection = selectionOfDocument(gql` 170 + { 171 + a 172 + ... @defer { 173 + b 174 + } 175 + ... { 176 + ... @defer { 177 + c 178 + } 179 + } 180 + ... { 181 + ... { 182 + d 183 + } 184 + } 185 + ... @defer { 186 + ... { 187 + e 188 + } 189 + } 190 + ... { 191 + ... { 192 + f 193 + } 194 + } 195 + ... { 196 + g 197 + } 198 + h 199 + } 200 + `); 201 + 202 + const iterate = makeSelectionIterator( 203 + 'Query', 204 + 'Query', 205 + false, 206 + selection, 207 + ctx 208 + ); 209 + 210 + const deferred: boolean[] = []; 211 + while (iterate()) deferred.push(deferRef); 212 + expect(deferred).toEqual([ 213 + false, // a 214 + true, // b 215 + true, // c 216 + false, // d 217 + true, // e 218 + false, // f 219 + false, // g 220 + false, // h 221 + ]); 222 + }); 223 + 224 + it('applies the parent’s defer state if needed', () => { 225 + const selection = selectionOfDocument(gql` 226 + { 227 + a 228 + ... @defer { 229 + b 230 + } 231 + ... { 232 + c 233 + } 234 + } 235 + `); 236 + 237 + const iterate = makeSelectionIterator( 238 + 'Query', 239 + 'Query', 240 + true, 241 + selection, 242 + ctx 243 + ); 244 + 245 + const deferred: boolean[] = []; 246 + while (iterate()) deferred.push(deferRef); 247 + expect(deferred).toEqual([true, true, true]); 248 + }); 249 + });
+43 -49
exchanges/graphcache/src/operations/shared.ts
··· 160 160 export const makeSelectionIterator = ( 161 161 typename: void | string, 162 162 entityKey: string, 163 - select: SelectionSet, 163 + defer: boolean, 164 + selectionSet: SelectionSet, 164 165 ctx: Context 165 166 ): SelectionIterator => { 166 - let childDeferred = false; 167 - let childIterator: SelectionIterator | void; 167 + let child: SelectionIterator | void; 168 168 let index = 0; 169 169 170 170 return function next() { 171 - if (!deferRef && childDeferred) deferRef = childDeferred; 172 - 173 - if (childIterator) { 174 - const node = childIterator(); 175 - if (node != null) { 176 - return node; 177 - } 178 - 179 - childIterator = undefined; 180 - childDeferred = false; 181 - if (process.env.NODE_ENV !== 'production') { 182 - popDebugNode(); 183 - } 184 - } 185 - 186 - while (index < select.length) { 187 - const node = select[index++]; 188 - if (!shouldInclude(node, ctx.variables)) { 189 - continue; 190 - } else if (!isFieldNode(node)) { 191 - // A fragment is either referred to by FragmentSpread or inline 192 - const fragmentNode = !isInlineFragment(node) 193 - ? ctx.fragments[getName(node)] 194 - : node; 195 - 196 - if (fragmentNode !== undefined) { 197 - const isMatching = ctx.store.schema 198 - ? isInterfaceOfType(ctx.store.schema, fragmentNode, typename) 199 - : isFragmentHeuristicallyMatching( 200 - fragmentNode, 171 + let node: FieldNode | undefined; 172 + while (child || index < selectionSet.length) { 173 + node = undefined; 174 + deferRef = defer; 175 + if (child) { 176 + if ((node = child())) { 177 + return node; 178 + } else { 179 + child = undefined; 180 + if (process.env.NODE_ENV !== 'production') popDebugNode(); 181 + } 182 + } else { 183 + const select = selectionSet[index++]; 184 + if (!shouldInclude(select, ctx.variables)) { 185 + /*noop*/ 186 + } else if (!isFieldNode(select)) { 187 + // A fragment is either referred to by FragmentSpread or inline 188 + const fragment = !isInlineFragment(select) 189 + ? ctx.fragments[getName(select)] 190 + : select; 191 + if (fragment) { 192 + const isMatching = 193 + !fragment.typeCondition || 194 + (ctx.store.schema 195 + ? isInterfaceOfType(ctx.store.schema, fragment, typename) 196 + : isFragmentHeuristicallyMatching( 197 + fragment, 198 + typename, 199 + entityKey, 200 + ctx.variables 201 + )); 202 + if (isMatching) { 203 + if (process.env.NODE_ENV !== 'production') 204 + pushDebugNode(typename, fragment); 205 + child = makeSelectionIterator( 201 206 typename, 202 207 entityKey, 203 - ctx.variables 208 + defer || isDeferred(select, ctx.variables), 209 + getSelectionSet(fragment), 210 + ctx 204 211 ); 205 - if (isMatching) { 206 - if (process.env.NODE_ENV !== 'production') { 207 - pushDebugNode(typename, fragmentNode); 208 212 } 209 - 210 - childDeferred = !!isDeferred(node, ctx.variables); 211 - if (!deferRef && childDeferred) deferRef = childDeferred; 212 - 213 - return (childIterator = makeSelectionIterator( 214 - typename, 215 - entityKey, 216 - getSelectionSet(fragmentNode)!, 217 - ctx 218 - ))(); 219 213 } 214 + } else { 215 + return select; 220 216 } 221 - } else { 222 - return node; 223 217 } 224 218 } 225 219 };
+1
exchanges/graphcache/src/operations/write.ts
··· 226 226 const iterate = makeSelectionIterator( 227 227 typename, 228 228 entityKey || typename, 229 + deferRef, 229 230 select, 230 231 ctx 231 232 );