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): Restore `stale` being set to `true` on blocked (but in-flight) operations (#3493)

authored by

Phil Pluckthun and committed by
GitHub
1bfadffe f7b78e2f

+176 -7
+5
.changeset/giant-beans-act.md
··· 1 + --- 2 + '@urql/exchange-graphcache': patch 3 + --- 4 + 5 + Set `stale: true` on cache results, even if a reexecution has been blocked by the loop protection, if the operation is already pending and in-flight.
+2 -1
exchanges/graphcache/e2e-tests/query.spec.tsx
··· 31 31 `); 32 32 33 33 const rootValue = { 34 - movie: () => { 34 + movie: async () => { 35 + await new Promise(resolve => setTimeout(resolve, 50)); 35 36 return { 36 37 id: 'foo', 37 38 title: 'title',
+154 -5
exchanges/graphcache/src/cacheExchange.test.ts
··· 6 6 OperationResult, 7 7 CombinedError, 8 8 } from '@urql/core'; 9 - import { print, stripIgnoredCharacters } from 'graphql'; 10 9 10 + import { print, stripIgnoredCharacters } from 'graphql'; 11 11 import { vi, expect, it, describe } from 'vitest'; 12 12 13 13 import { ··· 2197 2197 }); 2198 2198 }); 2199 2199 2200 + describe('looping protection', () => { 2201 + it('applies stale to blocked looping queries', () => { 2202 + let normalData: OperationResult | undefined; 2203 + let extendedData: OperationResult | undefined; 2204 + 2205 + const client = createClient({ 2206 + url: 'http://0.0.0.0', 2207 + exchanges: [], 2208 + }); 2209 + 2210 + const { source: ops$, next: nextOp } = makeSubject<Operation>(); 2211 + const { source: res$, next: nextRes } = makeSubject<OperationResult>(); 2212 + 2213 + vi.spyOn(client, 'reexecuteOperation').mockImplementation(nextOp); 2214 + 2215 + const normalQuery = gql` 2216 + { 2217 + __typename 2218 + item { 2219 + __typename 2220 + id 2221 + } 2222 + } 2223 + `; 2224 + 2225 + const extendedQuery = gql` 2226 + { 2227 + __typename 2228 + item { 2229 + __typename 2230 + extended: id 2231 + extra @_optional 2232 + } 2233 + } 2234 + `; 2235 + 2236 + const forward = (ops$: Source<Operation>): Source<OperationResult> => 2237 + share( 2238 + merge([ 2239 + pipe( 2240 + ops$, 2241 + filter(() => false) 2242 + ) as any, 2243 + res$, 2244 + ]) 2245 + ); 2246 + 2247 + pipe( 2248 + cacheExchange()({ forward, client, dispatchDebug })(ops$), 2249 + tap(result => { 2250 + if (result.operation.kind === 'query') { 2251 + if (result.operation.key === 1) { 2252 + normalData = result; 2253 + } else if (result.operation.key === 2) { 2254 + extendedData = result; 2255 + } 2256 + } 2257 + }), 2258 + publish 2259 + ); 2260 + 2261 + const normalOp = client.createRequestOperation( 2262 + 'query', 2263 + { 2264 + key: 1, 2265 + query: normalQuery, 2266 + variables: undefined, 2267 + }, 2268 + { 2269 + requestPolicy: 'cache-first', 2270 + } 2271 + ); 2272 + 2273 + const extendedOp = client.createRequestOperation( 2274 + 'query', 2275 + { 2276 + key: 2, 2277 + query: extendedQuery, 2278 + variables: undefined, 2279 + }, 2280 + { 2281 + requestPolicy: 'cache-first', 2282 + } 2283 + ); 2284 + 2285 + nextOp(normalOp); 2286 + 2287 + nextRes({ 2288 + operation: normalOp, 2289 + data: { 2290 + __typename: 'Query', 2291 + item: { 2292 + __typename: 'Node', 2293 + id: 'id', 2294 + }, 2295 + }, 2296 + stale: false, 2297 + hasNext: false, 2298 + }); 2299 + 2300 + expect(normalData).toMatchObject({ stale: false }); 2301 + expect(client.reexecuteOperation).toHaveBeenCalledTimes(0); 2302 + 2303 + nextOp(extendedOp); 2304 + 2305 + expect(extendedData).toMatchObject({ stale: true }); 2306 + expect(client.reexecuteOperation).toHaveBeenCalledTimes(1); 2307 + 2308 + // Out of band re-execute first operation 2309 + nextOp(normalOp); 2310 + nextRes({ 2311 + ...queryResponse, 2312 + operation: normalOp, 2313 + data: { 2314 + __typename: 'Query', 2315 + item: { 2316 + __typename: 'Node', 2317 + id: 'id', 2318 + }, 2319 + }, 2320 + }); 2321 + 2322 + expect(normalData).toMatchObject({ stale: false }); 2323 + expect(extendedData).toMatchObject({ stale: true }); 2324 + expect(client.reexecuteOperation).toHaveBeenCalledTimes(3); 2325 + 2326 + nextOp(extendedOp); 2327 + 2328 + expect(normalData).toMatchObject({ stale: false }); 2329 + expect(extendedData).toMatchObject({ stale: true }); 2330 + expect(client.reexecuteOperation).toHaveBeenCalledTimes(3); 2331 + 2332 + nextRes({ 2333 + ...queryResponse, 2334 + operation: extendedOp, 2335 + data: { 2336 + __typename: 'Query', 2337 + item: { 2338 + __typename: 'Node', 2339 + extended: 'id', 2340 + extra: 'extra', 2341 + }, 2342 + }, 2343 + }); 2344 + 2345 + expect(extendedData).toMatchObject({ stale: false }); 2346 + expect(client.reexecuteOperation).toHaveBeenCalledTimes(4); 2347 + }); 2348 + }); 2349 + 2200 2350 describe('commutativity', () => { 2201 2351 it('applies results that come in out-of-order commutatively and consistently', () => { 2202 2352 vi.useFakeTimers(); ··· 2873 3023 }); 2874 3024 const { source: ops$, next: nextOp } = makeSubject<Operation>(); 2875 3025 const { source: res$, next: nextRes } = makeSubject<OperationResult>(); 2876 - 2877 - vi.spyOn(client, 'reexecuteOperation').mockImplementation(nextOp); 3026 + client.reexecuteOperation = nextOp; 2878 3027 2879 3028 const normalQuery = gql` 2880 3029 { ··· 2911 3060 } 2912 3061 `; 2913 3062 2914 - const forward = (ops$: Source<Operation>): Source<OperationResult> => 3063 + const forward = (operations$: Source<Operation>): Source<OperationResult> => 2915 3064 share( 2916 3065 merge([ 2917 3066 pipe( 2918 - ops$, 3067 + operations$, 2919 3068 filter(() => false) 2920 3069 ) as any, 2921 3070 res$,
+10 -1
exchanges/graphcache/src/cacheExchange.ts
··· 32 32 noopDataState, 33 33 hydrateData, 34 34 reserveLayer, 35 + hasLayer, 35 36 } from './store/data'; 36 37 37 38 interface OperationResultWithMeta extends Partial<OperationResult> { ··· 377 378 (requestPolicy === 'cache-first' && 378 379 res.outcome === 'partial' && 379 380 !reexecutingOperations.has(res.operation.key))); 381 + // Set stale to true anyway, even if the reexecute will be blocked, if the operation 382 + // is in progress. We can be reasonably sure of that if a layer has been reserved for it. 383 + const stale = 384 + requestPolicy !== 'cache-only' && 385 + (shouldReexecute || 386 + (res.outcome === 'partial' && 387 + reexecutingOperations.has(res.operation.key) && 388 + hasLayer(store.data, res.operation.key))); 380 389 381 390 const result: OperationResult = { 382 391 operation: addMetadata(res.operation, { ··· 385 394 data: res.data, 386 395 error: res.error, 387 396 extensions: res.extensions, 388 - stale: shouldReexecute && !res.hasNext, 397 + stale: stale && !res.hasNext, 389 398 hasNext: shouldReexecute && res.hasNext, 390 399 }; 391 400
+5
exchanges/graphcache/src/store/data.ts
··· 528 528 data.commutativeKeys.add(layerKey); 529 529 }; 530 530 531 + /** Checks whether a given layer exists */ 532 + export const hasLayer = (data: InMemoryData, layerKey: number) => 533 + data.commutativeKeys.has(layerKey) || 534 + data.optimisticOrder.indexOf(layerKey) > -1; 535 + 531 536 /** Creates an optimistic layer of links and records */ 532 537 const createLayer = (data: InMemoryData, layerKey: number) => { 533 538 if (data.optimisticOrder.indexOf(layerKey) === -1) {