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.

(graphcache) - Fix offline queries not delivering any results on cache-miss and offline (#985)

* Fix fetching becoming infinite for offline queries

When the offline queries issues another request due
to a complete cache miss then fetching would be infinite
as the result would be swallowed.

* Add changeset

* Remove experimental note for offline support

* Use Subject to rebound operation instead of reexecuteOperation

* Update timing of rebound operation

* Update expected timing in tests

authored by

Phil Pluckthun and committed by
GitHub
0af76d43 6dbbacd3

+107 -34
+5
.changeset/pretty-pans-doubt.md
··· 1 + --- 2 + '@urql/exchange-graphcache': patch 3 + --- 4 + 5 + Fix operation results being obstructed by the `offlineExchange` when the network request has failed due to being offline and no cache result has been issued. Instead the `offlineExchange` will now retry with `cache-only` policy
-3
docs/graphcache/offline.md
··· 12 12 serves cached data entirely from memory when a user's device is offline and still display 13 13 optimistically executed mutations. 14 14 15 - > **NOTE:** Offline Support is currently experimental! It hasn't been extensively tested yet and 16 - > may not always behave as expected. Please try it out with caution! 17 - 18 15 ## Setup 19 16 20 17 Everything that's needed to set up offline-support is already packaged in the
+1 -24
exchanges/graphcache/src/cacheExchange.ts
··· 27 27 28 28 import { query, write, writeOptimistic } from './operations'; 29 29 import { makeDict, isDictEmpty } from './helpers/dict'; 30 + import { addCacheOutcome, toRequestPolicy } from './helpers/operation'; 30 31 import { filterVariables, getMainOperation } from './ast'; 31 32 import { Store, noopDataState, hydrateData, reserveLayer } from './store'; 32 33 ··· 48 49 type OperationMap = Map<number, Operation>; 49 50 type OptimisticDependencies = Map<number, Dependencies>; 50 51 type DependentOperations = Record<string, number[]>; 51 - 52 - // Returns the given operation result with added cacheOutcome meta field 53 - const addCacheOutcome = (op: Operation, outcome: CacheOutcome): Operation => ({ 54 - ...op, 55 - context: { 56 - ...op.context, 57 - meta: { 58 - ...op.context.meta, 59 - cacheOutcome: outcome, 60 - }, 61 - }, 62 - }); 63 - 64 - // Copy an operation and change the requestPolicy to skip the cache 65 - const toRequestPolicy = ( 66 - operation: Operation, 67 - requestPolicy: RequestPolicy 68 - ): Operation => ({ 69 - ...operation, 70 - context: { 71 - ...operation.context, 72 - requestPolicy, 73 - }, 74 - }); 75 52 76 53 export interface CacheExchangeOpts { 77 54 updates?: Partial<UpdatesConfig>;
+28
exchanges/graphcache/src/helpers/operation.ts
··· 1 + import { Operation, RequestPolicy, CacheOutcome } from '@urql/core'; 2 + 3 + // Returns the given operation result with added cacheOutcome meta field 4 + export const addCacheOutcome = ( 5 + op: Operation, 6 + outcome: CacheOutcome 7 + ): Operation => ({ 8 + ...op, 9 + context: { 10 + ...op.context, 11 + meta: { 12 + ...op.context.meta, 13 + cacheOutcome: outcome, 14 + }, 15 + }, 16 + }); 17 + 18 + // Copy an operation and change the requestPolicy to skip the cache 19 + export const toRequestPolicy = ( 20 + operation: Operation, 21 + requestPolicy: RequestPolicy 22 + ): Operation => ({ 23 + ...operation, 24 + context: { 25 + ...operation.context, 26 + requestPolicy, 27 + }, 28 + });
+57
exchanges/graphcache/src/offlineExchange.test.ts
··· 192 192 }); 193 193 }); 194 194 195 + it('should intercept errored queries', async () => { 196 + const client = createClient({ url: 'http://0.0.0.0' }); 197 + const onlineSpy = jest 198 + .spyOn(navigator, 'onLine', 'get') 199 + .mockReturnValueOnce(false); 200 + 201 + const queryOp = client.createRequestOperation('query', { 202 + key: 1, 203 + query: queryOne, 204 + }); 205 + 206 + const response = jest.fn( 207 + (forwardOp: Operation): OperationResult => { 208 + onlineSpy.mockReturnValueOnce(false); 209 + return { 210 + operation: forwardOp, 211 + // @ts-ignore 212 + error: { networkError: new Error('failed to fetch') }, 213 + }; 214 + } 215 + ); 216 + 217 + const { source: ops$, next } = makeSubject<Operation>(); 218 + const result = jest.fn(); 219 + const forward: ExchangeIO = ops$ => pipe(ops$, map(response)); 220 + 221 + storage.readData.mockReturnValueOnce({ then: () => undefined }); 222 + storage.readMetadata.mockReturnValueOnce({ then: () => undefined }); 223 + storage.writeMetadata.mockReturnValueOnce({ then: () => undefined }); 224 + 225 + pipe( 226 + offlineExchange({ storage })({ forward, client, dispatchDebug })(ops$), 227 + tap(result), 228 + publish 229 + ); 230 + 231 + next(queryOp); 232 + expect(result).toBeCalledTimes(0); 233 + expect(response).toBeCalledTimes(1); 234 + 235 + await Promise.resolve(); 236 + expect(result).toBeCalledTimes(1); 237 + expect(response).toBeCalledTimes(1); 238 + 239 + expect(result.mock.calls[0][0]).toEqual({ 240 + data: null, 241 + error: undefined, 242 + extensions: undefined, 243 + operation: expect.any(Object), 244 + }); 245 + 246 + expect(result.mock.calls[0][0]).toHaveProperty( 247 + 'operation.context.meta.cacheOutcome', 248 + 'miss' 249 + ); 250 + }); 251 + 195 252 it('should flush the queue when we become online', () => { 196 253 let flush: () => {}; 197 254 storage.onOnline.mockImplementation(cb => {
+16 -7
exchanges/graphcache/src/offlineExchange.ts
··· 1 - import { pipe, filter } from 'wonka'; 1 + import { pipe, merge, makeSubject, share, filter } from 'wonka'; 2 2 import { print, SelectionNode } from 'graphql'; 3 3 4 4 import { ··· 22 22 import { makeDict } from './helpers/dict'; 23 23 import { OptimisticMutationConfig, Variables } from './types'; 24 24 import { cacheExchange, CacheExchangeOpts } from './cacheExchange'; 25 + import { toRequestPolicy } from './helpers/operation'; 25 26 26 27 /** Determines whether a given query contains an optimistic mutation field */ 27 28 const isOptimisticMutation = ( ··· 62 63 forward: outerForward, 63 64 client, 64 65 dispatchDebug, 65 - }) => { 66 + }) => ops$ => { 66 67 const { storage } = opts; 68 + const { source: reboundOps$, next } = makeSubject<Operation>(); 67 69 let forward = outerForward; 68 70 69 71 if ( ··· 112 114 failedQueue.push(res.operation); 113 115 updateMetadata(); 114 116 return false; 115 - } else { 116 - return true; 117 117 } 118 - } else { 119 - return false; 118 + 119 + return true; 120 120 } 121 + 122 + Promise.resolve().then(() => 123 + next(toRequestPolicy(res.operation, 'cache-only')) 124 + ); 125 + 126 + return false; 121 127 }) 122 128 ); 123 129 }; ··· 133 139 }); 134 140 } 135 141 142 + const sharedOps$ = share(ops$); 143 + const opsAndRebound$ = merge([reboundOps$, sharedOps$]); 144 + 136 145 return cacheExchange(opts)({ 137 146 forward, 138 147 client, 139 148 dispatchDebug, 140 - }); 149 + })(opsAndRebound$); 141 150 };