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 flowing through the entire cacheExchange (#1011)

* (graphcache) - Fix offline queries not flowing through the entire cacheExchange

* Add support for failed queries to be reexecuted on online

* Update offlineExchange tests

authored by

Phil Pluckthun and committed by
GitHub
e55f2751 372b0e3f

+98 -65
+6
.changeset/strange-waves-confess.md
··· 1 + --- 2 + '@urql/exchange-graphcache': patch 3 + --- 4 + 5 + Fix queries that have erroed with a `NetworkError` (`isOfflineError`) not flowing back completely through the `cacheExchange`. 6 + These queries should also now be reexecuted when the client comes back online.
+5
.changeset/tidy-bobcats-sniff.md
··· 1 + --- 2 + '@urql/core': patch 3 + --- 4 + 5 + Allow `client.reexecuteOperation` to be called with mutations which skip the active operation minimums.
+11 -12
exchanges/graphcache/src/offlineExchange.test.ts
··· 61 61 62 62 it('should read the metadata and dispatch operations on initialization', () => { 63 63 const client = createClient({ url: 'http://0.0.0.0' }); 64 - const dispatchOperationSpy = jest.spyOn(client, 'dispatchOperation'); 64 + const reexecuteOperation = jest 65 + .spyOn(client, 'reexecuteOperation') 66 + .mockImplementation(() => undefined); 65 67 const op = client.createRequestOperation('mutation', { 66 68 key: 1, 67 69 query: mutationOne, ··· 81 83 82 84 storage.readData.mockReturnValueOnce({ then: () => undefined }); 83 85 storage.readMetadata.mockReturnValueOnce({ then: cb => cb([op]) }); 84 - dispatchOperationSpy.mockImplementation(() => undefined); 86 + reexecuteOperation.mockImplementation(() => undefined); 85 87 86 88 jest.useFakeTimers(); 87 89 pipe( ··· 92 94 jest.runAllTimers(); 93 95 94 96 expect(storage.readMetadata).toBeCalledTimes(1); 95 - expect(dispatchOperationSpy).toBeCalledTimes(1); 96 - expect(dispatchOperationSpy).toBeCalledWith({ 97 + expect(reexecuteOperation).toBeCalledTimes(1); 98 + expect(reexecuteOperation).toBeCalledWith({ 97 99 ...op, 98 100 key: expect.any(Number), 99 101 }); ··· 229 231 ); 230 232 231 233 next(queryOp); 232 - expect(result).toBeCalledTimes(0); 233 - expect(response).toBeCalledTimes(1); 234 234 235 - await Promise.resolve(); 236 235 expect(result).toBeCalledTimes(1); 237 236 expect(response).toBeCalledTimes(1); 238 237 ··· 258 257 const onlineSpy = jest.spyOn(navigator, 'onLine', 'get'); 259 258 260 259 const client = createClient({ url: 'http://0.0.0.0' }); 261 - const dispatchOperationSpy = jest 262 - .spyOn(client, 'dispatchOperation') 260 + const reexecuteOperation = jest 261 + .spyOn(client, 'reexecuteOperation') 263 262 .mockImplementation(() => undefined); 264 263 265 264 const mutationOp = client.createRequestOperation('mutation', { ··· 319 318 ]); 320 319 321 320 flush!(); 322 - expect(dispatchOperationSpy).toHaveBeenCalledTimes(1); 323 - expect((dispatchOperationSpy.mock.calls[0][0] as any).key).toEqual(1); 324 - expect((dispatchOperationSpy.mock.calls[0][0] as any).query).toEqual( 321 + expect(reexecuteOperation).toHaveBeenCalledTimes(1); 322 + expect((reexecuteOperation.mock.calls[0][0] as any).key).toEqual(1); 323 + expect((reexecuteOperation.mock.calls[0][0] as any).query).toEqual( 325 324 formatDocument(mutationOp.query) 326 325 ); 327 326 });
+72 -52
exchanges/graphcache/src/offlineExchange.ts
··· 3 3 4 4 import { 5 5 Operation, 6 - GraphQLRequest, 7 6 Exchange, 7 + ExchangeIO, 8 8 CombinedError, 9 9 createRequest, 10 10 } from '@urql/core'; ··· 20 20 } from './ast'; 21 21 22 22 import { makeDict } from './helpers/dict'; 23 - import { OptimisticMutationConfig, Variables } from './types'; 23 + import { 24 + SerializedRequest, 25 + OptimisticMutationConfig, 26 + Variables, 27 + } from './types'; 24 28 import { cacheExchange, CacheExchangeOpts } from './cacheExchange'; 25 29 import { toRequestPolicy } from './helpers/operation'; 26 30 ··· 59 63 error.networkError.message 60 64 )); 61 65 62 - export const offlineExchange = (opts: CacheExchangeOpts): Exchange => ({ 63 - forward: outerForward, 64 - client, 65 - dispatchDebug, 66 - }) => ops$ => { 66 + export const offlineExchange = (opts: CacheExchangeOpts): Exchange => input => { 67 67 const { storage } = opts; 68 - const { source: reboundOps$, next } = makeSubject<Operation>(); 69 - let forward = outerForward; 70 68 71 69 if ( 72 70 storage && ··· 74 72 storage.readMetadata && 75 73 storage.writeMetadata 76 74 ) { 75 + const { forward: outerForward, client, dispatchDebug } = input; 77 76 const optimisticMutations = opts.optimistic || {}; 78 - const failedQueue: GraphQLRequest[] = []; 77 + const failedQueue: Operation[] = []; 79 78 80 79 const updateMetadata = () => { 81 - storage.writeMetadata!( 82 - failedQueue.map(op => ({ 83 - query: print(op.query), 84 - variables: op.variables, 85 - })) 86 - ); 80 + const requests: SerializedRequest[] = []; 81 + for (let i = 0; i < failedQueue.length; i++) { 82 + const op = failedQueue[i]; 83 + if (op.operationName === 'mutation') { 84 + requests.push({ 85 + query: print(op.query), 86 + variables: op.variables, 87 + }); 88 + } 89 + } 90 + storage.writeMetadata!(requests); 87 91 }; 88 92 89 93 let _flushing = false; 90 94 const flushQueue = () => { 91 - let request: void | GraphQLRequest; 92 - while (!_flushing && (request = failedQueue.shift())) { 95 + if (!_flushing) { 93 96 _flushing = true; 94 - client.dispatchOperation( 95 - client.createRequestOperation('mutation', request) 96 - ); 97 + let operation: void | Operation; 98 + while ((operation = failedQueue.shift())) 99 + client.reexecuteOperation(operation); 100 + updateMetadata(); 97 101 _flushing = false; 98 102 } 99 - 100 - updateMetadata(); 101 103 }; 102 104 103 - forward = ops$ => { 105 + const forward: ExchangeIO = ops$ => { 104 106 return pipe( 105 107 outerForward(ops$), 106 108 filter(res => { 107 109 if ( 108 - res.operation.operationName === 'subscription' || 109 - !isOfflineError(res.error) 110 + res.operation.operationName === 'mutation' && 111 + isOfflineError(res.error) && 112 + isOptimisticMutation(optimisticMutations, res.operation) 110 113 ) { 111 - return true; 112 - } else if (res.operation.operationName === 'mutation') { 113 - if (isOptimisticMutation(optimisticMutations, res.operation)) { 114 - failedQueue.push(res.operation); 115 - updateMetadata(); 116 - return false; 117 - } 118 - 119 - return true; 114 + failedQueue.push(res.operation); 115 + updateMetadata(); 116 + return false; 120 117 } 121 118 122 - Promise.resolve().then(() => 123 - next(toRequestPolicy(res.operation, 'cache-only')) 124 - ); 125 - 126 - return false; 119 + return true; 127 120 }) 128 121 ); 129 122 }; 130 123 131 124 storage.onOnline(flushQueue); 132 125 storage.readMetadata().then(mutations => { 133 - if (mutations) 134 - for (let i = 0; i < mutations.length; i++) 126 + if (mutations) { 127 + for (let i = 0; i < mutations.length; i++) { 135 128 failedQueue.push( 136 - createRequest(mutations[i].query, mutations[i].variables) 129 + client.createRequestOperation( 130 + 'mutation', 131 + createRequest(mutations[i].query, mutations[i].variables) 132 + ) 137 133 ); 138 - flushQueue(); 134 + } 135 + 136 + flushQueue(); 137 + } 139 138 }); 140 - } 141 139 142 - const sharedOps$ = share(ops$); 143 - const opsAndRebound$ = merge([reboundOps$, sharedOps$]); 140 + const cacheResults$ = cacheExchange(opts)({ 141 + client, 142 + dispatchDebug, 143 + forward, 144 + }); 144 145 145 - return cacheExchange(opts)({ 146 - forward, 147 - client, 148 - dispatchDebug, 149 - })(opsAndRebound$); 146 + return ops$ => { 147 + const sharedOps$ = share(ops$); 148 + const { source: reboundOps$, next } = makeSubject<Operation>(); 149 + const opsAndRebound$ = merge([reboundOps$, sharedOps$]); 150 + 151 + return pipe( 152 + cacheResults$(opsAndRebound$), 153 + filter(res => { 154 + if ( 155 + res.operation.operationName === 'query' && 156 + isOfflineError(res.error) 157 + ) { 158 + next(toRequestPolicy(res.operation, 'cache-only')); 159 + failedQueue.push(res.operation); 160 + return false; 161 + } 162 + 163 + return true; 164 + }) 165 + ); 166 + }; 167 + } 168 + 169 + return cacheExchange(opts)(input); 150 170 };
+4 -1
packages/core/src/client.ts
··· 135 135 this.reexecuteOperation = (operation: Operation) => { 136 136 // Reexecute operation only if any subscribers are still subscribed to the 137 137 // operation's exchange results 138 - if ((this.activeOperations[operation.key] || 0) > 0) { 138 + if ( 139 + operation.operationName === 'mutation' || 140 + (this.activeOperations[operation.key] || 0) > 0 141 + ) { 139 142 this.queue.push(operation); 140 143 if (!isOperationBatchActive) { 141 144 Promise.resolve().then(this.dispatchOperation);