@aligent/microservice-util-lib 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aligent/microservice-util-lib",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "A set of utility functions for Aligent Microservices",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -11,11 +11,11 @@
11
11
  "directory": "packages/microservice-util-lib"
12
12
  },
13
13
  "dependencies": {
14
- "@aws-sdk/client-s3": "^3.800.0",
15
- "@aws-sdk/client-ssm": "^3.799.0",
14
+ "@aws-sdk/client-s3": "^3.922.0",
15
+ "@aws-sdk/client-ssm": "^3.922.0",
16
16
  "oauth-sign": "^0.9.0",
17
17
  "object-hash": "^3.0.0",
18
- "openapi-fetch": "^0.13.5"
18
+ "openapi-fetch": "^0.15.0"
19
19
  },
20
20
  "author": "Aligent",
21
21
  "license": "MIT",
package/src/index.d.ts CHANGED
@@ -3,8 +3,9 @@ import fetchSsmParams from './fetch-ssm-params/fetch-ssm-params';
3
3
  import getAwsIdFromArn from './get-aws-id-from-arn/get-aws-id-from-arn';
4
4
  import hasDefinedProperties from './has-properties-defined/has-properties-defined';
5
5
  import { ApiKey, Basic, OAuth10a, OAuth20, apiKeyAuthMiddleware, basicAuthMiddleware, oAuth10aAuthMiddleware, oAuth20AuthMiddleware } from './openapi-fetch-middlewares/authentications';
6
+ import { RetryConfig as RetryMiddlewareConfig, retryMiddleware } from './openapi-fetch-middlewares/retry';
6
7
  import remap, { ObjectMap, Remap } from './remap/remap';
7
8
  import retryWrapper, { RetryConfig } from './retry-wrapper/retry-wrapper';
8
9
  import S3Dao from './s3/s3';
9
- export type { ApiKey, Basic, OAuth10a, OAuth20, ObjectMap, Remap, RetryConfig, S3Dao };
10
- export { apiKeyAuthMiddleware, basicAuthMiddleware, chunkBy, fetchSsmParams, getAwsIdFromArn, hasDefinedProperties, oAuth10aAuthMiddleware, oAuth20AuthMiddleware, remap, retryWrapper, };
10
+ export type { ApiKey, Basic, OAuth10a, OAuth20, ObjectMap, Remap, RetryConfig, RetryMiddlewareConfig, S3Dao, };
11
+ export { apiKeyAuthMiddleware, basicAuthMiddleware, chunkBy, fetchSsmParams, getAwsIdFromArn, hasDefinedProperties, oAuth10aAuthMiddleware, oAuth20AuthMiddleware, remap, retryMiddleware, retryWrapper, };
package/src/index.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.retryWrapper = exports.remap = exports.oAuth20AuthMiddleware = exports.oAuth10aAuthMiddleware = exports.hasDefinedProperties = exports.getAwsIdFromArn = exports.fetchSsmParams = exports.chunkBy = exports.basicAuthMiddleware = exports.apiKeyAuthMiddleware = void 0;
6
+ exports.retryWrapper = exports.retryMiddleware = exports.remap = exports.oAuth20AuthMiddleware = exports.oAuth10aAuthMiddleware = exports.hasDefinedProperties = exports.getAwsIdFromArn = exports.fetchSsmParams = exports.chunkBy = exports.basicAuthMiddleware = exports.apiKeyAuthMiddleware = void 0;
7
7
  /* v8 ignore start */
8
8
  const chunk_by_1 = __importDefault(require("./chunk-by/chunk-by"));
9
9
  exports.chunkBy = chunk_by_1.default;
@@ -18,6 +18,8 @@ Object.defineProperty(exports, "apiKeyAuthMiddleware", { enumerable: true, get:
18
18
  Object.defineProperty(exports, "basicAuthMiddleware", { enumerable: true, get: function () { return authentications_1.basicAuthMiddleware; } });
19
19
  Object.defineProperty(exports, "oAuth10aAuthMiddleware", { enumerable: true, get: function () { return authentications_1.oAuth10aAuthMiddleware; } });
20
20
  Object.defineProperty(exports, "oAuth20AuthMiddleware", { enumerable: true, get: function () { return authentications_1.oAuth20AuthMiddleware; } });
21
+ const retry_1 = require("./openapi-fetch-middlewares/retry");
22
+ Object.defineProperty(exports, "retryMiddleware", { enumerable: true, get: function () { return retry_1.retryMiddleware; } });
21
23
  const remap_1 = __importDefault(require("./remap/remap"));
22
24
  exports.remap = remap_1.default;
23
25
  const retry_wrapper_1 = __importDefault(require("./retry-wrapper/retry-wrapper"));
@@ -0,0 +1,54 @@
1
+ import type { Middleware } from 'openapi-fetch';
2
+ import type { RetryConfig, RetryContext, RetryDelayFn } from './types/retry';
3
+ /**
4
+ * This middleware implements retry logic with support for:
5
+ * - Configurable number of retry attempts
6
+ * - Exponential backoff, linear backoff, or custom delay strategies
7
+ * - Custom retry conditions
8
+ * - Callbacks for retry events
9
+ * - Filtering by status codes
10
+ *
11
+ * @param {RetryConfig} [config={}] - The retry configuration.
12
+ * @returns {Middleware} The middleware for retry functionality.
13
+ *
14
+ * @example
15
+ * // Basic usage with defaults (3 retries, exponential backoff)
16
+ * const middleware = retryMiddleware();
17
+ *
18
+ * @example
19
+ * // Custom configuration
20
+ * const middleware = retryMiddleware({
21
+ * retries: 5,
22
+ * retryDelay: 'linear',
23
+ * retryDelayBase: 200,
24
+ * retryOn: [500, 502, 503, 504],
25
+ * onRetry: (context) => {
26
+ * console.log(`Retrying request (attempt ${context.attemptNumber})`);
27
+ * },
28
+ * });
29
+ *
30
+ * @example
31
+ * // Custom retry condition
32
+ * const middleware = retryMiddleware({
33
+ * retries: 3,
34
+ * retryCondition: async (context) => {
35
+ * // Only retry on 503 Service Unavailable
36
+ * return context.response.status === 503;
37
+ * },
38
+ * });
39
+ *
40
+ * @example
41
+ * // Custom delay function
42
+ * const middleware = retryMiddleware({
43
+ * retries: 3,
44
+ * retryDelay: (attemptNumber) => {
45
+ * // Custom delay with jitter
46
+ * const baseDelay = 100 * Math.pow(2, attemptNumber);
47
+ * const jitter = Math.random() * 100;
48
+ * return baseDelay + jitter;
49
+ * },
50
+ * });
51
+ */
52
+ declare function retryMiddleware(config?: RetryConfig): Middleware;
53
+ export { retryMiddleware };
54
+ export type { RetryConfig, RetryContext, RetryDelayFn };
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.retryMiddleware = retryMiddleware;
4
+ const is_network_error_1 = require("./utils/is-network-error");
5
+ const IDEMPOTENT_HTTP_METHODS = ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'];
6
+ /**
7
+ * Default retry condition function.
8
+ * Retries on:
9
+ * - Network errors
10
+ * - 5xx server errors
11
+ * - 429 Too Many Requests
12
+ * - 408 Request Timeout
13
+ * - Idempotent methods only
14
+ *
15
+ * @param {RetryContext} context - The retry context.
16
+ * @param {boolean} idempotentOnly - Whether to retry only when the HTTP method is an idempotent methods.
17
+ * @returns {boolean} Whether the request should be retried.
18
+ */
19
+ function defaultRetryCondition(context, idempotentOnly) {
20
+ const { request, response, error } = context;
21
+ if (!IDEMPOTENT_HTTP_METHODS.includes(request.method) && idempotentOnly) {
22
+ return false;
23
+ }
24
+ if ((0, is_network_error_1.isNetworkError)(error)) {
25
+ return true;
26
+ }
27
+ if (response) {
28
+ return response.status >= 500 || response.status === 429 || response.status === 408;
29
+ }
30
+ return false;
31
+ }
32
+ /**
33
+ * Calculates delay for exponential backoff strategy.
34
+ *
35
+ * @param {number} attempt - The current attempt number (1-indexed).
36
+ * @param {number} baseDelay - Base delay in milliseconds.
37
+ * @param {number} maxDelay - Maximum delay in milliseconds.
38
+ * @returns {number} The delay in milliseconds.
39
+ */
40
+ function exponentialDelay(attempt, baseDelay, maxDelay) {
41
+ const delay = baseDelay * Math.pow(2, attempt - 1);
42
+ return Math.min(delay, maxDelay);
43
+ }
44
+ /**
45
+ * Calculates delay for linear backoff strategy.
46
+ *
47
+ * @param {number} attempt - The current attempt number (1-indexed).
48
+ * @param {number} baseDelay - Base delay in milliseconds.
49
+ * @param {number} maxDelay - Maximum delay in milliseconds.
50
+ * @returns {number} The delay in milliseconds.
51
+ */
52
+ function linearDelay(attempt, baseDelay, maxDelay) {
53
+ const delay = baseDelay * attempt;
54
+ return Math.min(delay, maxDelay);
55
+ }
56
+ /**
57
+ * Gets the retry delay function based on the configuration.
58
+ *
59
+ * @param {RetryConfig} config - The retry configuration.
60
+ * @returns {RetryDelayFn} The retry delay function.
61
+ */
62
+ function getRetryDelayFn(config) {
63
+ const baseDelay = config?.baseDelay ?? 100;
64
+ const maxDelay = config?.maxDelay ?? 30000;
65
+ const retryDelay = config?.retryDelay;
66
+ if (typeof retryDelay === 'function') {
67
+ return retryDelay;
68
+ }
69
+ switch (retryDelay) {
70
+ case 'linear':
71
+ return (attempt) => linearDelay(attempt, baseDelay, maxDelay);
72
+ case 'exponential':
73
+ default:
74
+ return (attempt) => exponentialDelay(attempt, baseDelay, maxDelay);
75
+ }
76
+ }
77
+ /**
78
+ * Checks if the response status should trigger a retry based on the retryOn configuration.
79
+ *
80
+ * @param {number} status - The HTTP status code.
81
+ * @param {number[]} retryOn - Array of status codes to retry on.
82
+ * @returns {boolean} Whether the status should trigger a retry.
83
+ */
84
+ function shouldRetryOnStatus(status, retryOn) {
85
+ return retryOn.includes(status);
86
+ }
87
+ /**
88
+ * This middleware implements retry logic with support for:
89
+ * - Configurable number of retry attempts
90
+ * - Exponential backoff, linear backoff, or custom delay strategies
91
+ * - Custom retry conditions
92
+ * - Callbacks for retry events
93
+ * - Filtering by status codes
94
+ *
95
+ * @param {RetryConfig} [config={}] - The retry configuration.
96
+ * @returns {Middleware} The middleware for retry functionality.
97
+ *
98
+ * @example
99
+ * // Basic usage with defaults (3 retries, exponential backoff)
100
+ * const middleware = retryMiddleware();
101
+ *
102
+ * @example
103
+ * // Custom configuration
104
+ * const middleware = retryMiddleware({
105
+ * retries: 5,
106
+ * retryDelay: 'linear',
107
+ * retryDelayBase: 200,
108
+ * retryOn: [500, 502, 503, 504],
109
+ * onRetry: (context) => {
110
+ * console.log(`Retrying request (attempt ${context.attemptNumber})`);
111
+ * },
112
+ * });
113
+ *
114
+ * @example
115
+ * // Custom retry condition
116
+ * const middleware = retryMiddleware({
117
+ * retries: 3,
118
+ * retryCondition: async (context) => {
119
+ * // Only retry on 503 Service Unavailable
120
+ * return context.response.status === 503;
121
+ * },
122
+ * });
123
+ *
124
+ * @example
125
+ * // Custom delay function
126
+ * const middleware = retryMiddleware({
127
+ * retries: 3,
128
+ * retryDelay: (attemptNumber) => {
129
+ * // Custom delay with jitter
130
+ * const baseDelay = 100 * Math.pow(2, attemptNumber);
131
+ * const jitter = Math.random() * 100;
132
+ * return baseDelay + jitter;
133
+ * },
134
+ * });
135
+ */
136
+ function retryMiddleware(config) {
137
+ const normalisedConfig = {
138
+ ...config,
139
+ retries: config?.retries ?? 3,
140
+ retryCondition: config?.retryCondition ?? defaultRetryCondition,
141
+ retryDelay: getRetryDelayFn(config),
142
+ idempotentOnly: config?.idempotentOnly ?? true,
143
+ fetch: config?.fetch ?? fetch,
144
+ };
145
+ return {
146
+ async onResponse({ request, response }) {
147
+ const context = { attempt: 1, request, response, error: null };
148
+ // If retryOn is specified, only use that list
149
+ if (config?.retryOn && config.retryOn.length > 0) {
150
+ if (!shouldRetryOnStatus(response.status, config.retryOn)) {
151
+ return context.response;
152
+ }
153
+ return await performRetries(normalisedConfig, context);
154
+ }
155
+ // Otherwise, check if we should retry based on retry condition
156
+ const shouldRetry = await normalisedConfig.retryCondition(context, normalisedConfig.idempotentOnly);
157
+ if (!shouldRetry) {
158
+ return context.response;
159
+ }
160
+ return await performRetries(normalisedConfig, context);
161
+ },
162
+ async onError({ request, error }) {
163
+ if (!(0, is_network_error_1.isNetworkError)(error)) {
164
+ throw error;
165
+ }
166
+ return await performRetries(normalisedConfig, {
167
+ attempt: 1,
168
+ request,
169
+ response: null,
170
+ error,
171
+ });
172
+ },
173
+ };
174
+ }
175
+ /**
176
+ * Performs the retry attempts.
177
+ */
178
+ async function performRetries(config, context) {
179
+ const maxRetries = config.retries;
180
+ let response = context.response;
181
+ let attempt = 1;
182
+ do {
183
+ const delay = await config.retryDelay(attempt, { ...context, attempt, response });
184
+ await new Promise(resolve => setTimeout(resolve, delay));
185
+ if (config.onRetry) {
186
+ await config.onRetry({ ...context, attempt, response });
187
+ }
188
+ try {
189
+ const signal = config.shouldResetTimeout ? undefined : context.request.signal;
190
+ response = await config.fetch(new Request(context.request, { signal }));
191
+ context = { ...context, attempt: attempt + 1, response, error: null };
192
+ attempt++;
193
+ }
194
+ catch (err) {
195
+ // Network error occurred during retry
196
+ const error = err instanceof Error ? err : new Error(String(err));
197
+ context = { ...context, attempt: attempt + 1, response, error };
198
+ attempt++;
199
+ }
200
+ if (!response) {
201
+ continue;
202
+ }
203
+ if (config.retryOn && !shouldRetryOnStatus(response?.status, config.retryOn)) {
204
+ return response;
205
+ }
206
+ const shouldRetry = await config.retryCondition(context, config.idempotentOnly);
207
+ if (!shouldRetry) {
208
+ return response;
209
+ }
210
+ } while (attempt <= maxRetries);
211
+ if (!response) {
212
+ throw context.error;
213
+ }
214
+ if (!response.ok) {
215
+ throw new Error(`${response.status}: ${response.statusText}`);
216
+ }
217
+ return response;
218
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Represents the context for a retry attempt.
3
+ *
4
+ * @interface RetryContext
5
+ * @property {number} attempt - The current attempt number (1-indexed).
6
+ * @property {Request} request - The original request being retried.
7
+ * @property {Response | null} response - The response that triggered the retry.
8
+ * @property {Error | null} error - The error that triggered the retry, if any.
9
+ */
10
+ export interface RetryContext {
11
+ attempt: number;
12
+ request: Request;
13
+ response: Response | null;
14
+ error: Error | null;
15
+ }
16
+ /**
17
+ * Function type for custom retry condition.
18
+ * Returns true if the request should be retried.
19
+ *
20
+ * @param {RetryContext} context - The retry context containing attempt information.
21
+ * @param {boolean} idempotentOnly - Whether to retry only when the HTTP method is an idempotent methods.
22
+ * @returns {boolean | Promise<boolean>} Whether to retry the request.
23
+ */
24
+ export type RetryConditionFn = (context: RetryContext, idempotentOnly: boolean) => boolean | Promise<boolean>;
25
+ /**
26
+ * Function type for custom retry delay calculation.
27
+ * Returns the delay in milliseconds before the next retry attempt.
28
+ *
29
+ * @param {number} attempt - The current attempt number (1-indexed).
30
+ * @param {RetryContext} context - The retry context containing attempt information.
31
+ * @returns {number | Promise<number>} The delay in milliseconds.
32
+ */
33
+ export type RetryDelayFn = (attempt: number, context: RetryContext) => number | Promise<number>;
34
+ /**
35
+ * Function type for the onRetry callback.
36
+ * Called before each retry attempt.
37
+ *
38
+ * @param {RetryContext} context - The retry context containing attempt information.
39
+ * @returns {void | Promise<void>}
40
+ */
41
+ export type OnRetryFn = (context: RetryContext) => void | Promise<void>;
42
+ /**
43
+ * Configuration for the retry middleware.
44
+ *
45
+ * This interface provides options to configure retry behavior, including:
46
+ * - Number of retry attempts
47
+ * - Custom retry conditions
48
+ * - Retry delay strategies (exponential backoff, linear, custom)
49
+ * - Callbacks for retry events
50
+ *
51
+ * @interface RetryConfig
52
+ * @property {number} [retries=3] - The maximum number of retry attempts.
53
+ * @property {RetryConditionFn} [retryCondition]
54
+ * - Custom function to determine if a request should be retried.
55
+ * - Defaults to retrying on 5xx, 429, 408 errors and network errors.
56
+ * @property {RetryDelayFn | 'exponential' | 'linear'} [retryDelay='exponential']
57
+ * - Strategy for calculating delay between retries.
58
+ * - 'exponential': Exponential backoff (100ms * 2^attemptNumber)
59
+ * - 'linear': Linear backoff (100ms * attemptNumber)
60
+ * - Custom function: Allows custom delay calculation
61
+ * @property {number} [retryDelayBase=100] - Base delay in milliseconds for built-in delay strategies.
62
+ * @property {number} [maxRetryDelay=30000] - Maximum delay in milliseconds between retry attempts.
63
+ * @property {boolean} [shouldResetTimeout=false] - Whether to reset the timeout between retries.
64
+ * @property {OnRetryFn} [onRetry] - Callback function executed before each retry attempt.
65
+ * @property {number[]} [retryOn]
66
+ * - Array of HTTP status codes that should trigger a retry.
67
+ * - If not specified, defaults to 5xx, 429 and 408 errors.
68
+ * @property {boolean} [idempotentOnly]
69
+ * - Whether to retry only when the HTTP method is an idempotent methods.
70
+ * - If not specified, defaults to true and retry only on GET, HEAD, OPTIONS, PUT or DELETE method.
71
+ * @property {typeof fetch} [fetch]
72
+ * - Custom fetch function to use for retries. Defaults to the global fetch function.
73
+ * - Useful for testing or using a custom fetch implementation.
74
+ */
75
+ export interface RetryConfig {
76
+ retries?: number;
77
+ retryCondition?: RetryConditionFn;
78
+ retryDelay?: RetryDelayFn | 'exponential' | 'linear';
79
+ baseDelay?: number;
80
+ maxDelay?: number;
81
+ shouldResetTimeout?: boolean;
82
+ onRetry?: OnRetryFn;
83
+ retryOn?: number[];
84
+ idempotentOnly?: boolean;
85
+ fetch?: typeof fetch;
86
+ }
87
+ export type NormalisedConfig = Omit<RetryConfig, 'retries' | 'retryCondition' | 'retryDelay' | 'idempotentOnly' | 'fetch'> & {
88
+ retries: number;
89
+ retryCondition: RetryConditionFn;
90
+ retryDelay: RetryDelayFn;
91
+ idempotentOnly: boolean;
92
+ fetch: typeof fetch;
93
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * This is a Typescript copy of the amazing work from Sindre Sorhus
3
+ * https://github.com/sindresorhus/is-network-error
4
+ */
5
+ export declare function isNetworkError(error: unknown): error is TypeError;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ /**
3
+ * This is a Typescript copy of the amazing work from Sindre Sorhus
4
+ * https://github.com/sindresorhus/is-network-error
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.isNetworkError = isNetworkError;
8
+ const ERROR_MESSAGES = new Set([
9
+ 'network error', // Chrome
10
+ 'Failed to fetch', // Chrome
11
+ 'NetworkError when attempting to fetch resource.', // Firefox
12
+ 'The Internet connection appears to be offline.', // Safari 16
13
+ 'Network request failed', // `cross-fetch`
14
+ 'fetch failed', // Undici (Node.js)
15
+ 'terminated', // Undici (Node.js)
16
+ ' A network error occurred.', // Bun (WebKit)
17
+ 'Network connection lost', // Cloudflare Workers (fetch)
18
+ ]);
19
+ function isError(object) {
20
+ return Object.prototype.toString.call(object) === '[object Error]';
21
+ }
22
+ function isNetworkError(error) {
23
+ const isValid = error && isError(error) && error.name === 'TypeError' && typeof error.message === 'string';
24
+ if (!isValid) {
25
+ return false;
26
+ }
27
+ const { message, stack } = error;
28
+ // Safari 17+ has generic message but no stack for network errors
29
+ if (message === 'Load failed') {
30
+ return (stack === undefined ||
31
+ // Sentry adds its own stack trace to the fetch error, so also check for that
32
+ '__sentry_captured__' in error);
33
+ }
34
+ // Deno network errors start with specific text
35
+ if (message.startsWith('error sending request for url')) {
36
+ return true;
37
+ }
38
+ // Standard network error messages
39
+ return ERROR_MESSAGES.has(message);
40
+ }