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): Add missing retries after offline rehydration (#3196)

authored by

Phil Pluckthun and committed by
GitHub
ec5fd389 8640fdbd

+72 -91
+5
.changeset/wet-crabs-dream.md
··· 1 + --- 2 + '@urql/exchange-graphcache': patch 3 + --- 4 + 5 + Retry operations against offline cache and stabilize timing of flushing failed operations queue after rehydrating the storage data.
+1
exchanges/graphcache/src/cacheExchange.ts
··· 71 71 const store = new Store<C>(opts); 72 72 73 73 if (opts && opts.storage) { 74 + store.data.hydrating = true; 74 75 opts.storage.readData().then(entries => { 75 76 hydrateData(store.data, opts!.storage!, entries); 76 77 });
+3 -48
exchanges/graphcache/src/offlineExchange.test.ts
··· 5 5 Operation, 6 6 OperationResult, 7 7 } from '@urql/core'; 8 - import { print } from 'graphql'; 9 8 import { vi, expect, it, describe, beforeAll } from 'vitest'; 10 9 11 10 import { pipe, share, map, makeSubject, tap, publish } from 'wonka'; ··· 105 104 106 105 describe('offline', () => { 107 106 beforeAll(() => { 107 + vi.resetAllMocks(); 108 108 global.navigator = { onLine: true } as any; 109 109 }); 110 110 ··· 167 167 expect(queryOneData).toMatchObject(result.mock.calls[0][0].data); 168 168 169 169 next(mutationOp); 170 - expect(result).toBeCalledTimes(1); 171 - expect(storage.writeMetadata).toHaveBeenCalled(); 172 - expect(storage.writeMetadata).toHaveBeenCalledWith([ 173 - { 174 - query: `mutation { 175 - updateAuthor { 176 - id 177 - name 178 - __typename 179 - } 180 - }`, 181 - variables: {}, 182 - }, 183 - ]); 170 + expect(result).toBeCalledTimes(2); 184 171 185 172 next(queryOp); 186 - expect(result).toBeCalledTimes(2); 187 - expect(result.mock.calls[1][0].data).toMatchObject({ 188 - authors: [{ id: '123', name: 'URQL', __typename: 'Author' }], 189 - }); 173 + expect(result).toBeCalledTimes(3); 190 174 }); 191 175 192 176 it('should intercept errored queries', async () => { ··· 261 245 url: 'http://0.0.0.0', 262 246 exchanges: [], 263 247 }); 264 - const reexecuteOperation = vi 265 - .spyOn(client, 'reexecuteOperation') 266 - .mockImplementation(() => undefined); 267 248 268 249 const mutationOp = client.createRequestOperation('mutation', { 269 250 key: 1, ··· 300 281 ); 301 282 302 283 next(mutationOp); 303 - expect(storage.writeMetadata).toHaveBeenCalled(); 304 - expect(storage.writeMetadata).toHaveBeenCalledWith([ 305 - { 306 - query: `mutation { 307 - updateAuthor { 308 - id 309 - name 310 - __typename 311 - } 312 - }`, 313 - variables: {}, 314 - }, 315 - ]); 316 284 317 285 await onOnlineCalled; 318 286 319 287 flush!(); 320 - expect(reexecuteOperation).toHaveBeenCalled(); 321 - expect((reexecuteOperation.mock.calls[0][0] as any).key).toEqual(1); 322 - expect(print((reexecuteOperation.mock.calls[0][0] as any).query)).toEqual( 323 - print(gql` 324 - mutation { 325 - updateAuthor { 326 - id 327 - name 328 - __typename 329 - } 330 - } 331 - `) 332 - ); 333 288 }); 334 289 });
+48 -40
exchanges/graphcache/src/offlineExchange.ts
··· 1 - import { pipe, share, merge, makeSubject, filter } from 'wonka'; 1 + import { pipe, share, merge, makeSubject, filter, onPush } from 'wonka'; 2 2 import { SelectionNode } from '@0no-co/graphql.web'; 3 3 4 4 import { ··· 128 128 const { source: reboundOps$, next } = makeSubject<Operation>(); 129 129 const optimisticMutations = opts.optimistic || {}; 130 130 const failedQueue: Operation[] = []; 131 + let hasRehydrated = false; 132 + let isFlushingQueue = false; 131 133 132 134 const updateMetadata = () => { 133 - const requests: SerializedRequest[] = []; 134 - for (let i = 0; i < failedQueue.length; i++) { 135 - const operation = failedQueue[i]; 136 - if (operation.kind === 'mutation') { 137 - requests.push({ 138 - query: stringifyDocument(operation.query), 139 - variables: operation.variables, 140 - extensions: operation.extensions, 141 - }); 135 + if (hasRehydrated) { 136 + const requests: SerializedRequest[] = []; 137 + for (let i = 0; i < failedQueue.length; i++) { 138 + const operation = failedQueue[i]; 139 + if (operation.kind === 'mutation') { 140 + requests.push({ 141 + query: stringifyDocument(operation.query), 142 + variables: operation.variables, 143 + extensions: operation.extensions, 144 + }); 145 + } 142 146 } 147 + storage.writeMetadata!(requests); 143 148 } 144 - storage.writeMetadata!(requests); 145 149 }; 146 150 147 - let isFlushingQueue = false; 148 151 const flushQueue = () => { 149 152 if (!isFlushingQueue) { 150 153 isFlushingQueue = true; 151 - 152 154 for (let i = 0; i < failedQueue.length; i++) { 153 155 const operation = failedQueue[i]; 154 - if (operation.kind === 'mutation') { 156 + if (operation.kind === 'mutation') 155 157 next(makeOperation('teardown', operation)); 156 - } 158 + next(operation); 157 159 } 158 - 159 - for (let i = 0; i < failedQueue.length; i++) 160 - client.reexecuteOperation(failedQueue[i]); 161 160 162 161 failedQueue.length = 0; 163 162 isFlushingQueue = false; ··· 170 169 outerForward(ops$), 171 170 filter(res => { 172 171 if ( 172 + hasRehydrated && 173 173 res.operation.kind === 'mutation' && 174 174 isOfflineError(res.error, res) && 175 175 isOptimisticMutation(optimisticMutations, res.operation) ··· 185 185 ); 186 186 }; 187 187 188 - storage 189 - .readMetadata() 190 - .then(mutations => { 191 - if (mutations) { 192 - for (let i = 0; i < mutations.length; i++) { 193 - failedQueue.push( 194 - client.createRequestOperation( 195 - 'mutation', 196 - createRequest(mutations[i].query, mutations[i].variables), 197 - mutations[i].extensions 198 - ) 199 - ); 200 - } 201 - 202 - flushQueue(); 203 - } 204 - }) 205 - .finally(() => storage.onOnline!(flushQueue)); 206 - 207 188 const cacheResults$ = cacheExchange({ 208 189 ...opts, 209 190 storage: { 210 191 ...storage, 211 192 readData() { 212 - return storage.readData().finally(flushQueue); 193 + const hydrate = storage.readData(); 194 + return { 195 + async then(onEntries) { 196 + const mutations = await storage.readMetadata!(); 197 + for (let i = 0; mutations && i < mutations.length; i++) { 198 + failedQueue.push( 199 + client.createRequestOperation( 200 + 'mutation', 201 + createRequest(mutations[i].query, mutations[i].variables), 202 + mutations[i].extensions 203 + ) 204 + ); 205 + } 206 + onEntries!(await hydrate); 207 + storage.onOnline!(flushQueue); 208 + hasRehydrated = true; 209 + flushQueue(); 210 + }, 211 + }; 213 212 }, 214 213 }, 215 214 })({ ··· 219 218 }); 220 219 221 220 return operations$ => { 222 - const opsAndRebound$ = merge([reboundOps$, operations$]); 221 + const opsAndRebound$ = merge([ 222 + reboundOps$, 223 + pipe( 224 + operations$, 225 + onPush(operation => { 226 + if (operation.kind === 'query' && !hasRehydrated) { 227 + failedQueue.push(operation); 228 + } 229 + }) 230 + ), 231 + ]); 223 232 224 233 return pipe( 225 234 cacheResults$(opsAndRebound$), ··· 232 241 failedQueue.push(res.operation); 233 242 return false; 234 243 } 235 - 236 244 return true; 237 245 }) 238 246 );
+15 -3
exchanges/graphcache/src/store/data.ts
··· 31 31 } 32 32 33 33 export interface InMemoryData { 34 + /** Flag for whether the data is waiting for hydration */ 35 + hydrating: boolean; 34 36 /** Flag for whether deferred tasks have been scheduled yet */ 35 37 defer: boolean; 36 38 /** A list of entities that have been flagged for gargabe collection since no references to them are left */ ··· 109 111 // We don't create new layers for read operations and instead simply 110 112 // apply the currently available layer, if any 111 113 currentOptimisticKey = layerKey; 112 - } else if (isOptimistic || data.optimisticOrder.length > 1) { 114 + } else if ( 115 + isOptimistic || 116 + data.hydrating || 117 + data.optimisticOrder.length > 1 118 + ) { 113 119 // If this operation isn't optimistic and we see it for the first time, 114 120 // then it must've been optimistic in the past, so we can proactively 115 121 // clear the optimistic data before writing ··· 155 161 currentOptimisticKey = null; 156 162 157 163 // Determine whether the current operation has been a commutative layer 158 - if (layerKey && data.optimisticOrder.indexOf(layerKey) > -1) { 164 + if ( 165 + !data.hydrating && 166 + layerKey && 167 + data.optimisticOrder.indexOf(layerKey) > -1 168 + ) { 159 169 // Squash all layers in reverse order (low priority upwards) that have 160 170 // been written already 161 171 let i = data.optimisticOrder.length; ··· 217 227 }; 218 228 219 229 export const make = (queryRootKey: string): InMemoryData => ({ 230 + hydrating: false, 220 231 defer: false, 221 232 gc: new Set(), 222 233 persist: new Set(), ··· 634 645 } 635 646 } 636 647 648 + data.storage = storage; 649 + data.hydrating = false; 637 650 clearDataState(); 638 - data.storage = storage; 639 651 };