From 6ecfc832ebec94df95579328da88879a5a3d43c5 Mon Sep 17 00:00:00 2001 From: Jimmy Thomson Date: Thu, 21 Apr 2022 11:16:58 +0100 Subject: [PATCH 1/2] Add axios-client package --- packages/axios-client/README.md | 40 ++++++++++++++++ packages/axios-client/jest.config.ts | 10 ++++ packages/axios-client/package.json | 33 +++++++++++++ packages/axios-client/src/constants.ts | 14 ++++++ packages/axios-client/src/index.ts | 49 +++++++++++++++++++ packages/axios-client/src/types.ts | 11 +++++ packages/axios-client/tsconfig.json | 10 ++++ pnpm-lock.yaml | 66 ++++++++++++++++++++++++-- 8 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 packages/axios-client/README.md create mode 100644 packages/axios-client/jest.config.ts create mode 100644 packages/axios-client/package.json create mode 100644 packages/axios-client/src/constants.ts create mode 100644 packages/axios-client/src/index.ts create mode 100644 packages/axios-client/src/types.ts create mode 100644 packages/axios-client/tsconfig.json diff --git a/packages/axios-client/README.md b/packages/axios-client/README.md new file mode 100644 index 0000000..ed0da7f --- /dev/null +++ b/packages/axios-client/README.md @@ -0,0 +1,40 @@ +# `@gradientedge/axios-client` + +A simple factory function for creating an axios client for use in a nodejs environment. No +options are required when calling the `getAxiosClient` function. In this case, default values +will be used for the retry and HttpsAgent config. + +Note that no `HttpAgent` config is provided. We expect all requests to be made via SSL and +therefore the only agent we care about is `HttpsAgent`. + +Minimal example: +```typescript +import { getAxiosClient } from '@gradientedge/axios-client' + +/** + * No options provided, so this will use the default retry and HttpsAgent configuration. + */ +const client = getAxiosClient() +``` + +Full config override example: +```typescript +import { getAxiosClient } from '@gradientedge/axios-client' + +const client = getAxiosClient({ + httpsAgent: { + keepAlive: false, + timeout: 10000, + maxSockets: 40, + scheduling: 'lifo', + + }, + retry: { + delayMs: 100, + maxRetries: 5, + } +}) +``` + +The `client` variable above is an [AxiosInstance](https://github.com/axios/axios/blob/master/index.d.ts#L235) +which gives you access to all the standard axios methods and functionality. diff --git a/packages/axios-client/jest.config.ts b/packages/axios-client/jest.config.ts new file mode 100644 index 0000000..787e0f7 --- /dev/null +++ b/packages/axios-client/jest.config.ts @@ -0,0 +1,10 @@ +export default { + preset: 'ts-jest', + testEnvironment: 'node', + verbose: true, + passWithNoTests: true, + rootDir: '.', + testMatch: ['**/__tests__/**/*.test.ts'], + testPathIgnorePatterns: ['node_modules/', 'dist/'], + setupFilesAfterEnv: ['jest-extended/all'], +} diff --git a/packages/axios-client/package.json b/packages/axios-client/package.json new file mode 100644 index 0000000..fe2858e --- /dev/null +++ b/packages/axios-client/package.json @@ -0,0 +1,33 @@ +{ + "name": "@gradientedge/axios-client", + "version": "0.0.0", + "description": "Factory for creating an axios client object with built-in default keep-alive/retry mechanism", + "author": "Jimmy Thomson ", + "homepage": "https://github.com/gradientedge/common-utils#readme", + "license": "ISC", + "main": "dist/index", + "types": "dist/index", + "files": [ + "dist", + "!dist/**/__tests__" + ], + "repository": { + "type": "git", + "url": "https://github.com/gradientedge/common-utils.git" + }, + "scripts": { + "clean": "rimraf dist && rm -f tsconfig.tsbuildinfo", + "build": "tsc --build", + "build:watch": "tsc --build --incremental --watch", + "test": "pnpm test --workspace --filter @gradientedge/common-utils" + }, + "bugs": { + "url": "https://github.com/gradientedge/common-utils/issues" + }, + "dependencies": { + "axios": "^0.26.1", + "axios-retry": "^3.2.4", + "agentkeepalive": "^4.2.1" + }, + "devDependencies": {} +} diff --git a/packages/axios-client/src/constants.ts b/packages/axios-client/src/constants.ts new file mode 100644 index 0000000..4f7ce31 --- /dev/null +++ b/packages/axios-client/src/constants.ts @@ -0,0 +1,14 @@ +import { RetryConfig } from './types' +import { HttpsOptions } from 'agentkeepalive' + +export const DEFAULT_RETRY_CONFIG: RetryConfig = { + delayMs: 20, + maxRetries: 3, +} + +export const DEFAULT_AGENT_CONFIG: HttpsOptions = { + keepAlive: true, + timeout: 5000, + maxSockets: 20, + scheduling: 'fifo', +} diff --git a/packages/axios-client/src/index.ts b/packages/axios-client/src/index.ts new file mode 100644 index 0000000..f6f59f7 --- /dev/null +++ b/packages/axios-client/src/index.ts @@ -0,0 +1,49 @@ +import { HttpsAgent } from 'agentkeepalive' +import axios, { AxiosError } from 'axios' +import axiosRetry, { isNetworkOrIdempotentRequestError } from 'axios-retry' +import { RequestConfig } from './types' +import { DEFAULT_AGENT_CONFIG, DEFAULT_RETRY_CONFIG } from './constants' + +/** + * Get an axios instance configured with the given retry mechanism + * and custom agent for socket re-use. + */ +export function getAxiosClient(options?: RequestConfig) { + const retry = options?.retry || DEFAULT_RETRY_CONFIG + const httpsAgent = new HttpsAgent({ + ...DEFAULT_AGENT_CONFIG, + ...options?.httpsAgent, + }) + const client = axios.create({ + httpsAgent, + timeout: httpsAgent.options.timeout, + }) + + if (retry.maxRetries) { + axiosRetry(client, { + retries: retry.maxRetries, + retryDelay: getCalculateRetryDelayFn(retry.delayMs), + shouldResetTimeout: true, + retryCondition: function (error: AxiosError) { + // The error code for a timeout is ECONNABORTED. Default axios-retry + // behaviour doesn't consider timeouts 'retryable'. + return isRequestRetryable(error) + }, + }) + } + + return client +} + +export function getCalculateRetryDelayFn(delayMs: number) { + return (retryCount: number) => { + if (retryCount === 0) { + return 0 + } + return delayMs * 2 ** (retryCount - 1) + } +} + +export function isRequestRetryable(error: AxiosError) { + return isNetworkOrIdempotentRequestError(error) || (!error.response && error.code === 'ECONNABORTED') +} diff --git a/packages/axios-client/src/types.ts b/packages/axios-client/src/types.ts new file mode 100644 index 0000000..d355af1 --- /dev/null +++ b/packages/axios-client/src/types.ts @@ -0,0 +1,11 @@ +import { AgentOptions } from 'https' + +export interface RequestConfig { + httpsAgent: AgentOptions + retry: RetryConfig +} + +export interface RetryConfig { + maxRetries: number + delayMs: number +} diff --git a/packages/axios-client/tsconfig.json b/packages/axios-client/tsconfig.json new file mode 100644 index 0000000..28f1127 --- /dev/null +++ b/packages/axios-client/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 17e6b77..d636569 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,6 +43,16 @@ importers: packages/als: specifiers: {} + packages/axios-client: + specifiers: + agentkeepalive: ^4.2.1 + axios: ^0.26.1 + axios-retry: ^3.2.4 + dependencies: + agentkeepalive: 4.2.1 + axios: 0.26.1 + axios-retry: 3.2.4 + packages/error: specifiers: '@types/json-stringify-safe': ^5.0.0 @@ -366,7 +376,6 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.13.9 - dev: true /@babel/template/7.16.7: resolution: {integrity: sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==} @@ -1230,6 +1239,17 @@ packages: - supports-color dev: true + /agentkeepalive/4.2.1: + resolution: {integrity: sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==} + engines: {node: '>= 8.0.0'} + dependencies: + debug: 4.3.3 + depd: 1.1.2 + humanize-ms: 1.2.1 + transitivePeerDependencies: + - supports-color + dev: false + /ajv/6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} dependencies: @@ -1310,6 +1330,21 @@ packages: resolution: {integrity: sha1-x57Zf380y48robyXkLzDZkdLS3k=} dev: true + /axios-retry/3.2.4: + resolution: {integrity: sha512-Co3UXiv4npi6lM963mfnuH90/YFLKWWDmoBYfxkHT5xtkSSWNqK9zdG3fw5/CP/dsoKB5aMMJCsgab+tp1OxLQ==} + dependencies: + '@babel/runtime': 7.17.2 + is-retry-allowed: 2.2.0 + dev: false + + /axios/0.26.1: + resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==} + dependencies: + follow-redirects: 1.14.9 + transitivePeerDependencies: + - debug + dev: false + /babel-jest/27.5.1_@babel+core@7.17.5: resolution: {integrity: sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -1659,7 +1694,6 @@ packages: optional: true dependencies: ms: 2.1.2 - dev: true /decamelize-keys/1.1.0: resolution: {integrity: sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=} @@ -1702,6 +1736,11 @@ packages: engines: {node: '>=0.4.0'} dev: true + /depd/1.1.2: + resolution: {integrity: sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=} + engines: {node: '>= 0.6'} + dev: false + /detect-indent/6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -2078,6 +2117,16 @@ packages: resolution: {integrity: sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==} dev: true + /follow-redirects/1.14.9: + resolution: {integrity: sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /form-data/3.0.1: resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} engines: {node: '>= 6'} @@ -2272,6 +2321,12 @@ packages: engines: {node: '>=10.17.0'} dev: true + /humanize-ms/1.2.1: + resolution: {integrity: sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=} + dependencies: + ms: 2.1.2 + dev: false + /husky/7.0.4: resolution: {integrity: sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==} engines: {node: '>=12'} @@ -2381,6 +2436,11 @@ packages: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} dev: true + /is-retry-allowed/2.2.0: + resolution: {integrity: sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==} + engines: {node: '>=10'} + dev: false + /is-stream/2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -3233,7 +3293,6 @@ packages: /ms/2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true /natural-compare/1.4.0: resolution: {integrity: sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=} @@ -3566,7 +3625,6 @@ packages: /regenerator-runtime/0.13.9: resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} - dev: true /regexpp/3.2.0: resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} From 03c3e88cf43ec4500f78e5e6e2419a9cf8c375b8 Mon Sep 17 00:00:00 2001 From: Jimmy Thomson Date: Thu, 21 Apr 2022 11:17:55 +0100 Subject: [PATCH 2/2] docs(changeset): Add axios-client package --- .changeset/rude-carrots-repeat.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rude-carrots-repeat.md diff --git a/.changeset/rude-carrots-repeat.md b/.changeset/rude-carrots-repeat.md new file mode 100644 index 0000000..85ae337 --- /dev/null +++ b/.changeset/rude-carrots-repeat.md @@ -0,0 +1,5 @@ +--- +'@gradientedge/axios-client': minor +--- + +Initial cut of axios-client package