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.

(retry) - Add retryWith option to update operations (#1881)

* (retry) - Add retryWith option to update operations

* Allow retryWith to return falsy value

* Add tests and improve mutually exclusive logic

authored by

Phil Pluckthun and committed by
GitHub
ab7ac600 792c6a11

+127 -6
+5
.changeset/stale-candles-enjoy.md
··· 1 + --- 2 + '@urql/exchange-retry': minor 3 + --- 4 + 5 + Add a new `retryWith` option which allows operations to be updated when a request is being retried.
+103
exchanges/retry/src/retryExchange.test.ts
··· 3 3 import { 4 4 gql, 5 5 createClient, 6 + makeOperation, 6 7 Operation, 7 8 OperationResult, 8 9 ExchangeIO, ··· 227 228 expect(response).toHaveBeenCalledTimes(mockOptions.maxNumberAttempts); 228 229 expect(result).toHaveBeenCalledTimes(1); 229 230 }); 231 + 232 + it('should allow retryWhen to return falsy value and act as replacement of retryIf', () => { 233 + const errorWithNetworkError = { 234 + ...queryOneError, 235 + networkError: 'scary network error', 236 + }; 237 + const response = jest.fn( 238 + (forwardOp: Operation): OperationResult => { 239 + expect(forwardOp.key).toBe(op.key); 240 + return { 241 + operation: forwardOp, 242 + // @ts-ignore 243 + error: errorWithNetworkError, 244 + }; 245 + } 246 + ); 247 + 248 + const result = jest.fn(); 249 + const forward: ExchangeIO = ops$ => { 250 + return pipe(ops$, map(response)); 251 + }; 252 + 253 + const retryWith = jest.fn(() => null); 254 + 255 + pipe( 256 + retryExchange({ 257 + ...mockOptions, 258 + retryIf: undefined, 259 + retryWith, 260 + })({ 261 + forward, 262 + client, 263 + dispatchDebug, 264 + })(ops$), 265 + tap(result), 266 + publish 267 + ); 268 + 269 + next(op); 270 + 271 + jest.runAllTimers(); 272 + 273 + // max number of retries, plus original call 274 + expect(retryWith).toHaveBeenCalledTimes(1); 275 + expect(response).toHaveBeenCalledTimes(1); 276 + expect(result).toHaveBeenCalledTimes(1); 277 + }); 278 + 279 + it('should allow retryWhen to return new operations when retrying', () => { 280 + const errorWithNetworkError = { 281 + ...queryOneError, 282 + networkError: 'scary network error', 283 + }; 284 + const response = jest.fn( 285 + (forwardOp: Operation): OperationResult => { 286 + expect(forwardOp.key).toBe(op.key); 287 + return { 288 + operation: forwardOp, 289 + // @ts-ignore 290 + error: errorWithNetworkError, 291 + }; 292 + } 293 + ); 294 + 295 + const result = jest.fn(); 296 + const forward: ExchangeIO = ops$ => { 297 + return pipe(ops$, map(response)); 298 + }; 299 + 300 + const retryWith = jest.fn((_error, operation) => { 301 + return makeOperation(operation.kind, operation, { 302 + ...operation.context, 303 + counter: (operation.context?.counter || 0) + 1, 304 + }); 305 + }); 306 + 307 + pipe( 308 + retryExchange({ 309 + ...mockOptions, 310 + retryIf: undefined, 311 + retryWith, 312 + })({ 313 + forward, 314 + client, 315 + dispatchDebug, 316 + })(ops$), 317 + tap(result), 318 + publish 319 + ); 320 + 321 + next(op); 322 + 323 + jest.runAllTimers(); 324 + 325 + // max number of retries, plus original call 326 + expect(retryWith).toHaveBeenCalledTimes(mockOptions.maxNumberAttempts - 1); 327 + expect(response).toHaveBeenCalledTimes(mockOptions.maxNumberAttempts); 328 + expect(result).toHaveBeenCalledTimes(1); 329 + 330 + expect(response.mock.calls[1][0]).toHaveProperty('context.counter', 1); 331 + expect(response.mock.calls[2][0]).toHaveProperty('context.counter', 2); 332 + });
+19 -6
exchanges/retry/src/retryExchange.ts
··· 25 25 maxNumberAttempts?: number; 26 26 /** Conditionally determine whether an error should be retried */ 27 27 retryIf?: (error: CombinedError, operation: Operation) => boolean; 28 + /** Conditionally update operations as they're retried (retryIf can be replaced with this) */ 29 + retryWith?: ( 30 + error: CombinedError, 31 + operation: Operation 32 + ) => Operation | null | undefined; 28 33 } 29 34 30 35 export const retryExchange = ({ ··· 32 37 maxDelayMs, 33 38 randomDelay, 34 39 maxNumberAttempts, 35 - retryIf: retryIfOption, 40 + retryIf, 41 + retryWith, 36 42 }: RetryExchangeOptions): Exchange => { 37 43 const MIN_DELAY = initialDelayMs || 1000; 38 44 const MAX_DELAY = maxDelayMs || 15000; 39 45 const MAX_ATTEMPTS = maxNumberAttempts || 2; 40 46 const RANDOM_DELAY = randomDelay || true; 41 - 42 - const retryIf = 43 - retryIfOption || ((err: CombinedError) => err && err.networkError); 44 47 45 48 return ({ forward, dispatchDebug }) => ops$ => { 46 49 const sharedOps$ = pipe(ops$, share); ··· 107 110 filter(res => { 108 111 // Only retry if the error passes the conditional retryIf function (if passed) 109 112 // or if the error contains a networkError 110 - if (!res.error || !retryIf(res.error, res.operation)) { 113 + if ( 114 + !res.error || 115 + (retryIf 116 + ? !retryIf(res.error, res.operation) 117 + : !retryWith && !res.error.networkError) 118 + ) { 111 119 return true; 112 120 } 113 121 ··· 115 123 (res.operation.context.retryCount || 0) >= MAX_ATTEMPTS - 1; 116 124 117 125 if (!maxNumberAttemptsExceeded) { 126 + const operation = retryWith 127 + ? retryWith(res.error, res.operation) 128 + : res.operation; 129 + if (!operation) return true; 130 + 118 131 // Send failed responses to be retried by calling next on the retry$ subject 119 132 // Exclude operations that have been retried more than the specified max 120 - nextRetryOperation(res.operation); 133 + nextRetryOperation(operation); 121 134 return false; 122 135 } 123 136