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.

(exchanges) - implement requestPolicyExchange (#899)

* add requestPolicyExchange

* add readme

* add it to docs

* remove redundant else

* add options annotation

* add shoulUpgrade option

* document and test shouldUpgrade

* rename test

authored by

Jovi De Croock and committed by
GitHub
5a37a649 5594b55f

+313 -1
+2 -1
docs/concepts/exchanges.md
··· 25 25 - `retryExchange`: Allows operations to be retried 26 26 - `devtoolsExchange`: Provides the ability to use the [urql-devtools](https://github.com/FormidableLabs/urql-devtools) 27 27 - `multipartFetchExchange`: Provides multipart file upload capability 28 - - `suspenseExchange` (experimental): Allows the use of React Suspense on the client-side with `urql`'s built-in suspense mode 28 + - `suspenseExchange` (experimental): Allows the use of React Suspense on the client-side with `urql`'s built-in suspense modeµ 29 + - `requestPolicyExchange`: Automatically upgrades `cache-only` and `cache-first` operations to `cache-and-network` after a given amount of time. 29 30 30 31 It is also possible to apply custom exchanges to override the default logic. 31 32
+5
exchanges/request-policy/CHANGELOG.md
··· 1 + # Changelog 2 + 3 + ## v0.1.0 4 + 5 + **Initial Release**
+39
exchanges/request-policy/README.md
··· 1 + # @urql/exchange-request-policy (Exchange factory) 2 + 3 + `@urql/exchange-request-policy` is an exchange for the [`urql`](../../README.md) GraphQL client that will automatically upgrade operation request-policies 4 + on a time-to-live basis. 5 + 6 + ## Quick Start Guide 7 + 8 + First install `@urql/exchange-request-policy` alongside `urql`: 9 + 10 + ```sh 11 + yarn add @urql/exchange-request-policy 12 + # or 13 + npm install --save @urql/exchange-request-policy 14 + ``` 15 + 16 + Then add it to your client. 17 + 18 + ```js 19 + import { createClient, dedupExchange, cacheExchange, fetchExchange } from 'urql'; 20 + import { requestPolicyExchange } from '@urql/exchange-request-policy'; 21 + 22 + const client = createClient({ 23 + url: 'http://localhost:1234/graphql', 24 + exchanges: [ 25 + dedupExchange, 26 + requestPolicyExchange({ 27 + // The amount of time in ms that has to go by before upgrading, default is 5 minutes. 28 + ttl: 60 * 1000, // 1 minute. 29 + // An optional function that allows you to specify whether an operation should be upgraded. 30 + shouldUpgrade: operation => operation.context.requestPolicy !== 'cache-only', 31 + }), 32 + cacheExchange, 33 + fetchExchange, 34 + ], 35 + }); 36 + ``` 37 + 38 + Now when the exchange sees a `cache-first` operation that hasn't been seen in ttl amount of time it will upgrade 39 + the `requestPolicy` to `cache-and-network`.
+66
exchanges/request-policy/package.json
··· 1 + { 2 + "name": "@urql/exchange-request-policy", 3 + "version": "0.1.0", 4 + "description": "An exchange for operation request-policy upgrading in urql", 5 + "sideEffects": false, 6 + "homepage": "https://formidable.com/open-source/urql/docs/", 7 + "bugs": "https://github.com/FormidableLabs/urql/issues", 8 + "license": "MIT", 9 + "repository": { 10 + "type": "git", 11 + "url": "https://github.com/FormidableLabs/urql.git", 12 + "directory": "exchanges/request-policy" 13 + }, 14 + "keywords": [ 15 + "urql", 16 + "graphql client", 17 + "formidablelabs", 18 + "exchanges", 19 + "react", 20 + "request-policy" 21 + ], 22 + "main": "dist/urql-exchange-request-policy", 23 + "module": "dist/urql-exchange-request-policy.mjs", 24 + "types": "dist/types/index.d.ts", 25 + "source": "src/index.ts", 26 + "exports": { 27 + ".": { 28 + "import": "./dist/urql-exchange-request-policy.mjs", 29 + "require": "./dist/urql-exchange-request-policy.js", 30 + "types": "./dist/types/index.d.ts", 31 + "source": "./src/index.ts" 32 + }, 33 + "./package.json": "./package.json" 34 + }, 35 + "files": [ 36 + "LICENSE", 37 + "CHANGELOG.md", 38 + "README.md", 39 + "dist/" 40 + ], 41 + "scripts": { 42 + "test": "jest", 43 + "clean": "rimraf dist", 44 + "check": "tsc --noEmit", 45 + "lint": "eslint --ext=js,jsx,ts,tsx .", 46 + "build": "rollup -c ../../scripts/rollup/config.js", 47 + "prepare": "node ../../scripts/prepare/index.js", 48 + "prepublishOnly": "run-s clean build" 49 + }, 50 + "jest": { 51 + "preset": "../../scripts/jest/preset" 52 + }, 53 + "devDependencies": { 54 + "graphql": "^15.1.0" 55 + }, 56 + "peerDependencies": { 57 + "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" 58 + }, 59 + "dependencies": { 60 + "@urql/core": ">=1.12.0", 61 + "wonka": "^4.0.14" 62 + }, 63 + "publishConfig": { 64 + "access": "public" 65 + } 66 + }
+1
exchanges/request-policy/src/index.ts
··· 1 + export { requestPolicyExchange } from './requestPolicyExchange';
+136
exchanges/request-policy/src/requestPolicyExchange.test.ts
··· 1 + import gql from 'graphql-tag'; 2 + 3 + import { pipe, map, makeSubject, publish, tap } from 'wonka'; 4 + 5 + import { 6 + createClient, 7 + Operation, 8 + OperationResult, 9 + ExchangeIO, 10 + } from '@urql/core'; 11 + import { requestPolicyExchange } from './requestPolicyExchange'; 12 + 13 + const dispatchDebug = jest.fn(); 14 + 15 + const mockOptions = { 16 + ttl: 5, 17 + }; 18 + 19 + const queryOne = gql` 20 + { 21 + author { 22 + id 23 + name 24 + } 25 + } 26 + `; 27 + 28 + const queryOneData = { 29 + __typename: 'Query', 30 + author: { 31 + __typename: 'Author', 32 + id: '123', 33 + name: 'Author', 34 + }, 35 + }; 36 + 37 + let client, op, ops$, next; 38 + beforeEach(() => { 39 + client = createClient({ url: 'http://0.0.0.0' }); 40 + op = client.createRequestOperation('query', { 41 + key: 1, 42 + query: queryOne, 43 + }); 44 + 45 + ({ source: ops$, next } = makeSubject<Operation>()); 46 + }); 47 + 48 + it(`upgrades to cache-and-network`, done => { 49 + const response = jest.fn( 50 + (forwardOp: Operation): OperationResult => { 51 + return { 52 + operation: forwardOp, 53 + data: queryOneData, 54 + }; 55 + } 56 + ); 57 + 58 + const result = jest.fn(); 59 + const forward: ExchangeIO = ops$ => { 60 + return pipe(ops$, map(response)); 61 + }; 62 + 63 + pipe( 64 + requestPolicyExchange(mockOptions)({ 65 + forward, 66 + client, 67 + dispatchDebug, 68 + })(ops$), 69 + tap(result), 70 + publish 71 + ); 72 + 73 + next(op); 74 + 75 + expect(response).toHaveBeenCalledTimes(1); 76 + expect(response.mock.calls[0][0].context.requestPolicy).toEqual( 77 + 'cache-first' 78 + ); 79 + expect(result).toHaveBeenCalledTimes(1); 80 + 81 + setTimeout(() => { 82 + next(op); 83 + expect(response).toHaveBeenCalledTimes(2); 84 + expect(response.mock.calls[1][0].context.requestPolicy).toEqual( 85 + 'cache-and-network' 86 + ); 87 + expect(result).toHaveBeenCalledTimes(2); 88 + done(); 89 + }, 10); 90 + }); 91 + 92 + it(`doesn't upgrade when shouldUpgrade returns false`, done => { 93 + const response = jest.fn( 94 + (forwardOp: Operation): OperationResult => { 95 + return { 96 + operation: forwardOp, 97 + data: queryOneData, 98 + }; 99 + } 100 + ); 101 + 102 + const result = jest.fn(); 103 + const forward: ExchangeIO = ops$ => { 104 + return pipe(ops$, map(response)); 105 + }; 106 + 107 + const shouldUpgrade = jest.fn(() => false); 108 + pipe( 109 + requestPolicyExchange({ ...mockOptions, shouldUpgrade })({ 110 + forward, 111 + client, 112 + dispatchDebug, 113 + })(ops$), 114 + tap(result), 115 + publish 116 + ); 117 + 118 + next(op); 119 + 120 + expect(response).toHaveBeenCalledTimes(1); 121 + expect(response.mock.calls[0][0].context.requestPolicy).toEqual( 122 + 'cache-first' 123 + ); 124 + expect(result).toHaveBeenCalledTimes(1); 125 + 126 + setTimeout(() => { 127 + next(op); 128 + expect(response).toHaveBeenCalledTimes(2); 129 + expect(response.mock.calls[1][0].context.requestPolicy).toEqual( 130 + 'cache-first' 131 + ); 132 + expect(result).toHaveBeenCalledTimes(2); 133 + expect(shouldUpgrade).toBeCalledTimes(1); 134 + done(); 135 + }, 10); 136 + });
+51
exchanges/request-policy/src/requestPolicyExchange.ts
··· 1 + import { Operation, Exchange } from '@urql/core'; 2 + import { pipe, map } from 'wonka'; 3 + 4 + const defaultTTL = 5 * 60 * 1000; 5 + 6 + export interface Options { 7 + shouldUpgrade?: (op: Operation) => boolean; 8 + ttl?: number; 9 + } 10 + 11 + export const requestPolicyExchange = (options: Options): Exchange => ({ 12 + forward, 13 + }) => { 14 + const operations = new Map(); 15 + 16 + const TTL = (options || {}).ttl || defaultTTL; 17 + 18 + const processIncomingOperation = (operation: Operation): Operation => { 19 + if ( 20 + operation.context.requestPolicy !== 'cache-first' && 21 + operation.context.requestPolicy !== 'cache-only' 22 + ) 23 + return operation; 24 + if (!operations.has(operation.key)) { 25 + operations.set(operation.key, new Date()); 26 + return operation; 27 + } 28 + 29 + const lastOccurrence = operations.get(operation.key); 30 + const currentTime = new Date().getTime(); 31 + if ( 32 + currentTime - lastOccurrence.getTime() > TTL && 33 + (options.shouldUpgrade ? options.shouldUpgrade(operation) : true) 34 + ) { 35 + operations.set(operation.key, new Date()); 36 + return { 37 + ...operation, 38 + context: { 39 + ...operation.context, 40 + requestPolicy: 'cache-and-network', 41 + }, 42 + }; 43 + } 44 + 45 + return operation; 46 + }; 47 + 48 + return ops$ => { 49 + return forward(pipe(ops$, map(processIncomingOperation))); 50 + }; 51 + };
+13
exchanges/request-policy/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.json", 3 + "include": ["src"], 4 + "compilerOptions": { 5 + "baseUrl": "./", 6 + "paths": { 7 + "urql": ["../../node_modules/urql/src"], 8 + "*-urql": ["../../node_modules/*-urql/src"], 9 + "@urql/core/*": ["../../node_modules/@urql/core/src/*"], 10 + "@urql/*": ["../../node_modules/@urql/*/src"] 11 + } 12 + } 13 + }