Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 111 lines 4.0 kB view raw
1import type { Operation, OperationResult, Exchange } from '@urql/core'; 2import { makeOperation } from '@urql/core'; 3import { pipe, tap, map } from 'wonka'; 4 5const defaultTTL = 5 * 60 * 1000; 6 7/** Input parameters for the {@link requestPolicyExchange}. */ 8export interface Options { 9 /** Predicate allowing you to selectively not upgrade `Operation`s. 10 * 11 * @remarks 12 * When `shouldUpgrade` is set, it may be used to selectively return a boolean 13 * per `Operation`. This allows certain `Operation`s to not be upgraded to a 14 * `cache-and-network` policy, when `false` is returned. 15 * 16 * By default, all `Operation`s are subject to be upgraded. 17 * operation to "cache-and-network". 18 */ 19 shouldUpgrade?: (op: Operation) => boolean; 20 /** The time-to-live (TTL) for which a request policy won't be upgraded. 21 * 22 * @remarks 23 * The `ttl` defines the time frame in which the `Operation` won't be updated 24 * with a `cache-and-network` request policy. If an `Operation` is sent again 25 * and the `ttl` time period has expired, the policy is upgraded. 26 * 27 * @defaultValue `300_000` - 5min 28 */ 29 ttl?: number; 30} 31 32/** Exchange factory that upgrades request policies to `cache-and-network` for queries outside of a defined `ttl`. 33 * 34 * @param options - An {@link Options} configuration object. 35 * @returns the created request-policy {@link Exchange}. 36 * 37 * @remarks 38 * The `requestPolicyExchange` upgrades query operations based on {@link Options.ttl}. 39 * The `ttl` defines a timeframe outside of which a query's request policy is set to 40 * `cache-and-network` to refetch it in the background. 41 * 42 * You may define a {@link Options.shouldUpgrade} function to selectively ignore some 43 * operations by returning `false` there. 44 * 45 * @example 46 * ```ts 47 * requestPolicyExchange({ 48 * // Upgrade when we haven't seen this operation for 1 second 49 * ttl: 1000, 50 * // and only upgrade operations that query the `todos` field. 51 * shouldUpgrade: op => op.kind === 'query' && op.query.definitions[0].name?.value === 'todos' 52 * }); 53 * ``` 54 */ 55export const requestPolicyExchange = 56 (options: Options): Exchange => 57 ({ forward }) => { 58 const operations = new Map(); 59 const TTL = (options || {}).ttl || defaultTTL; 60 const dispatched = new Map<number, number>(); 61 let counter = 0; 62 63 const processIncomingOperation = (operation: Operation): Operation => { 64 if ( 65 operation.kind !== 'query' || 66 (operation.context.requestPolicy !== 'cache-first' && 67 operation.context.requestPolicy !== 'cache-only') 68 ) { 69 return operation; 70 } 71 72 const currentTime = new Date().getTime(); 73 // When an operation passes by we track the current time 74 dispatched.set(operation.key, counter); 75 queueMicrotask(() => { 76 counter = (counter + 1) | 0; 77 }); 78 const lastOccurrence = operations.get(operation.key) || 0; 79 if ( 80 currentTime - lastOccurrence > TTL && 81 (!options.shouldUpgrade || options.shouldUpgrade(operation)) 82 ) { 83 return makeOperation(operation.kind, operation, { 84 ...operation.context, 85 requestPolicy: 'cache-and-network', 86 }); 87 } 88 89 return operation; 90 }; 91 92 const processIncomingResults = (result: OperationResult): void => { 93 // When we get a result for the operation we check whether it resolved 94 // synchronously by checking whether the counter is different from the 95 // dispatched counter. 96 const lastDispatched = dispatched.get(result.operation.key) || 0; 97 if (counter !== lastDispatched) { 98 // We only delete in the case of a miss to ensure that cache-and-network 99 // is properly taken care of 100 dispatched.delete(result.operation.key); 101 operations.set(result.operation.key, new Date().getTime()); 102 } 103 }; 104 105 return ops$ => { 106 return pipe( 107 forward(pipe(ops$, map(processIncomingOperation))), 108 tap(processIncomingResults) 109 ); 110 }; 111 };