Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 6.6 kB view raw
1/* eslint-disable @typescript-eslint/no-use-before-define */ 2import { filter, map, merge, pipe, tap } from 'wonka'; 3 4import type { Client } from '../client'; 5import type { Exchange, Operation, OperationResult } from '../types'; 6 7import { 8 makeOperation, 9 addMetadata, 10 collectTypenames, 11 formatDocument, 12 makeResult, 13} from '../utils'; 14 15type ResultCache = Map<number, OperationResult>; 16type OperationCache = Map<string, Set<number>>; 17 18const shouldSkip = ({ kind }: Operation) => 19 kind !== 'mutation' && kind !== 'query'; 20 21/** Adds unique typenames to query (for invalidating cache entries) */ 22export const mapTypeNames = (operation: Operation): Operation => { 23 const query = formatDocument(operation.query); 24 if (query !== operation.query) { 25 const formattedOperation = makeOperation(operation.kind, operation); 26 formattedOperation.query = query; 27 return formattedOperation; 28 } else { 29 return operation; 30 } 31}; 32 33/** Default document cache exchange. 34 * 35 * @remarks 36 * The default document cache in `urql` avoids sending the same GraphQL request 37 * multiple times by caching it using the {@link Operation.key}. It will invalidate 38 * query results automatically whenever it sees a mutation responses with matching 39 * `__typename`s in their responses. 40 * 41 * The document cache will get the introspected `__typename` fields by modifying 42 * your GraphQL operation documents using the {@link formatDocument} utility. 43 * 44 * This automatic invalidation strategy can fail if your query or mutation don’t 45 * contain matching typenames, for instance, because the query contained an 46 * empty list. 47 * You can manually add hints for this exchange by specifying a list of 48 * {@link OperationContext.additionalTypenames} for queries and mutations that 49 * should invalidate one another. 50 * 51 * @see {@link https://urql.dev/goto/docs/basics/document-caching} for more information on this cache. 52 */ 53export const cacheExchange: Exchange = ({ forward, client, dispatchDebug }) => { 54 const resultCache: ResultCache = new Map(); 55 const operationCache: OperationCache = new Map(); 56 57 const isOperationCached = (operation: Operation) => 58 operation.kind === 'query' && 59 operation.context.requestPolicy !== 'network-only' && 60 (operation.context.requestPolicy === 'cache-only' || 61 resultCache.has(operation.key)); 62 63 return ops$ => { 64 const cachedOps$ = pipe( 65 ops$, 66 filter(op => !shouldSkip(op) && isOperationCached(op)), 67 map(operation => { 68 const cachedResult = resultCache.get(operation.key); 69 70 dispatchDebug({ 71 operation, 72 ...(cachedResult 73 ? { 74 type: 'cacheHit', 75 message: 'The result was successfully retrieved from the cache', 76 } 77 : { 78 type: 'cacheMiss', 79 message: 'The result could not be retrieved from the cache', 80 }), 81 }); 82 83 let result: OperationResult = 84 cachedResult || 85 makeResult(operation, { 86 data: null, 87 }); 88 89 result = { 90 ...result, 91 operation: addMetadata(operation, { 92 cacheOutcome: cachedResult ? 'hit' : 'miss', 93 }), 94 }; 95 96 if (operation.context.requestPolicy === 'cache-and-network') { 97 result.stale = true; 98 reexecuteOperation(client, operation); 99 } 100 101 return result; 102 }) 103 ); 104 105 const forwardedOps$ = pipe( 106 merge([ 107 pipe( 108 ops$, 109 filter(op => !shouldSkip(op) && !isOperationCached(op)), 110 map(mapTypeNames) 111 ), 112 pipe( 113 ops$, 114 filter(op => shouldSkip(op)) 115 ), 116 ]), 117 map(op => addMetadata(op, { cacheOutcome: 'miss' })), 118 filter( 119 op => op.kind !== 'query' || op.context.requestPolicy !== 'cache-only' 120 ), 121 forward, 122 tap(response => { 123 let { operation } = response; 124 if (!operation) return; 125 126 let typenames = operation.context.additionalTypenames || []; 127 // NOTE: For now, we only respect `additionalTypenames` from subscriptions to 128 // avoid unexpected breaking changes 129 // We'd expect live queries or other update mechanisms to be more suitable rather 130 // than using subscriptions as “signals” to reexecute queries. However, if they’re 131 // just used as signals, it’s intuitive to hook them up using `additionalTypenames` 132 if (response.operation.kind !== 'subscription') { 133 typenames = collectTypenames(response.data).concat(typenames); 134 } 135 136 // Invalidates the cache given a mutation's response 137 if ( 138 response.operation.kind === 'mutation' || 139 response.operation.kind === 'subscription' 140 ) { 141 const pendingOperations = new Set<number>(); 142 143 dispatchDebug({ 144 type: 'cacheInvalidation', 145 message: `The following typenames have been invalidated: ${typenames}`, 146 operation, 147 data: { typenames, response }, 148 }); 149 150 for (let i = 0; i < typenames.length; i++) { 151 const typeName = typenames[i]; 152 let operations = operationCache.get(typeName); 153 if (!operations) 154 operationCache.set(typeName, (operations = new Set())); 155 for (const key of operations.values()) pendingOperations.add(key); 156 operations.clear(); 157 } 158 159 for (const key of pendingOperations.values()) { 160 if (resultCache.has(key)) { 161 operation = (resultCache.get(key) as OperationResult).operation; 162 resultCache.delete(key); 163 reexecuteOperation(client, operation); 164 } 165 } 166 } else if (operation.kind === 'query' && response.data) { 167 resultCache.set(operation.key, response); 168 for (let i = 0; i < typenames.length; i++) { 169 const typeName = typenames[i]; 170 let operations = operationCache.get(typeName); 171 if (!operations) 172 operationCache.set(typeName, (operations = new Set())); 173 operations.add(operation.key); 174 } 175 } 176 }) 177 ); 178 179 return merge([cachedOps$, forwardedOps$]); 180 }; 181}; 182 183/** Reexecutes an `Operation` with the `network-only` request policy. 184 * @internal 185 */ 186export const reexecuteOperation = (client: Client, operation: Operation) => { 187 return client.reexecuteOperation( 188 makeOperation(operation.kind, operation, { 189 requestPolicy: 'network-only', 190 }) 191 ); 192};