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(auth): Fix willAuthError not being called on waiting operations (#3017)

authored by

Phil Pluckthun and committed by
GitHub
64319ec2 3fa84604

+86 -42
+5
.changeset/stupid-cobras-clap.md
··· 1 + --- 2 + '@urql/exchange-auth': patch 3 + --- 4 + 5 + Fix `willAuthError` not being called for operations that are waiting on the authentication state to update. This can actually lead to a common issue where operations that came in during the authentication initialization (on startup) will never have `willAuthError` called on them. This can cause an easy mistake where the initial authentication state is never checked to be valid.
+50
exchanges/auth/src/authExchange.test.ts
··· 165 165 toPromise 166 166 ); 167 167 168 + await new Promise(resolve => setTimeout(resolve)); 169 + 168 170 next(queryOperation); 169 171 170 172 next( ··· 333 335 'final-token' 334 336 ); 335 337 }); 338 + 339 + it('calls willAuthError on queued operations', async () => { 340 + const { exchangeArgs, result, operations } = makeExchangeArgs(); 341 + const { source, next } = makeSubject<any>(); 342 + 343 + let initialAuthResolve: ((_?: any) => void) | undefined; 344 + 345 + const willAuthError = vi.fn().mockReturnValue(false); 346 + 347 + pipe( 348 + source, 349 + authExchange<{ token: string }>({ 350 + async getAuth() { 351 + await new Promise(resolve => { 352 + initialAuthResolve = resolve; 353 + }); 354 + 355 + return { token: 'token' }; 356 + }, 357 + willAuthError, 358 + didAuthError: () => false, 359 + addAuthToOperation: ({ authState, operation }) => { 360 + return withAuthHeader(operation, authState?.token); 361 + }, 362 + })(exchangeArgs), 363 + publish 364 + ); 365 + 366 + await Promise.resolve(); 367 + 368 + next(queryOperation); 369 + expect(result).toHaveBeenCalledTimes(0); 370 + expect(willAuthError).toHaveBeenCalledTimes(0); 371 + 372 + expect(initialAuthResolve).toBeDefined(); 373 + initialAuthResolve!(); 374 + 375 + await new Promise(resolve => setTimeout(resolve)); 376 + 377 + expect(willAuthError).toHaveBeenCalledTimes(1); 378 + expect(result).toHaveBeenCalledTimes(1); 379 + 380 + expect(operations.length).toBe(1); 381 + expect(operations[0]).toHaveProperty( 382 + 'context.fetchOptions.headers.Authorization', 383 + 'token' 384 + ); 385 + });
+31 -42
exchanges/auth/src/authExchange.ts
··· 1 1 import { 2 + Source, 2 3 pipe, 3 4 map, 4 - mergeMap, 5 - fromPromise, 6 - fromValue, 7 5 filter, 8 6 onStart, 9 - empty, 10 7 take, 11 8 makeSubject, 12 9 toPromise, 13 10 merge, 14 11 share, 15 - takeUntil, 16 12 } from 'wonka'; 17 13 18 14 import { ··· 72 68 willAuthError, 73 69 }: AuthConfig<T>): Exchange { 74 70 return ({ client, forward }) => { 71 + const bypassQueue: WeakSet<Operation> = new WeakSet(); 75 72 const retryQueue: Map<number, Operation> = new Map(); 73 + 76 74 const { 77 75 source: retrySource$, 78 76 next: retryOperation, ··· 94 92 95 93 return pipe( 96 94 result$, 97 - onStart(() => retryOperation(operation)), 95 + onStart(() => { 96 + bypassQueue.add(operation); 97 + retryOperation(operation); 98 + }), 98 99 filter(result => result.operation.key === operation.key), 99 100 take(1), 100 101 toPromise ··· 142 143 ); 143 144 144 145 const opsWithAuth$ = pipe( 145 - merge([ 146 - retrySource$, 147 - pipe( 148 - pendingOps$, 149 - mergeMap(operation => { 150 - if (retryQueue.has(operation.key)) { 151 - return empty; 152 - } 146 + merge([retrySource$, pendingOps$]), 147 + map(operation => { 148 + if (bypassQueue.has(operation)) { 149 + return operation; 150 + } else if (authPromise) { 151 + operation = addAuthAttemptToOperation(operation, false); 152 + retryQueue.set( 153 + operation.key, 154 + addAuthAttemptToOperation(operation, false) 155 + ); 156 + return null; 157 + } else if ( 158 + !operation.context.authAttempt && 159 + willAuthError && 160 + willAuthError({ operation, authState }) 161 + ) { 162 + refreshAuth(operation); 163 + return null; 164 + } 153 165 154 - if ( 155 - !authPromise && 156 - willAuthError && 157 - willAuthError({ operation, authState }) 158 - ) { 159 - refreshAuth(operation); 160 - return empty; 161 - } else if (!authPromise) { 162 - return fromValue(addAuthAttemptToOperation(operation, false)); 163 - } 164 - 165 - const teardown$ = pipe( 166 - sharedOps$, 167 - filter(op => { 168 - return op.kind === 'teardown' && op.key === operation.key; 169 - }) 170 - ); 171 - 172 - return pipe( 173 - fromPromise(authPromise), 174 - map(() => addAuthAttemptToOperation(operation, false)), 175 - takeUntil(teardown$) 176 - ); 177 - }) 178 - ), 179 - ]), 180 - map(operation => addAuthToOperation({ operation, authState })) 181 - ); 166 + operation = addAuthAttemptToOperation(operation, false); 167 + return addAuthToOperation({ operation, authState }); 168 + }), 169 + filter(Boolean) 170 + ) as Source<Operation>; 182 171 183 172 const result$ = pipe(merge([opsWithAuth$, teardownOps$]), forward, share); 184 173