Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
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 };