├── .editorconfig
├── .github
└── workflows
│ ├── ci.yml
│ └── publish-latest.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── package.json
├── rollup.config.js
├── src
├── __tests__
│ ├── test-cacheAdapterEnhancer.ts
│ ├── test-retryAdapterEnhancer.ts
│ └── test-throttleAdapterEnhancer.ts
├── cacheAdapterEnhancer.ts
├── index.ts
├── retryAdapterEnhancer.ts
├── throttleAdapterEnhancer.ts
└── utils
│ ├── __tests__
│ ├── test-buildSortedURL.ts
│ └── test-isCacheLike.ts
│ ├── buildSortedURL.ts
│ └── isCacheLike.ts
├── tsconfig.esm.json
├── tsconfig.json
└── tslint.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [**]
5 | # Unix-style newlines with a newline ending every file
6 | end_of_line = lf
7 | insert_final_newline = true
8 |
9 | trim_trailing_whitespace = true
10 |
11 | charset = utf-8
12 |
13 | # use tab as indent style
14 | indent_style = tab
15 |
16 | [*.md]
17 | trim_trailing_whitespace = false
18 |
19 | [{package.json,*.babelrc,.eslintrc,.esdocrc}]
20 | indent_style = space
21 | indent_size = 2
22 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - master
8 | - next
9 | - 1.x
10 |
11 | jobs:
12 | check:
13 | runs-on: ubuntu-latest
14 |
15 | strategy:
16 | matrix:
17 | node-version: [14.x, 16.x]
18 |
19 | steps:
20 | - uses: actions/checkout@v2
21 |
22 | - name: Use Node.js ${{ matrix.node-version }}
23 | uses: actions/setup-node@v2
24 | with:
25 | node-version: ${{ matrix.node-version }}
26 |
27 | - name: Get yarn cache directory path
28 | id: yarn-cache-dir-path
29 | run: echo "::set-output name=dir::$(yarn cache dir)"
30 |
31 | - uses: actions/cache@v2
32 | id: yarn-cache
33 | with:
34 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
35 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
36 | restore-keys: |
37 | ${{ runner.os }}-yarn-
38 | - run: yarn
39 | - run: yarn ci
40 |
--------------------------------------------------------------------------------
/.github/workflows/publish-latest.yml:
--------------------------------------------------------------------------------
1 | name: Publish Latest Version
2 |
3 | on:
4 | push:
5 | tags:
6 | - v3.*
7 |
8 | jobs:
9 | publish:
10 |
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 |
16 | - uses: actions/setup-node@v2
17 | with:
18 | node-version: '14.x'
19 | check-latest: true
20 | registry-url: 'https://registry.npmjs.org'
21 |
22 | - run: yarn
23 | - run: yarn publish --tag latest
24 | env:
25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .DS_Store
3 | .nyc_output/
4 | coverage.lcov
5 | coverage/
6 | node_modules/
7 | dist/
8 | esm/
9 | lib/
10 | yarn.lock
11 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | __tests__
2 | *.js.map
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 axios-extensions
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # axios-extensions
2 |
3 | [](https://www.npmjs.com/package/axios-extensions)
4 | [](https://codecov.io/gh/kuitos/axios-extensions)
5 | [](https://www.npmjs.com/package/axios-extensions)
6 | [](https://github.com/kuitos/axios-extensions/actions/workflows/ci.yml)
7 |
8 | A non-invasive, simple, reliable collection of axios extension
9 |
10 | ## Extension List
11 | *v3.x has a lot of api changes, if you are looking for v2.x doc, see [here](https://github.com/kuitos/axios-extensions/tree/v2.0.3)*
12 |
13 | *Not working with axios v0.19.0 as its custom config bug, See https://github.com/axios/axios/pull/2207.*
14 |
15 | * [cacheAdapterEnhancer](#cacheadapterenhancer) makes request cacheable
16 | * [throttleAdapterEnhancer](#throttleadapterenhancer) makes GET requests throttled automatically
17 | * [retryAdapterEnhancer](#retryadapterenhancer) makes request retry with special times while it failed
18 |
19 | ## Installing
20 | ```bash
21 | npm i axios-extensions -S
22 | ```
23 | or
24 | ```bash
25 | yarn add axios-extensions
26 | ```
27 |
28 | or
29 | ```html
30 | // exposed as window['axios-extensions']
31 |
32 | ```
33 |
34 | ## Usage
35 |
36 | ```javascript
37 | import axios from 'axios';
38 | import { cacheAdapterEnhancer, throttleAdapterEnhancer } from 'axios-extensions';
39 |
40 | // enhance the original axios adapter with throttle and cache enhancer
41 | const http = axios.create({
42 | baseURL: '/',
43 | headers: { 'Cache-Control': 'no-cache' },
44 | adapter: throttleAdapterEnhancer(cacheAdapterEnhancer(axios.defaults.adapter))
45 | });
46 | ```
47 |
48 | ### Enable Logging
49 |
50 | It is highly recommended to enable the request logging recorder in development environment(disabled by default).
51 |
52 | #### browser (webpack)
53 | ```js
54 | new webpack.DefinePlugin({
55 | 'process.env.LOGGER_LEVEL': JSON.stringify('info')
56 | })
57 | ```
58 | #### node
59 | ```json
60 | // package.json
61 | "scripts": {
62 | "start": "cross-env LOGGER_LEVEL=info node server.js"
63 | }
64 | ```
65 |
66 | ## API
67 |
68 | ### cacheAdapterEnhancer
69 |
70 | > Makes axios cacheable
71 |
72 | ```typescript
73 | cacheAdapterEnhancer(adapter: AxiosAdapter, options: Options): AxiosAdapter
74 | ```
75 |
76 | Where `adapter` is an axios adapter which following the [axios adapter standard](https://github.com/axios/axios/blob/master/lib/adapters/README.md), `options` is an optional that configuring caching:
77 |
78 | | Param | Type | Default value | Description |
79 | | ---------------- | ---------------------------------------- | ------------------------------------------------------------ | ---- |
80 | | enabledByDefault | boolean | true | Enables cache for all requests without explicit definition in request config (e.g. `cache: true`) |
81 | | cacheFlag | string | 'cache' | Configures key (flag) for explicit definition of cache usage in axios request |
82 | | defaultCache | CacheLike |
new LRUCache({ maxAge: FIVE_MINUTES, max: 100 })
| a CacheLike instance that will be used for storing requests by default, except you define a custom Cache with your request config |
83 |
84 | `cacheAdapterEnhancer` enhances the given adapter and returns a new cacheable adapter back, so you can compose it with any other enhancers, e.g. `throttleAdapterEnhancer`.
85 |
86 | #### basic usage
87 |
88 | ```javascript
89 | import axios from 'axios';
90 | import { cacheAdapterEnhancer } from 'axios-extensions';
91 |
92 | const http = axios.create({
93 | baseURL: '/',
94 | headers: { 'Cache-Control': 'no-cache' },
95 | // cache will be enabled by default
96 | adapter: cacheAdapterEnhancer(axios.defaults.adapter)
97 | });
98 |
99 | http.get('/users'); // make real http request
100 | http.get('/users'); // use the response from the cache of previous request, without real http request made
101 | http.get('/users', { cache: false }); // disable cache manually and the the real http request invoked
102 | ```
103 |
104 | #### custom cache flag
105 |
106 | ```javascript
107 | const http = axios.create({
108 | baseURL: '/',
109 | headers: { 'Cache-Control': 'no-cache' },
110 | // disable the default cache and set the cache flag
111 | adapter: cacheAdapterEnhancer(axios.defaults.adapter, { enabledByDefault: false, cacheFlag: 'useCache'})
112 | });
113 |
114 | http.get('/users'); // default cache was disabled and then the real http request invoked
115 | http.get('/users', { useCache: true }); // make the request cacheable(real http request made due to first request invoke)
116 | http.get('/users', { useCache: true }); // use the response cache from previous request
117 | ```
118 |
119 | ##### custom cache typing
120 |
121 | Note that if you are using custom cache flag and typescript, you may need to add the typing declaration like below:
122 |
123 | ```ts
124 | import { ICacheLike } from 'axios-extensions';
125 | declare module 'axios' {
126 | interface AxiosRequestConfig {
127 | // if your cacheFlag was setting to 'useCache'
128 | useCache?: boolean | ICacheLike;
129 | }
130 | }
131 | ```
132 |
133 | #### more advanced
134 |
135 | Besides configuring the request through the `cacheAdapterEnhancer`, we can enjoy more advanced features via configuring every individual request.
136 |
137 | ```js
138 | import axios from 'axios';
139 | import { cacheAdapterEnhancer, Cache } from 'axios-extensions';
140 |
141 | const http = axios.create({
142 | baseURL: '/',
143 | headers: { 'Cache-Control': 'no-cache' },
144 | // disable the default cache
145 | adapter: cacheAdapterEnhancer(axios.defaults.adapter, { enabledByDefault: false })
146 | });
147 |
148 | http.get('/users', { cache: true }); // make the request cacheable(real http request made due to first request invoke)
149 |
150 | // define a cache manually
151 | const cacheA = new Cache();
152 | // or a cache-like instance
153 | const cacheB = { get() {/*...*/}, set() {/*...*/}, del() {/*...*/} };
154 |
155 | // two actual request will be made due to the different cache
156 | http.get('/users', { cache: cacheA });
157 | http.get('/users', { cache: cacheB });
158 |
159 | // a actual request made and cached due to force update configured
160 | http.get('/users', { cache: cacheA, forceUpdate: true });
161 | ```
162 |
163 | *Note: If you are using typescript, do not forget to enable `"esModuleInterop": true` and `"allowSyntheticDefaultImports": true` for better development experience.*
164 |
165 | ### throttleAdapterEnhancer
166 |
167 | > Throttle GET requests most once per threshold milliseconds
168 |
169 | ```ts
170 | throttleAdapterEnhancer(adapter: AxiosAdapter, options: Options): AxiosAdapter
171 | ```
172 |
173 | Where `adapter` is an axios adapter which following the [axios adapter standard](https://github.com/axios/axios/blob/master/lib/adapters/README.md), `options` is an optional object that configuring throttling:
174 |
175 | | Param | Type |Default value | Description |
176 | | --------- | ---- |--------------------------- | ------------------------------------------------------------ |
177 | | threshold | number |1000 | The number of milliseconds to throttle request invocations to |
178 | | cache | CacheLike |new LRUCache({ max: 10 })
| CacheLike instance that will be used for storing throttled requests |
179 |
180 | Basically we recommend using the `throttleAdapterEnhancer` with `cacheAdapterEnhancer` together for the maximum caching benefits.
181 | Note that POST and other methods besides GET are not affected.
182 |
183 | ```js
184 | throttleAdapterEnhancer(cacheAdapterEnhancer(axios.defaults.adapter))
185 | ```
186 |
187 | Check [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) to learn more details about throttle and how it differs from debounce.
188 |
189 | #### basic usage
190 |
191 | ```js
192 | import axios from 'axios';
193 | import { throttleAdapterEnhancer } from 'axios-extensions';
194 |
195 | const http = axios.create({
196 | baseURL: '/',
197 | headers: { 'Cache-Control': 'no-cache' },
198 | adapter: throttleAdapterEnhancer(axios.defaults.adapter, { threshold: 2 * 1000 })
199 | });
200 |
201 | http.get('/users'); // make real http request
202 | http.get('/users'); // responsed from the cache
203 | http.get('/users'); // responsed from the cache
204 |
205 | setTimeout(() => {
206 | http.get('/users'); // after 2s, the real request makes again
207 | }, 2 * 1000);
208 | ```
209 |
210 | ### retryAdapterEnhancer
211 |
212 | > Retry the failed request with special times
213 |
214 | ```ts
215 | retryAdapterEnhancer(adapter: AxiosAdapter, options: Options): AxiosAdapter
216 | ```
217 |
218 | Where `adapter` is an axios adapter which following the [axios adapter standard](https://github.com/axios/axios/blob/master/lib/adapters/README.md), `options` is an optional that configuring caching:
219 | | Param | Type | Default value | Description |
220 | | ---------------- | ---------------------------------------- | ------------------------------------------------------------ | ---- |
221 | | times | number | 2 | Set the retry times for failed request globally. |
222 |
223 | #### basic usage
224 |
225 | ```ts
226 | import axios from 'axios';
227 | import { retryAdapterEnhancer } from 'axios-extensions';
228 |
229 | const http = axios.create({
230 | baseURL: '/',
231 | headers: { 'Cache-Control': 'no-cache' },
232 | adapter: retryAdapterEnhancer(axios.defaults.adapter)
233 | });
234 |
235 | // this request will retry two times if it failed
236 | http.get('/users');
237 |
238 | // you could also set the retry times for a special request
239 | http.get('/special', { retryTimes: 3 });
240 | ```
241 |
242 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "axios-extensions",
3 | "version": "3.1.7",
4 | "description": "make axios great again",
5 | "homepage": "https://github.com/kuitos/axios-extensions",
6 | "repository": "https://github.com/kuitos/axios-extensions.git",
7 | "license": "MIT",
8 | "author": "kuitos",
9 | "keywords": [
10 | "axios",
11 | "cache",
12 | "extensions",
13 | "adapter"
14 | ],
15 | "main": "./lib/index.js",
16 | "module": "./esm/index.js",
17 | "types": "./esm/index.d.ts",
18 | "sideEffects": false,
19 | "scripts": {
20 | "build": "rm -fr dist && npm run tsc && npm run dist",
21 | "tsc": "npm run tsc:cjs & npm run tsc:esm",
22 | "tsc:cjs": "rm -fr lib && tsc",
23 | "tsc:esm": "rm -fr esm && tsc -p tsconfig.esm.json",
24 | "dist": "rm -fr dist && rollup -c rollup.config.js",
25 | "start": "tsc -w && ava -w",
26 | "prepush": "npm run lint",
27 | "prepublishOnly": "npm run build",
28 | "release": "np --no-cleanup --yolo --no-publish",
29 | "test": "npm run tsc:cjs && nyc ava -v",
30 | "test:pure": "tsc && ava",
31 | "report": "nyc report --reporter=html",
32 | "codecov": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
33 | "coverageCheck": "nyc --check-coverage --lines 80 --functions 80 --branches 80 npm run test:pure",
34 | "lint": "tslint 'src/**/*.ts' & npm run coverageCheck",
35 | "ci": "npm run lint && npm run codecov"
36 | },
37 | "files": [
38 | "esm",
39 | "lib",
40 | "dist",
41 | "src"
42 | ],
43 | "dependencies": {
44 | "lru-cache": "^7.14.0",
45 | "tslib": "^2.1.0",
46 | "util": "^0.12.3"
47 | },
48 | "peerDependencies": {
49 | "axios": "*"
50 | },
51 | "devDependencies": {
52 | "@types/node": "^14.14.22",
53 | "@types/sinon": "^9.0.10",
54 | "ava": "^3.15.0",
55 | "axios": ">=0.21.1",
56 | "codecov": "^3.8.1",
57 | "husky": "^4.3.8",
58 | "np": "^7.2.0",
59 | "nyc": "^15.1.0",
60 | "rollup": "^2.37.1",
61 | "rollup-plugin-commonjs": "^10.0.0",
62 | "rollup-plugin-node-builtins": "^2.1.2",
63 | "rollup-plugin-node-resolve": "^5.0.0",
64 | "rollup-plugin-terser": "^7.0.2",
65 | "rollup-plugin-uglify": "^6.0.4",
66 | "sinon": "^9.2.3",
67 | "tslint": "^5.20.1",
68 | "tslint-eslint-rules": "^5.4.0",
69 | "typescript": "^4.1.3"
70 | },
71 | "ava": {
72 | "files": [
73 | "lib/**/__tests__/**/*.js"
74 | ]
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2019-05-27
4 | */
5 |
6 | const resolve = require('rollup-plugin-node-resolve');
7 | const commonjs = require('rollup-plugin-commonjs');
8 | const builtin = require('rollup-plugin-node-builtins');
9 | const terser = require('rollup-plugin-terser').terser;
10 |
11 | function genConfig(minimize = false) {
12 |
13 | return {
14 | input: './esm/index.js',
15 | output: {
16 | name: 'axios-extensions',
17 | file: minimize ? './dist/axios-extensions.min.js' : './dist/axios-extensions.js',
18 | format: 'umd',
19 | sourcemap: true,
20 | },
21 | external: ['axios'],
22 | plugins: [
23 | resolve(),
24 | commonjs(),
25 | builtin(),
26 | minimize ? terser() : void 0,
27 | ],
28 | };
29 | }
30 |
31 | module.exports = [
32 | genConfig(),
33 | genConfig(true),
34 | ];
35 |
--------------------------------------------------------------------------------
/src/__tests__/test-cacheAdapterEnhancer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @homepage https://github.com/kuitos/
4 | * @since 2017-10-16
5 | */
6 |
7 | import test from 'ava';
8 | import axios, { AxiosPromise } from 'axios';
9 | import LRUCache from 'lru-cache';
10 | import { spy } from 'sinon';
11 |
12 | import cacheAdapterEnhancer from '../cacheAdapterEnhancer';
13 |
14 | // mock the actual request
15 | const genMockAdapter = (cb: any) => (config: any) => {
16 | cb();
17 | if (config.error) {
18 | return Promise.reject(config);
19 | }
20 | return Promise.resolve(config);
21 | };
22 |
23 | test('cache adapter should cache request without noCacheFlag', async t => {
24 |
25 | const adapterCb = spy();
26 | const mockedAdapter = genMockAdapter(adapterCb);
27 | const http = axios.create({
28 | adapter: cacheAdapterEnhancer(mockedAdapter, { enabledByDefault: true }),
29 | });
30 |
31 | const onSuccess = spy();
32 | const promises = [];
33 |
34 | for (let i = 0; i < 5; i++) {
35 | promises.push(http.get('/users').then(onSuccess));
36 | }
37 |
38 | await Promise.all(promises);
39 |
40 | t.is(onSuccess.callCount, 5);
41 | t.is(adapterCb.callCount, 1);
42 | t.is(adapterCb.calledBefore(onSuccess), true);
43 |
44 | await http.get('/users', { params: { name: 'kuitos' } }).then(onSuccess);
45 | t.is(onSuccess.callCount, 6);
46 | t.is(adapterCb.callCount, 2);
47 |
48 | await http.get('/users', { params: { name: 'kuitos' }, cache: true } as any).then(onSuccess);
49 | t.is(onSuccess.callCount, 7);
50 | t.is(adapterCb.callCount, 2);
51 |
52 | });
53 |
54 | test('cache adapter shouldn\'t cache request with noCacheFlag', async t => {
55 |
56 | const adapterCb = spy();
57 | const mockedAdapter = genMockAdapter(adapterCb);
58 | const http = axios.create({
59 | adapter: cacheAdapterEnhancer(mockedAdapter, { enabledByDefault: true, cacheFlag: 'cache' }),
60 | });
61 |
62 | const onSuccess = spy();
63 | await Promise.all([
64 | http.get('/users', { cache: false } as any).then(onSuccess),
65 | http.get('/users', { cache: false } as any).then(onSuccess),
66 | ]);
67 | t.is(onSuccess.callCount, 2);
68 | t.is(adapterCb.callCount, 2);
69 |
70 | });
71 |
72 | test('cache will be removed when request error', async t => {
73 |
74 | const adapterCb = spy();
75 | const mockedAdapter = genMockAdapter(adapterCb);
76 | const http = axios.create({
77 | adapter: cacheAdapterEnhancer(mockedAdapter, { enabledByDefault: true }),
78 | });
79 |
80 | const onSuccess = spy();
81 | const onError = spy();
82 | await Promise.all([
83 | http.get('/users', { error: true } as any).then(onSuccess, onError),
84 | http.get('/users').then(onSuccess, onError),
85 | ]);
86 | // as the previous uses invocation failed, the following users request will respond with the rejected promise
87 | t.is(onSuccess.callCount, 0);
88 | t.is(onError.callCount, 2);
89 | t.is(adapterCb.callCount, 1);
90 |
91 | await Promise.all([
92 | http.get('/users').then(onSuccess, onError),
93 | http.get('/users').then(onSuccess, onError),
94 | ]);
95 | t.is(onSuccess.callCount, 2);
96 | t.is(adapterCb.callCount, 2);
97 |
98 | });
99 |
100 | test('disable default cache switcher', async t => {
101 |
102 | const adapterCb = spy();
103 | const mockedAdapter = genMockAdapter(adapterCb);
104 | const http = axios.create({
105 | adapter: cacheAdapterEnhancer(mockedAdapter),
106 | });
107 |
108 | const onSuccess = spy();
109 | await Promise.all([
110 | http.get('/users').then(onSuccess),
111 | http.get('/users').then(onSuccess),
112 | http.get('/users', { cache: false } as any).then(onSuccess),
113 | ]);
114 | t.is(onSuccess.callCount, 3);
115 | t.is(adapterCb.callCount, 2);
116 |
117 | });
118 |
119 | test('request will refresh the cache with forceUpdate config', async t => {
120 |
121 | const adapterCb = spy();
122 | const mockedAdapter = genMockAdapter(adapterCb);
123 | const cache = new LRUCache({ max: 100 });
124 | const http = axios.create({
125 | adapter: cacheAdapterEnhancer(mockedAdapter, { enabledByDefault: true, cacheFlag: 'cache', defaultCache: cache }),
126 | });
127 |
128 | const onSuccess = spy();
129 | await http.get('/users').then(onSuccess);
130 | const responed1 = await cache.get('/users') as any;
131 | await http.get('/users', { forceUpdate: true } as any).then(onSuccess);
132 | const responed2 = await cache.get('/users') as any;
133 | t.is(adapterCb.callCount, 2);
134 | t.is(onSuccess.callCount, 2);
135 |
136 | if (responed1) {
137 | t.is(responed1.url, '/users');
138 | t.is(responed1.url, responed2.url);
139 | }
140 |
141 | t.not(responed1, responed2);
142 |
143 | });
144 |
145 | test('use a custom cache with request individual config', async t => {
146 |
147 | const adapterCb = spy();
148 | const mockedAdapter = genMockAdapter(adapterCb);
149 | const http = axios.create({
150 | adapter: cacheAdapterEnhancer(mockedAdapter),
151 | });
152 |
153 | const cache1 = new LRUCache({ max: 100 });
154 | const cache2 = new LRUCache({ max: 100 });
155 | await Promise.all([http.get('/users', { cache: cache1 } as any), http.get('/users', { cache: cache2 } as any)]);
156 | t.is(adapterCb.callCount, 2);
157 |
158 | cache2.clear();
159 | await Promise.all([http.get('/users', { cache: cache1 } as any), http.get('/users', { cache: cache2 } as any)]);
160 |
161 | t.is(adapterCb.callCount, 3);
162 | });
163 |
--------------------------------------------------------------------------------
/src/__tests__/test-retryAdapterEnhancer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2020-02-18
4 | */
5 |
6 | import test from 'ava';
7 | import axios from 'axios';
8 | import { spy } from 'sinon';
9 | import retryAdapterEnhancer from '../retryAdapterEnhancer';
10 |
11 | test('should retry the request with special times while request failed', async (t) => {
12 |
13 | const times = 3;
14 | const spyFn = spy();
15 | const mockedAdapter = (config: any) => {
16 | spyFn();
17 | if (spyFn.callCount === times + 1) {
18 | return Promise.resolve(config);
19 | }
20 | return Promise.reject(config);
21 | };
22 | const http = axios.create({
23 | adapter: retryAdapterEnhancer(mockedAdapter, { times }),
24 | });
25 |
26 | await http.get('/test');
27 |
28 | t.is(spyFn.callCount, times + 1);
29 | });
30 |
31 | test('should return the result immediately while the request succeed', async (t) => {
32 | const spyFn = spy();
33 | const mockedAdapter = (config: any) => {
34 | spyFn();
35 | if (spyFn.calledTwice) {
36 | return Promise.resolve(config);
37 | }
38 |
39 | return Promise.reject(config);
40 | };
41 | const http = axios.create({
42 | adapter: retryAdapterEnhancer(mockedAdapter),
43 | });
44 |
45 | await http.get('/test');
46 |
47 | t.truthy(spyFn.calledTwice);
48 | });
49 |
50 | test('should throw an exception while request still failed after retry', async (t) => {
51 |
52 | const defaultTimes = 2;
53 | const spyFn = spy();
54 | const mockedAdapter = (config: any) => {
55 | spyFn();
56 | return Promise.reject(config);
57 | };
58 | const http = axios.create({
59 | adapter: retryAdapterEnhancer(mockedAdapter),
60 | });
61 |
62 | try {
63 | await http.get('/test');
64 | } catch (e: any) {
65 | t.is(e.url, '/test');
66 | t.is(spyFn.callCount, defaultTimes + 1);
67 | }
68 | });
69 |
70 | test('should retry with special times for the custom config request', async (t) => {
71 |
72 | const spyFn = spy();
73 | const mockedAdapter = (config: any) => {
74 | spyFn();
75 | return Promise.reject(config);
76 | };
77 | const http = axios.create({
78 | adapter: retryAdapterEnhancer(mockedAdapter, { times: 2 }),
79 | });
80 |
81 | const customRetryTimes = 4;
82 | try {
83 | await http.get('/test', { retryTimes: customRetryTimes });
84 | } catch (e: any) {
85 | t.is(e.url, '/test');
86 | t.is(spyFn.callCount, customRetryTimes + 1);
87 | }
88 | });
89 |
--------------------------------------------------------------------------------
/src/__tests__/test-throttleAdapterEnhancer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @homepage https://github.com/kuitos/
4 | * @since 2017-10-16
5 | */
6 |
7 | import test from 'ava';
8 | import axios from 'axios';
9 | import LRUCache from 'lru-cache';
10 | import { spy } from 'sinon';
11 |
12 | import throttleAdapterEnhancer, { RecordedCache } from '../throttleAdapterEnhancer';
13 |
14 | const genMockAdapter = (cb: any) => (config: any) => {
15 | cb();
16 | if (config.error) {
17 | return Promise.reject(config);
18 | }
19 | return Promise.resolve(config);
20 | };
21 |
22 | test('throttle adapter should cache request in a threshold seconds', async t => {
23 |
24 | const threshold = 1000;
25 | const adapterCb = spy();
26 | const mockedAdapter = genMockAdapter(adapterCb);
27 | const http = axios.create({
28 | adapter: throttleAdapterEnhancer(mockedAdapter, { threshold }),
29 | });
30 |
31 | const onSuccess = spy();
32 | const promises = [];
33 |
34 | const start = Date.now();
35 | for (let i = 0; i < 5; i++) {
36 | promises.push(http.get('/users').then(onSuccess));
37 | }
38 |
39 | await Promise.all(promises);
40 | const end = Date.now();
41 | t.is(onSuccess.callCount, 5);
42 | t.is(adapterCb.callCount, 1);
43 | t.is(adapterCb.calledBefore(onSuccess), true);
44 | t.is(end - start < threshold, true);
45 |
46 | await new Promise(r => setTimeout(r, threshold));
47 | await Promise.all([
48 | http.get('/users').then(onSuccess),
49 | http.get('/users').then(onSuccess),
50 | ]);
51 | t.is(onSuccess.callCount, 7);
52 | t.is(adapterCb.callCount, 2);
53 |
54 | });
55 |
56 | test('throttle adapter shouldn`t do anything when a non-get request invoked', async t => {
57 |
58 | const adapterCb = spy();
59 | const mockedAdapter = genMockAdapter(adapterCb);
60 | const http = axios.create({
61 | adapter: throttleAdapterEnhancer(mockedAdapter),
62 | });
63 |
64 | const onSuccess = spy();
65 | await Promise.all([
66 | http.post('/users').then(onSuccess),
67 | http.post('/users').then(onSuccess),
68 | ]);
69 | t.is(onSuccess.callCount, 2);
70 | t.is(adapterCb.callCount, 2);
71 |
72 | });
73 |
74 | test('cache will be removed when request error', async t => {
75 |
76 | const adapterCb = spy();
77 | const mockedAdapter = genMockAdapter(adapterCb);
78 | const http = axios.create({
79 | adapter: throttleAdapterEnhancer(mockedAdapter),
80 | });
81 |
82 | const onSuccess = spy();
83 | const onError = spy();
84 | await Promise.all([
85 | http.get('/users', { error: true } as any).then(onSuccess, onError),
86 | http.get('/users').then(onSuccess, onError),
87 | ]);
88 | t.is(onSuccess.callCount, 0);
89 | t.is(onError.callCount, 2);
90 | t.is(adapterCb.callCount, 1);
91 |
92 | await Promise.all([
93 | http.get('/users').then(onSuccess, onError),
94 | http.get('/users').then(onSuccess, onError),
95 | ]);
96 | t.is(onSuccess.callCount, 2);
97 | t.is(adapterCb.callCount, 2);
98 |
99 | });
100 |
101 | test('use a custom cache for throttle enhancer', async t => {
102 |
103 | const adapterCb = spy();
104 | const mockedAdapter = genMockAdapter(adapterCb);
105 | const cache = new LRUCache({ max: 100 });
106 | const http = axios.create({
107 | adapter: throttleAdapterEnhancer(mockedAdapter, { cache }),
108 | });
109 |
110 | const onSuccess = spy();
111 | await Promise.all([
112 | http.get('/users').then(onSuccess),
113 | http.get('/users').then(onSuccess),
114 | ]);
115 | t.is(onSuccess.callCount, 2);
116 | t.is(adapterCb.callCount, 1);
117 |
118 | cache.delete('/users');
119 | await Promise.all([
120 | http.get('/users').then(onSuccess),
121 | http.get('/users').then(onSuccess),
122 | ]);
123 | t.is(onSuccess.callCount, 4);
124 | t.is(adapterCb.callCount, 2);
125 | });
126 |
--------------------------------------------------------------------------------
/src/cacheAdapterEnhancer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @homepage https://github.com/kuitos/
4 | * @since 2017-10-12
5 | */
6 |
7 | import { AxiosAdapter, AxiosPromise } from 'axios';
8 | import LRUCache from 'lru-cache';
9 | import buildSortedURL from './utils/buildSortedURL';
10 | import isCacheLike, { ICacheLike } from './utils/isCacheLike';
11 |
12 | declare module 'axios' {
13 | interface AxiosRequestConfig {
14 | forceUpdate?: boolean;
15 | cache?: boolean | ICacheLike;
16 | }
17 | }
18 |
19 | const FIVE_MINUTES = 1000 * 60 * 5;
20 | const CAPACITY = 100;
21 |
22 | export type Options = {
23 | enabledByDefault?: boolean,
24 | cacheFlag?: string,
25 | defaultCache?: ICacheLike,
26 | };
27 |
28 | export default function cacheAdapterEnhancer(adapter: AxiosAdapter, options: Options = {}): AxiosAdapter {
29 |
30 | const {
31 | enabledByDefault = true,
32 | cacheFlag = 'cache',
33 | defaultCache = new LRUCache({ ttl: FIVE_MINUTES, max: CAPACITY }),
34 | } = options;
35 |
36 | return config => {
37 |
38 | const { url, method, params, paramsSerializer, forceUpdate } = config;
39 | const useCache = ((config as any)[cacheFlag] !== void 0 && (config as any)[cacheFlag] !== null)
40 | ? (config as any)[cacheFlag]
41 | : enabledByDefault;
42 |
43 | if (method === 'get' && useCache) {
44 |
45 | // if had provided a specified cache, then use it instead
46 | const cache: ICacheLike = isCacheLike(useCache) ? useCache : defaultCache;
47 |
48 | // build the index according to the url and params
49 | const index = buildSortedURL(url, params, paramsSerializer);
50 |
51 | let responsePromise = cache.get(index);
52 |
53 | if (!responsePromise || forceUpdate) {
54 |
55 | responsePromise = (async () => {
56 |
57 | try {
58 | return await adapter(config);
59 | } catch (reason) {
60 | 'delete' in cache ? cache.delete(index) : cache.del(index);
61 | throw reason;
62 | }
63 |
64 | })();
65 |
66 | // put the promise for the non-transformed response into cache as a placeholder
67 | cache.set(index, responsePromise);
68 |
69 | return responsePromise;
70 | }
71 |
72 | /* istanbul ignore next */
73 | if (process.env.LOGGER_LEVEL === 'info') {
74 | // eslint-disable-next-line no-console
75 | console.info(`[axios-extensions] request cached by cache adapter --> url: ${index}`);
76 | }
77 |
78 | return responsePromise;
79 | }
80 |
81 | return adapter(config);
82 | };
83 | }
84 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @homepage https://github.com/kuitos/
4 | * @since 2017-09-28
5 | */
6 |
7 | import Cache from 'lru-cache';
8 | import cacheAdapterEnhancer from './cacheAdapterEnhancer';
9 | import retryAdapterEnhancer from './retryAdapterEnhancer';
10 | import throttleAdapterEnhancer from './throttleAdapterEnhancer';
11 | import { ICacheLike } from './utils/isCacheLike';
12 |
13 | export {
14 | Cache,
15 | ICacheLike,
16 | cacheAdapterEnhancer,
17 | throttleAdapterEnhancer,
18 | retryAdapterEnhancer,
19 | };
20 |
--------------------------------------------------------------------------------
/src/retryAdapterEnhancer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @since 2020-02-18
4 | */
5 |
6 | import { AxiosAdapter, AxiosResponse } from 'axios';
7 |
8 | declare module 'axios' {
9 | interface AxiosRequestConfig {
10 | retryTimes?: number;
11 | }
12 | }
13 |
14 | export type Options = {
15 | times?: number;
16 | };
17 |
18 | export default function retryAdapterEnhancer(adapter: AxiosAdapter, options: Options = {}): AxiosAdapter {
19 |
20 | const { times = 2 } = options;
21 |
22 | return async config => {
23 |
24 | const { retryTimes = times } = config;
25 |
26 | let timeUp = false;
27 | let count = 0;
28 | const request = async (): Promise => {
29 | try {
30 | return await adapter(config);
31 | } catch (e) {
32 | timeUp = retryTimes === count;
33 | if (timeUp) {
34 | throw e;
35 | }
36 |
37 | count++;
38 |
39 | /* istanbul ignore next */
40 | if (process.env.LOGGER_LEVEL === 'info') {
41 | console.info(`[axios-extensions] request start retrying --> url: ${config.url} , time: ${count}`);
42 | }
43 |
44 | return request();
45 | }
46 | };
47 |
48 | return request();
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/src/throttleAdapterEnhancer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @homepage https://github.com/kuitos/
4 | * @since 2017-10-11
5 | */
6 |
7 | import { AxiosAdapter, AxiosPromise, InternalAxiosRequestConfig } from 'axios';
8 | import LRUCache from 'lru-cache';
9 | import buildSortedURL from './utils/buildSortedURL';
10 | import { ICacheLike } from './utils/isCacheLike';
11 |
12 | export type RecordedCache = {
13 | timestamp: number;
14 | value?: AxiosPromise;
15 | };
16 |
17 | export type Options = {
18 | threshold?: number,
19 | cache?: ICacheLike,
20 | };
21 |
22 | export default function throttleAdapterEnhancer(adapter: AxiosAdapter, options: Options = {}): AxiosAdapter {
23 |
24 | const { threshold = 1000, cache = new LRUCache({ max: 10 }) } = options;
25 |
26 | const recordCacheWithRequest = (index: string, config: InternalAxiosRequestConfig) => {
27 |
28 | const responsePromise = (async () => {
29 |
30 | try {
31 | const response = await adapter(config);
32 |
33 | cache.set(index, {
34 | timestamp: Date.now(),
35 | value: Promise.resolve(response),
36 | });
37 |
38 | return response;
39 | } catch (reason) {
40 | 'delete' in cache ? cache.delete(index) : cache.del(index);
41 | throw reason;
42 | }
43 |
44 | })();
45 |
46 | cache.set(index, {
47 | timestamp: Date.now(),
48 | value: responsePromise,
49 | });
50 |
51 | return responsePromise;
52 | };
53 |
54 | return config => {
55 |
56 | const { url, method, params, paramsSerializer } = config;
57 | const index = buildSortedURL(url, params, paramsSerializer);
58 |
59 | const now = Date.now();
60 | const cachedRecord = cache.get(index) || { timestamp: now };
61 |
62 | if (method === 'get') {
63 |
64 | if (now - cachedRecord.timestamp <= threshold) {
65 |
66 | const responsePromise = cachedRecord.value;
67 | if (responsePromise) {
68 |
69 | /* istanbul ignore next */
70 | if (process.env.LOGGER_LEVEL === 'info') {
71 | // eslint-disable-next-line no-console
72 | console.info(`[axios-extensions] request cached by throttle adapter --> url: ${index}`);
73 | }
74 |
75 | return responsePromise;
76 | }
77 | }
78 |
79 | return recordCacheWithRequest(index, config);
80 | }
81 |
82 | return adapter(config);
83 | };
84 | }
85 |
--------------------------------------------------------------------------------
/src/utils/__tests__/test-buildSortedURL.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @homepage https://github.com/kuitos/
4 | * @since 2017-10-17
5 | */
6 |
7 | import test from 'ava';
8 | import buildSortedURL from '../buildSortedURL';
9 |
10 | test('build a simple url without params', t => {
11 |
12 | const url = '//cross-domain.test/users';
13 | const params = {};
14 |
15 | const builtUrl = buildSortedURL(url, params);
16 | t.is(builtUrl, `${url}`);
17 | });
18 |
19 | test('build a simple url with params', t => {
20 |
21 | const url = '//cross-domain.test/users';
22 | const params = { name: 'kuitos', age: 18 };
23 |
24 | const builtUrl = buildSortedURL(url, params);
25 | t.is(builtUrl, `${url}?age=18&name=kuitos`);
26 | });
27 |
28 | test('build a url which already had a query string with params', t => {
29 |
30 | const url = '//cross-domain.test/users?title=genius';
31 | const params = { name: 'kuitos', age: 18 };
32 |
33 | const builtUrl = buildSortedURL(url, params);
34 | t.is(builtUrl, '//cross-domain.test/users?age=18&name=kuitos&title=genius');
35 | });
36 |
--------------------------------------------------------------------------------
/src/utils/__tests__/test-isCacheLike.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by Kuitos
3 | * @homepage https://github.com/kuitos/
4 | * @since 2018/3/19 下午11:22
5 | */
6 |
7 | import test from 'ava';
8 | import isCacheLike from '../isCacheLike';
9 |
10 | test('a object with specified method will be regard as cache', t => {
11 |
12 | let cache = {};
13 | t.is(isCacheLike(cache), false);
14 |
15 | cache = {
16 |
17 | // tslint:disable-next-line
18 | get() {
19 | },
20 | // tslint:disable-next-line
21 | set() {
22 | },
23 | // tslint:disable-next-line
24 | del() {
25 | },
26 | };
27 | t.is(isCacheLike(cache), true);
28 | });
29 |
--------------------------------------------------------------------------------
/src/utils/buildSortedURL.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @homepage https://github.com/kuitos/
4 | * @since 2017-10-12
5 | */
6 |
7 | import axios from 'axios';
8 |
9 | export default function buildSortedURL(...args: any[]) {
10 | const [url, params, paramsSerializer] = args;
11 | const builtURL = axios.getUri({ url, params, paramsSerializer });
12 |
13 | const [urlPath, queryString] = builtURL.split('?');
14 |
15 | if (queryString) {
16 | const paramsPair = queryString.split('&');
17 | return `${urlPath}?${paramsPair.sort().join('&')}`;
18 | }
19 |
20 | return builtURL;
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/isCacheLike.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Kuitos
3 | * @homepage https://github.com/kuitos/
4 | * @since 2018-03-19
5 | */
6 | export type ICacheLike = {
7 | get(key: string): T | undefined;
8 | set(key: string, value: T): void;
9 | } & ({ del(key: string): void } | { delete(key: string): void });
10 |
11 | export default function isCacheLike(cache: any): cache is ICacheLike {
12 | return typeof cache.get === 'function' && typeof cache.set === 'function' && (typeof cache.delete === 'function' || typeof cache.del === 'function');
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.esm.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "compilerOptions": {
4 | "module": "es2015",
5 | "outDir": "esm",
6 | "moduleResolution": "node"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "es5",
5 | /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
6 | "module": "commonjs",
7 | /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
8 | "lib": [
9 | "es2015"
10 | ],
11 | /* Specify library files to be included in the compilation: */
12 | // "allowJs": true, /* Allow javascript files to be compiled. */
13 | // "checkJs": true, /* Report errors in .js files. */
14 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
15 | "declaration": true,
16 | /* Generates corresponding '.d.ts' file. */
17 | "sourceMap": true,
18 | /* Generates corresponding '.map' file. */
19 | // "outFile": "./", /* Concatenate and emit output to single file. */
20 | "outDir": "lib",
21 | /* Redirect output structure to the directory. */
22 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
23 | // "removeComments": true, /* Do not emit comments to output. */
24 | // "noEmit": true, /* Do not emit outputs. */
25 | "importHelpers": true, /* Import emit helpers from 'tslib'. */
26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
28 |
29 | /* Strict Type-Checking Options */
30 | "strict": true,
31 | /* Enable all strict type-checking options. */
32 | "noImplicitAny": true,
33 | /* Raise error on expressions and declarations with an implied 'any' type. */
34 | "strictNullChecks": true,
35 | /* Enable strict null checks. */
36 | "noImplicitThis": true,
37 | /* Raise error on 'this' expressions with an implied 'any' type. */
38 | "alwaysStrict": true,
39 | /* Parse in strict mode and emit "use strict" for each source file. */
40 |
41 | /* Additional Checks */
42 | "noUnusedLocals": false,
43 | /* Report errors on unused locals. */
44 | "noUnusedParameters": true,
45 | /* Report errors on unused parameters. */
46 | "noImplicitReturns": true,
47 | /* Report error when not all code paths in function return a value. */
48 | "noFallthroughCasesInSwitch": true,
49 | /* Report errors for fallthrough cases in switch statement. */
50 | /* Module Resolution Options */
51 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
52 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
53 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
54 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
55 | // "typeRoots": [], /* List of folders to include type definitions from. */
56 | // "types": [], /* Type declaration files to be included in compilation. */
57 | // "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
58 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
59 |
60 | /* Source Map Options */
61 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
62 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
63 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
64 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
65 |
66 | /* Experimental Options */
67 | "esModuleInterop": true,
68 | "allowSyntheticDefaultImports": true,
69 | "experimentalDecorators": true
70 | /* Enables experimental support for ES7 framework. */
71 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for framework. */
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": [
4 | "tslint:recommended",
5 | "tslint-eslint-rules"
6 | ],
7 | "jsRules": {},
8 | "rules": {
9 | "indent": [
10 | "tabs"
11 | ],
12 | "quotemark": [
13 | true,
14 | "single"
15 | ],
16 | "no-console": false,
17 | "arrow-parens": [
18 | "ban-single-arg-parens"
19 | ],
20 | "object-literal-sort-keys": false,
21 | "object-curly-spacing": [
22 | true,
23 | "always"
24 | ],
25 | "member-access": false,
26 | "interface-name": false,
27 | "member-ordering": [
28 | true,
29 | {
30 | "order": [
31 | "private-static-field",
32 | "public-static-field",
33 | "private-instance-field",
34 | "public-instance-field",
35 | "private-constructor",
36 | "public-constructor",
37 | "private-instance-method",
38 | "protected-instance-method",
39 | "public-static-method",
40 | "public-instance-method"
41 | ]
42 | }
43 | ],
44 | "max-classes-per-file": [true, 5, "exclude-class-expressions"],
45 | "no-unused-expression": true,
46 | "max-line-length": [true, 140],
47 | "interface-over-type-literal": false
48 | }
49 | }
50 |
--------------------------------------------------------------------------------