diff options
| author | pack <pack@packgekko.xyz> | 2026-08-09 10:37:07 +0000 |
|---|---|---|
| committer | pack <pack@packgekko.xyz> | 2026-08-09 10:37:07 +0000 |
| commit | 55a4f1fc869e41aca748c63d3018f0448b1606e0 (patch) | |
| tree | d132d1d01772b6d53faee3e63c534dea5706b78a /node_modules/exponential-backoff | |
| download | crud-55a4f1fc869e41aca748c63d3018f0448b1606e0.tar.gz | |
first commit
Diffstat (limited to '')
37 files changed, 1339 insertions, 0 deletions
diff --git a/node_modules/exponential-backoff/LICENSE b/node_modules/exponential-backoff/LICENSE new file mode 100644 index 0000000..4be46a9 --- /dev/null +++ b/node_modules/exponential-backoff/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Coveo Solutions Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/node_modules/exponential-backoff/README.md b/node_modules/exponential-backoff/README.md new file mode 100644 index 0000000..10c7568 --- /dev/null +++ b/node_modules/exponential-backoff/README.md @@ -0,0 +1,89 @@ +# exponential-backoff + +A utility that allows retrying a function with an exponential delay between attempts. + +## Installation + +``` +npm i exponential-backoff +``` + +## Usage + +The `backOff<T>` function takes a promise-returning function to retry, and an optional `BackOffOptions` object. It returns a `Promise<T>`. + +```ts +function backOff<T>( + request: () => Promise<T>, + options?: BackOffOptions +): Promise<T>; +``` + +Here is an example retrying a function that calls a hypothetical weather endpoint: + +```js +import { backOff } from "exponential-backoff"; + +function getWeather() { + return fetch("weather-endpoint"); +} + +async function main() { + try { + const response = await backOff(() => getWeather()); + // process response + } catch (e) { + // handle error + } +} + +main(); +``` + +Migrating across major versions? Here are our [breaking changes](https://github.com/coveo/exponential-backoff/tree/master/doc/migration-guide.md). + +### `BackOffOptions` + +- `delayFirstAttempt?: boolean` + + Decides whether the `startingDelay` should be applied before the first call. If `false`, the first call will occur without a delay. + + Default value is `false`. + +- `jitter?: JitterType | string` + + Decides whether a [jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) should be applied to the delay. Possible values are `full` and `none`. + + Default value is `none`. + +- `maxDelay?: number` + + The maximum delay, in milliseconds, between two consecutive attempts. + + Default value is `Infinity`. + +- `numOfAttempts?: number` + + The maximum number of times to attempt the function. + + Default value is `10`. + + Minimum value is `1`. + +- `retry?: (e: any, attemptNumber: number) => boolean | Promise<boolean>` + + The `retry` function can be used to run logic after every failed attempt (e.g. logging a message, assessing the last error, etc.). It is called with the last error and the upcoming attempt number. Returning `true` will retry the function as long as the `numOfAttempts` has not been exceeded. Returning `false` will end the execution. + + Default value is a function that always returns `true`. + +- `startingDelay?: number` + + The delay, in milliseconds, before executing the function for the first time. + + Default value is `100` ms. + +- `timeMultiple?: number` + + The `startingDelay` is multiplied by the `timeMultiple` to increase the delay between reattempts. + + Default value is `2`. diff --git a/node_modules/exponential-backoff/dist/backoff.d.ts b/node_modules/exponential-backoff/dist/backoff.d.ts new file mode 100644 index 0000000..2ea9505 --- /dev/null +++ b/node_modules/exponential-backoff/dist/backoff.d.ts @@ -0,0 +1,9 @@ +import { IBackOffOptions, BackoffOptions } from "./options"; +export { BackoffOptions, IBackOffOptions }; +/** + * Executes a function with exponential backoff. + * @param request the function to be executed + * @param options options to customize the backoff behavior + * @returns Promise that resolves to the result of the `request` function + */ +export declare function backOff<T>(request: () => Promise<T>, options?: BackoffOptions): Promise<T>; diff --git a/node_modules/exponential-backoff/dist/backoff.js b/node_modules/exponential-backoff/dist/backoff.js new file mode 100644 index 0000000..6a1b6bd --- /dev/null +++ b/node_modules/exponential-backoff/dist/backoff.js @@ -0,0 +1,124 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (_) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var options_1 = require("./options"); +var delay_factory_1 = require("./delay/delay.factory"); +/** + * Executes a function with exponential backoff. + * @param request the function to be executed + * @param options options to customize the backoff behavior + * @returns Promise that resolves to the result of the `request` function + */ +function backOff(request, options) { + if (options === void 0) { options = {}; } + return __awaiter(this, void 0, void 0, function () { + var sanitizedOptions, backOff; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + sanitizedOptions = options_1.getSanitizedOptions(options); + backOff = new BackOff(request, sanitizedOptions); + return [4 /*yield*/, backOff.execute()]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); +} +exports.backOff = backOff; +var BackOff = /** @class */ (function () { + function BackOff(request, options) { + this.request = request; + this.options = options; + this.attemptNumber = 0; + } + BackOff.prototype.execute = function () { + return __awaiter(this, void 0, void 0, function () { + var e_1, shouldRetry; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!!this.attemptLimitReached) return [3 /*break*/, 7]; + _a.label = 1; + case 1: + _a.trys.push([1, 4, , 6]); + return [4 /*yield*/, this.applyDelay()]; + case 2: + _a.sent(); + return [4 /*yield*/, this.request()]; + case 3: return [2 /*return*/, _a.sent()]; + case 4: + e_1 = _a.sent(); + this.attemptNumber++; + return [4 /*yield*/, this.options.retry(e_1, this.attemptNumber)]; + case 5: + shouldRetry = _a.sent(); + if (!shouldRetry || this.attemptLimitReached) { + throw e_1; + } + return [3 /*break*/, 6]; + case 6: return [3 /*break*/, 0]; + case 7: throw new Error("Something went wrong."); + } + }); + }); + }; + Object.defineProperty(BackOff.prototype, "attemptLimitReached", { + get: function () { + return this.attemptNumber >= this.options.numOfAttempts; + }, + enumerable: true, + configurable: true + }); + BackOff.prototype.applyDelay = function () { + return __awaiter(this, void 0, void 0, function () { + var delay; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + delay = delay_factory_1.DelayFactory(this.options, this.attemptNumber); + return [4 /*yield*/, delay.apply()]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); + }; + return BackOff; +}()); +//# sourceMappingURL=backoff.js.map
\ No newline at end of file diff --git a/node_modules/exponential-backoff/dist/delay/always/always.delay.d.ts b/node_modules/exponential-backoff/dist/delay/always/always.delay.d.ts new file mode 100644 index 0000000..641d0bc --- /dev/null +++ b/node_modules/exponential-backoff/dist/delay/always/always.delay.d.ts @@ -0,0 +1,3 @@ +import { Delay } from "../delay.base"; +export declare class AlwaysDelay extends Delay { +} diff --git a/node_modules/exponential-backoff/dist/delay/always/always.delay.js b/node_modules/exponential-backoff/dist/delay/always/always.delay.js new file mode 100644 index 0000000..40e3407 --- /dev/null +++ b/node_modules/exponential-backoff/dist/delay/always/always.delay.js @@ -0,0 +1,25 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +var delay_base_1 = require("../delay.base"); +var AlwaysDelay = /** @class */ (function (_super) { + __extends(AlwaysDelay, _super); + function AlwaysDelay() { + return _super !== null && _super.apply(this, arguments) || this; + } + return AlwaysDelay; +}(delay_base_1.Delay)); +exports.AlwaysDelay = AlwaysDelay; +//# sourceMappingURL=always.delay.js.map
\ No newline at end of file diff --git a/node_modules/exponential-backoff/dist/delay/delay.base.d.ts b/node_modules/exponential-backoff/dist/delay/delay.base.d.ts new file mode 100644 index 0000000..3cdd15d --- /dev/null +++ b/node_modules/exponential-backoff/dist/delay/delay.base.d.ts @@ -0,0 +1,12 @@ +import { IDelay } from "./delay.interface"; +import { IBackOffOptions } from "../options"; +export declare abstract class Delay implements IDelay { + private options; + protected attempt: number; + constructor(options: IBackOffOptions); + apply(): Promise<unknown>; + setAttemptNumber(attempt: number): void; + private readonly jitteredDelay; + private readonly delay; + protected readonly numOfDelayedAttempts: number; +} diff --git a/node_modules/exponential-backoff/dist/delay/delay.base.js b/node_modules/exponential-backoff/dist/delay/delay.base.js new file mode 100644 index 0000000..b146c2f --- /dev/null +++ b/node_modules/exponential-backoff/dist/delay/delay.base.js @@ -0,0 +1,45 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +var jitter_factory_1 = require("../jitter/jitter.factory"); +var Delay = /** @class */ (function () { + function Delay(options) { + this.options = options; + this.attempt = 0; + } + Delay.prototype.apply = function () { + var _this = this; + return new Promise(function (resolve) { return setTimeout(resolve, _this.jitteredDelay); }); + }; + Delay.prototype.setAttemptNumber = function (attempt) { + this.attempt = attempt; + }; + Object.defineProperty(Delay.prototype, "jitteredDelay", { + get: function () { + var jitter = jitter_factory_1.JitterFactory(this.options); + return jitter(this.delay); + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(Delay.prototype, "delay", { + get: function () { + var constant = this.options.startingDelay; + var base = this.options.timeMultiple; + var power = this.numOfDelayedAttempts; + var delay = constant * Math.pow(base, power); + return Math.min(delay, this.options.maxDelay); + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(Delay.prototype, "numOfDelayedAttempts", { + get: function () { + return this.attempt; + }, + enumerable: true, + configurable: true + }); + return Delay; +}()); +exports.Delay = Delay; +//# sourceMappingURL=delay.base.js.map
\ No newline at end of file diff --git a/node_modules/exponential-backoff/dist/delay/delay.factory.d.ts b/node_modules/exponential-backoff/dist/delay/delay.factory.d.ts new file mode 100644 index 0000000..618ca4e --- /dev/null +++ b/node_modules/exponential-backoff/dist/delay/delay.factory.d.ts @@ -0,0 +1,3 @@ +import { IBackOffOptions } from "../options"; +import { IDelay } from "./delay.interface"; +export declare function DelayFactory(options: IBackOffOptions, attempt: number): IDelay; diff --git a/node_modules/exponential-backoff/dist/delay/delay.factory.js b/node_modules/exponential-backoff/dist/delay/delay.factory.js new file mode 100644 index 0000000..33008db --- /dev/null +++ b/node_modules/exponential-backoff/dist/delay/delay.factory.js @@ -0,0 +1,17 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +var skip_first_delay_1 = require("./skip-first/skip-first.delay"); +var always_delay_1 = require("./always/always.delay"); +function DelayFactory(options, attempt) { + var delay = initDelayClass(options); + delay.setAttemptNumber(attempt); + return delay; +} +exports.DelayFactory = DelayFactory; +function initDelayClass(options) { + if (!options.delayFirstAttempt) { + return new skip_first_delay_1.SkipFirstDelay(options); + } + return new always_delay_1.AlwaysDelay(options); +} +//# sourceMappingURL=delay.factory.js.map
\ No newline at end of file diff --git a/node_modules/exponential-backoff/dist/delay/delay.interface.d.ts b/node_modules/exponential-backoff/dist/delay/delay.interface.d.ts new file mode 100644 index 0000000..6f2a10b --- /dev/null +++ b/node_modules/exponential-backoff/dist/delay/delay.interface.d.ts @@ -0,0 +1,4 @@ +export interface IDelay { + apply: () => Promise<unknown>; + setAttemptNumber: (attempt: number) => void; +} diff --git a/node_modules/exponential-backoff/dist/delay/delay.interface.js b/node_modules/exponential-backoff/dist/delay/delay.interface.js new file mode 100644 index 0000000..6fe2a5a --- /dev/null +++ b/node_modules/exponential-backoff/dist/delay/delay.interface.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=delay.interface.js.map
\ No newline at end of file diff --git a/node_modules/exponential-backoff/dist/delay/skip-first/skip-first.delay.d.ts b/node_modules/exponential-backoff/dist/delay/skip-first/skip-first.delay.d.ts new file mode 100644 index 0000000..d4d7caf --- /dev/null +++ b/node_modules/exponential-backoff/dist/delay/skip-first/skip-first.delay.d.ts @@ -0,0 +1,6 @@ +import { Delay } from "../delay.base"; +export declare class SkipFirstDelay extends Delay { + apply(): Promise<unknown>; + private readonly isFirstAttempt; + protected readonly numOfDelayedAttempts: number; +} diff --git a/node_modules/exponential-backoff/dist/delay/skip-first/skip-first.delay.js b/node_modules/exponential-backoff/dist/delay/skip-first/skip-first.delay.js new file mode 100644 index 0000000..73f8841 --- /dev/null +++ b/node_modules/exponential-backoff/dist/delay/skip-first/skip-first.delay.js @@ -0,0 +1,82 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (_) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var delay_base_1 = require("../delay.base"); +var SkipFirstDelay = /** @class */ (function (_super) { + __extends(SkipFirstDelay, _super); + function SkipFirstDelay() { + return _super !== null && _super.apply(this, arguments) || this; + } + SkipFirstDelay.prototype.apply = function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, this.isFirstAttempt ? true : _super.prototype.apply.call(this)]; + }); + }); + }; + Object.defineProperty(SkipFirstDelay.prototype, "isFirstAttempt", { + get: function () { + return this.attempt === 0; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(SkipFirstDelay.prototype, "numOfDelayedAttempts", { + get: function () { + return this.attempt - 1; + }, + enumerable: true, + configurable: true + }); + return SkipFirstDelay; +}(delay_base_1.Delay)); +exports.SkipFirstDelay = SkipFirstDelay; +//# sourceMappingURL=skip-first.delay.js.map
\ No newline at end of file diff --git a/node_modules/exponential-backoff/dist/jitter/full/full.jitter.d.ts b/node_modules/exponential-backoff/dist/jitter/full/full.jitter.d.ts new file mode 100644 index 0000000..d034464 --- /dev/null +++ b/node_modules/exponential-backoff/dist/jitter/full/full.jitter.d.ts @@ -0,0 +1 @@ +export declare function fullJitter(delay: number): number; diff --git a/node_modules/exponential-backoff/dist/jitter/full/full.jitter.js b/node_modules/exponential-backoff/dist/jitter/full/full.jitter.js new file mode 100644 index 0000000..16cee36 --- /dev/null +++ b/node_modules/exponential-backoff/dist/jitter/full/full.jitter.js @@ -0,0 +1,8 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +function fullJitter(delay) { + var jitteredDelay = Math.random() * delay; + return Math.round(jitteredDelay); +} +exports.fullJitter = fullJitter; +//# sourceMappingURL=full.jitter.js.map
\ No newline at end of file diff --git a/node_modules/exponential-backoff/dist/jitter/jitter.factory.d.ts b/node_modules/exponential-backoff/dist/jitter/jitter.factory.d.ts new file mode 100644 index 0000000..f84bdea --- /dev/null +++ b/node_modules/exponential-backoff/dist/jitter/jitter.factory.d.ts @@ -0,0 +1,3 @@ +import { IBackOffOptions } from "../options"; +export declare type Jitter = (delay: number) => number; +export declare function JitterFactory(options: IBackOffOptions): Jitter; diff --git a/node_modules/exponential-backoff/dist/jitter/jitter.factory.js b/node_modules/exponential-backoff/dist/jitter/jitter.factory.js new file mode 100644 index 0000000..8aafe45 --- /dev/null +++ b/node_modules/exponential-backoff/dist/jitter/jitter.factory.js @@ -0,0 +1,15 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +var full_jitter_1 = require("./full/full.jitter"); +var no_jitter_1 = require("./no/no.jitter"); +function JitterFactory(options) { + switch (options.jitter) { + case "full": + return full_jitter_1.fullJitter; + case "none": + default: + return no_jitter_1.noJitter; + } +} +exports.JitterFactory = JitterFactory; +//# sourceMappingURL=jitter.factory.js.map
\ No newline at end of file diff --git a/node_modules/exponential-backoff/dist/jitter/no/no.jitter.d.ts b/node_modules/exponential-backoff/dist/jitter/no/no.jitter.d.ts new file mode 100644 index 0000000..3afaddd --- /dev/null +++ b/node_modules/exponential-backoff/dist/jitter/no/no.jitter.d.ts @@ -0,0 +1 @@ +export declare function noJitter(delay: number): number; diff --git a/node_modules/exponential-backoff/dist/jitter/no/no.jitter.js b/node_modules/exponential-backoff/dist/jitter/no/no.jitter.js new file mode 100644 index 0000000..15a40bb --- /dev/null +++ b/node_modules/exponential-backoff/dist/jitter/no/no.jitter.js @@ -0,0 +1,7 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +function noJitter(delay) { + return delay; +} +exports.noJitter = noJitter; +//# sourceMappingURL=no.jitter.js.map
\ No newline at end of file diff --git a/node_modules/exponential-backoff/dist/options.d.ts b/node_modules/exponential-backoff/dist/options.d.ts new file mode 100644 index 0000000..94e0bb4 --- /dev/null +++ b/node_modules/exponential-backoff/dist/options.d.ts @@ -0,0 +1,55 @@ +/** + * Type of jitter to apply to the delay. + * - `"none"`: no jitter is applied + * - `"full"`: full jitter is applied (random value between `0` and `delay`) + */ +export declare type JitterType = "none" | "full"; +export declare type BackoffOptions = Partial<IBackOffOptions>; +export interface IBackOffOptions { + /** + * Decides whether the `startingDelay` should be applied before the first call. + * If `false`, the first call will occur without a delay. + * @defaultValue `false` + */ + delayFirstAttempt: boolean; + /** + * Decides whether a [jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) + * should be applied to the delay. Possible values are `"full"` and `"none"`. + * @defaultValue `"none"` + */ + jitter: JitterType; + /** + * The maximum delay, in milliseconds, between two consecutive attempts. + * @defaultValue `Infinity` + */ + maxDelay: number; + /** + * The maximum number of times to attempt the function. + * Must be at least `1`. + * @defaultValue `10` + */ + numOfAttempts: number; + /** + * The `retry` function can be used to run logic after every failed attempt (e.g. logging a message, + * assessing the last error, etc.). + * It is called with the last error and the upcoming attempt number. + * Returning `true` will retry the function as long as the `numOfAttempts` has not been exceeded. + * Returning `false` will end the execution. + * @defaultValue a function that always returns `true`. + * @param e The last error thrown by the function. + * @param attemptNumber The upcoming attempt number. + * @returns `true` to retry the function, `false` to end the execution + */ + retry: (e: any, attemptNumber: number) => boolean | Promise<boolean>; + /** + * The delay, in milliseconds, before executing the function for the first time. + * @defaultValue `100` + */ + startingDelay: number; + /** + * The `startingDelay` is multiplied by the `timeMultiple` to increase the delay between reattempts. + * @defaultValue `2` + */ + timeMultiple: number; +} +export declare function getSanitizedOptions(options: BackoffOptions): IBackOffOptions; diff --git a/node_modules/exponential-backoff/dist/options.js b/node_modules/exponential-backoff/dist/options.js new file mode 100644 index 0000000..1d2ca17 --- /dev/null +++ b/node_modules/exponential-backoff/dist/options.js @@ -0,0 +1,31 @@ +"use strict"; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var defaultOptions = { + delayFirstAttempt: false, + jitter: "none", + maxDelay: Infinity, + numOfAttempts: 10, + retry: function () { return true; }, + startingDelay: 100, + timeMultiple: 2 +}; +function getSanitizedOptions(options) { + var sanitized = __assign(__assign({}, defaultOptions), options); + if (sanitized.numOfAttempts < 1) { + sanitized.numOfAttempts = 1; + } + return sanitized; +} +exports.getSanitizedOptions = getSanitizedOptions; +//# sourceMappingURL=options.js.map
\ No newline at end of file diff --git a/node_modules/exponential-backoff/package.json b/node_modules/exponential-backoff/package.json new file mode 100644 index 0000000..e3e8dc9 --- /dev/null +++ b/node_modules/exponential-backoff/package.json @@ -0,0 +1,62 @@ +{ + "name": "exponential-backoff", + "version": "3.1.3", + "description": "A utility that allows retrying a function with an exponential delay between attempts.", + "files": [ + "dist/", + "src/" + ], + "main": "dist/backoff.js", + "types": "dist/backoff.d.ts", + "scripts": { + "build": "tsc", + "test": "jest", + "test:watch": "jest --watch" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "*.{ts,json,md}": [ + "prettier --write", + "git add" + ] + }, + "jest": { + "transform": { + "^.+\\.ts$": "ts-jest" + }, + "testRegex": "\\.spec\\.ts$", + "moduleFileExtensions": [ + "ts", + "js" + ] + }, + "repository": { + "type": "git", + "url": "git+https://github.com/coveooss/exponential-backoff.git" + }, + "keywords": [ + "exponential", + "backoff", + "retry" + ], + "author": "Sami Sayegh", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/coveooss/exponential-backoff/issues" + }, + "homepage": "https://github.com/coveooss/exponential-backoff#readme", + "devDependencies": { + "@types/jest": "^24.0.18", + "@types/node": "^10.14.21", + "husky": "^3.0.9", + "jest": "^24.9.0", + "lint-staged": "^9.4.2", + "prettier": "^1.18.2", + "ts-jest": "^24.1.0", + "typescript": "^3.6.4" + } +} diff --git a/node_modules/exponential-backoff/src/backoff.spec.ts b/node_modules/exponential-backoff/src/backoff.spec.ts new file mode 100644 index 0000000..24fd95b --- /dev/null +++ b/node_modules/exponential-backoff/src/backoff.spec.ts @@ -0,0 +1,194 @@ +import { backOff } from "./backoff"; +import { BackoffOptions } from "./options"; + +describe("BackOff", () => { + const mockSuccessResponse = { success: true }; + const mockFailResponse = { success: false }; + let backOffRequest: () => Promise<any>; + let backOffOptions: BackoffOptions; + + function initBackOff() { + return backOff(backOffRequest, backOffOptions); + } + + function promiseThatIsResolved() { + return () => Promise.resolve(mockSuccessResponse); + } + + function promiseThatIsRejected() { + return () => Promise.reject(mockFailResponse); + } + + function promiseThatFailsOnceThenSucceeds() { + return (() => { + let firstAttempt = true; + + const request = () => { + if (firstAttempt) { + firstAttempt = false; + return Promise.reject(mockFailResponse); + } + + return Promise.resolve(mockSuccessResponse); + }; + + return request; + })(); + } + + beforeEach(() => { + backOffOptions = { startingDelay: 0 }; + backOffRequest = jest.fn(promiseThatIsResolved()); + }); + + describe("when request function is a promise that resolves", () => { + it("returns the resolved value", () => { + const request = initBackOff(); + return request.then(response => + expect(response).toBe(mockSuccessResponse) + ); + }); + + it("calls the request function only once", () => { + const request = initBackOff(); + return request.then(() => + expect(backOffRequest).toHaveBeenCalledTimes(1) + ); + }); + + it(`when the #backOffOptions.numOfAttempts is 0, + it overrides the value and calls the method only once`, () => { + backOffOptions.numOfAttempts = 0; + const request = initBackOff(); + + return request.then(() => + expect(backOffRequest).toHaveBeenCalledTimes(1) + ); + }); + }); + + describe(`when the #backOffOptions.startingDelay is 100ms`, () => { + const startingDelay = 100; + + beforeEach(() => (backOffOptions.startingDelay = startingDelay)); + + it(`does not delay the first attempt`, () => { + const startTime = Date.now(); + const request = initBackOff(); + + return request.then(() => { + const endTime = Date.now(); + const duration = endTime - startTime; + const roundedDuration = Math.round(duration / 100) * 100; + + expect(roundedDuration).toBe(0); + }); + }); + + it(`when #backOffOptions.delayFirstAttempt is 'true', + it delays the first attempt`, () => { + backOffOptions.delayFirstAttempt = true; + const startTime = Date.now(); + const request = initBackOff(); + + return request.then(() => { + const endTime = Date.now(); + const duration = endTime - startTime; + const roundedDuration = Math.round(duration / 100) * 100; + + expect(roundedDuration).toBe(startingDelay); + }); + }); + }); + + describe("when request function is a promise that is rejected", () => { + beforeEach(() => (backOffRequest = promiseThatIsRejected())); + + it("returns the rejected value", () => { + const request = initBackOff(); + return request.catch(err => expect(err).toBe(mockFailResponse)); + }); + + it("retries the request as many times as specified in #BackOffOptions.numOfAttempts", async () => { + const numOfAttemps = 2; + backOffOptions.numOfAttempts = numOfAttemps; + backOffRequest = jest.fn(() => Promise.reject(mockFailResponse)); + + try { + await initBackOff(); + } catch { + expect(backOffRequest).toHaveBeenCalledTimes(numOfAttemps); + } + }); + + it(`when the #BackOffOptions.retry function is set to always return false, + it only calls request function one time`, async () => { + backOffOptions.retry = () => false; + backOffOptions.numOfAttempts = 2; + backOffRequest = jest.fn(() => Promise.reject(mockFailResponse)); + + try { + await initBackOff(); + } catch { + expect(backOffRequest).toHaveBeenCalledTimes(1); + } + }); + }); + + it("when the #BackOffOptions.retry function returns a promise, it awaits it", async () => { + const retryDuration = 100; + backOffOptions.retry = () => + new Promise(resolve => setTimeout(() => resolve(true), retryDuration)); + backOffRequest = promiseThatFailsOnceThenSucceeds(); + + const start = Date.now(); + await initBackOff(); + const end = Date.now(); + + const duration = end - start; + const roundedDuration = + Math.round(duration / retryDuration) * retryDuration; + + expect(roundedDuration).toBe(retryDuration); + }); + + describe(`when calling #backOff with a function that throws an error the first time, and succeeds the second time`, () => { + beforeEach( + () => (backOffRequest = jest.fn(promiseThatFailsOnceThenSucceeds())) + ); + + it(`returns a successful response`, () => { + const request = initBackOff(); + return request.then(response => + expect(response).toBe(mockSuccessResponse) + ); + }); + + it("calls the request function two times", async () => { + await initBackOff(); + expect(backOffRequest).toHaveBeenCalledTimes(2); + }); + + it(`when setting the #BackOffOption.timeMultiple to a value, + when setting the #BackOffOption.delayFirstAttempt to true, + it applies a delay between the first and the second call`, async () => { + const startingDelay = 100; + const timeMultiple = 3; + const totalExpectedDelay = startingDelay + timeMultiple * startingDelay; + + backOffOptions.startingDelay = startingDelay; + backOffOptions.timeMultiple = timeMultiple; + backOffOptions.delayFirstAttempt = true; + + const start = Date.now(); + await initBackOff(); + const end = Date.now(); + + const duration = end - start; + const roundedDuration = + Math.round(duration / startingDelay) * startingDelay; + + expect(roundedDuration).toBe(totalExpectedDelay); + }); + }); +}); diff --git a/node_modules/exponential-backoff/src/backoff.ts b/node_modules/exponential-backoff/src/backoff.ts new file mode 100644 index 0000000..549770b --- /dev/null +++ b/node_modules/exponential-backoff/src/backoff.ts @@ -0,0 +1,60 @@ +import { + IBackOffOptions, + getSanitizedOptions, + BackoffOptions +} from "./options"; +import { DelayFactory } from "./delay/delay.factory"; + +export { BackoffOptions, IBackOffOptions }; + +/** + * Executes a function with exponential backoff. + * @param request the function to be executed + * @param options options to customize the backoff behavior + * @returns Promise that resolves to the result of the `request` function + */ +export async function backOff<T>( + request: () => Promise<T>, + options: BackoffOptions = {} +): Promise<T> { + const sanitizedOptions = getSanitizedOptions(options); + const backOff = new BackOff(request, sanitizedOptions); + + return await backOff.execute(); +} + +class BackOff<T> { + private attemptNumber = 0; + + constructor( + private request: () => Promise<T>, + private options: IBackOffOptions + ) {} + + public async execute(): Promise<T> { + while (!this.attemptLimitReached) { + try { + await this.applyDelay(); + return await this.request(); + } catch (e) { + this.attemptNumber++; + const shouldRetry = await this.options.retry(e, this.attemptNumber); + + if (!shouldRetry || this.attemptLimitReached) { + throw e; + } + } + } + + throw new Error("Something went wrong."); + } + + private get attemptLimitReached() { + return this.attemptNumber >= this.options.numOfAttempts; + } + + private async applyDelay() { + const delay = DelayFactory(this.options, this.attemptNumber); + await delay.apply(); + } +} diff --git a/node_modules/exponential-backoff/src/delay/always/always.delay.spec.ts b/node_modules/exponential-backoff/src/delay/always/always.delay.spec.ts new file mode 100644 index 0000000..8a08cd8 --- /dev/null +++ b/node_modules/exponential-backoff/src/delay/always/always.delay.spec.ts @@ -0,0 +1,65 @@ +import { AlwaysDelay } from "./always.delay"; +import { IBackOffOptions, getSanitizedOptions } from "../../options"; + +describe(AlwaysDelay.name, () => { + let options: IBackOffOptions; + let delay: AlwaysDelay; + + function initClass() { + delay = new AlwaysDelay(options); + } + + beforeEach(() => { + options = getSanitizedOptions({}); + initClass(); + jest.useFakeTimers(); + }); + + it(`when calling #apply, the delay is equal to the starting delay`, async () => { + const spy = jest.fn(); + delay.apply().then(spy); + jest.runTimersToTime(options.startingDelay); + await Promise.resolve(); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it(`when the attempt number is 1, when calling #apply, + the delay is equal to the starting delay multiplied by the time multiple`, async () => { + delay.setAttemptNumber(1); + + const spy = jest.fn(); + delay.apply().then(spy); + jest.runTimersToTime(options.startingDelay * options.timeMultiple); + await Promise.resolve(); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it(`when the attempt number is 2, when calling #apply, + the delay is equal to the starting delay multiplied by the time multiple raised by the attempt number`, async () => { + const attemptNumber = 2; + delay.setAttemptNumber(attemptNumber); + + const spy = jest.fn(); + delay.apply().then(spy); + jest.runTimersToTime( + options.startingDelay * Math.pow(options.timeMultiple, attemptNumber) + ); + await Promise.resolve(); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it(`when the #maxDelay is less than #startingDelay, when calling #apply, + the delay is equal to the #maxDelay`, async () => { + options.maxDelay = options.startingDelay - 1; + + const spy = jest.fn(); + delay.apply().then(spy); + jest.runTimersToTime(options.maxDelay); + await Promise.resolve(); + + expect(spy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/node_modules/exponential-backoff/src/delay/always/always.delay.ts b/node_modules/exponential-backoff/src/delay/always/always.delay.ts new file mode 100644 index 0000000..ec86c62 --- /dev/null +++ b/node_modules/exponential-backoff/src/delay/always/always.delay.ts @@ -0,0 +1,3 @@ +import { Delay } from "../delay.base"; + +export class AlwaysDelay extends Delay {}
\ No newline at end of file diff --git a/node_modules/exponential-backoff/src/delay/delay.base.ts b/node_modules/exponential-backoff/src/delay/delay.base.ts new file mode 100644 index 0000000..bed90d6 --- /dev/null +++ b/node_modules/exponential-backoff/src/delay/delay.base.ts @@ -0,0 +1,34 @@ +import { IDelay } from "./delay.interface"; +import { IBackOffOptions } from "../options"; +import { JitterFactory } from "../jitter/jitter.factory"; + +export abstract class Delay implements IDelay { + protected attempt = 0; + constructor(private options: IBackOffOptions) {} + + public apply() { + return new Promise(resolve => setTimeout(resolve, this.jitteredDelay)); + } + + public setAttemptNumber(attempt: number) { + this.attempt = attempt; + } + + private get jitteredDelay() { + const jitter = JitterFactory(this.options); + return jitter(this.delay); + } + + private get delay() { + const constant = this.options.startingDelay; + const base = this.options.timeMultiple; + const power = this.numOfDelayedAttempts; + const delay = constant * Math.pow(base, power); + + return Math.min(delay, this.options.maxDelay); + } + + protected get numOfDelayedAttempts() { + return this.attempt; + } +} diff --git a/node_modules/exponential-backoff/src/delay/delay.factory.ts b/node_modules/exponential-backoff/src/delay/delay.factory.ts new file mode 100644 index 0000000..95e89fb --- /dev/null +++ b/node_modules/exponential-backoff/src/delay/delay.factory.ts @@ -0,0 +1,18 @@ +import { IBackOffOptions } from "../options"; +import { SkipFirstDelay } from "./skip-first/skip-first.delay"; +import { AlwaysDelay } from "./always/always.delay"; +import { IDelay } from "./delay.interface"; + +export function DelayFactory(options: IBackOffOptions, attempt: number): IDelay { + const delay = initDelayClass(options); + delay.setAttemptNumber(attempt); + return delay; +} + +function initDelayClass(options: IBackOffOptions) { + if (!options.delayFirstAttempt) { + return new SkipFirstDelay(options); + } + + return new AlwaysDelay(options); +}
\ No newline at end of file diff --git a/node_modules/exponential-backoff/src/delay/delay.interface.ts b/node_modules/exponential-backoff/src/delay/delay.interface.ts new file mode 100644 index 0000000..6ecc8da --- /dev/null +++ b/node_modules/exponential-backoff/src/delay/delay.interface.ts @@ -0,0 +1,4 @@ +export interface IDelay { + apply: () => Promise<unknown>; + setAttemptNumber: (attempt: number) => void; +} diff --git a/node_modules/exponential-backoff/src/delay/skip-first/skip-first.delay.ts b/node_modules/exponential-backoff/src/delay/skip-first/skip-first.delay.ts new file mode 100644 index 0000000..9f2b7f5 --- /dev/null +++ b/node_modules/exponential-backoff/src/delay/skip-first/skip-first.delay.ts @@ -0,0 +1,15 @@ +import { Delay } from "../delay.base"; + +export class SkipFirstDelay extends Delay { + public async apply() { + return this.isFirstAttempt ? true : super.apply(); + } + + private get isFirstAttempt() { + return this.attempt === 0; + } + + protected get numOfDelayedAttempts() { + return this.attempt - 1; + } +}
\ No newline at end of file diff --git a/node_modules/exponential-backoff/src/jitter/full/full.jitter.spec.ts b/node_modules/exponential-backoff/src/jitter/full/full.jitter.spec.ts new file mode 100644 index 0000000..003d08d --- /dev/null +++ b/node_modules/exponential-backoff/src/jitter/full/full.jitter.spec.ts @@ -0,0 +1,31 @@ +import { fullJitter } from './full.jitter'; + +describe(`Testing ${fullJitter.name}`, () => { + const delay = 100; + + function arrayWith5FullJitterDelays() { + return Array(5).fill(delay).map(fullJitter) + } + + describe(`when calling #fullJitter on the same delay multiple times`, () => { + it('all the delays are greater than or equal to 0', () => { + arrayWith5FullJitterDelays() + .forEach(value => expect(value).toBeGreaterThanOrEqual(0)); + }) + + it('all the delays are less than or equal to the original delay', () => { + arrayWith5FullJitterDelays() + .forEach(value => expect(value).toBeLessThanOrEqual(delay)); + }) + + it('the delays are not equal to one another', () => { + const delays = arrayWith5FullJitterDelays(); + expect(new Set(delays).size).not.toBe(1); + }) + + it('the delays are integers', () => { + arrayWith5FullJitterDelays() + .forEach(value => expect(Number.isInteger(value)).toBe(true)) + }) + }) +})
\ No newline at end of file diff --git a/node_modules/exponential-backoff/src/jitter/full/full.jitter.ts b/node_modules/exponential-backoff/src/jitter/full/full.jitter.ts new file mode 100644 index 0000000..b3c447b --- /dev/null +++ b/node_modules/exponential-backoff/src/jitter/full/full.jitter.ts @@ -0,0 +1,4 @@ +export function fullJitter(delay: number) { + const jitteredDelay = Math.random() * delay; + return Math.round(jitteredDelay); +}
\ No newline at end of file diff --git a/node_modules/exponential-backoff/src/jitter/jitter.factory.ts b/node_modules/exponential-backoff/src/jitter/jitter.factory.ts new file mode 100644 index 0000000..b549887 --- /dev/null +++ b/node_modules/exponential-backoff/src/jitter/jitter.factory.ts @@ -0,0 +1,16 @@ +import { IBackOffOptions } from "../options"; +import { fullJitter } from "./full/full.jitter"; +import { noJitter } from "./no/no.jitter"; + +export type Jitter = (delay: number) => number; + +export function JitterFactory(options: IBackOffOptions): Jitter { + switch (options.jitter) { + case "full": + return fullJitter; + + case "none": + default: + return noJitter; + } +} diff --git a/node_modules/exponential-backoff/src/jitter/no/no.jitter.spec.ts b/node_modules/exponential-backoff/src/jitter/no/no.jitter.spec.ts new file mode 100644 index 0000000..1d56524 --- /dev/null +++ b/node_modules/exponential-backoff/src/jitter/no/no.jitter.spec.ts @@ -0,0 +1,9 @@ +import { noJitter } from './no.jitter'; + +describe(`Testing ${noJitter.name}`, () => { + it(`when calling #noJitter with a delay, + it returns the same delay`, () => { + const delay = 100; + expect(noJitter(delay)).toBe(delay); + }) +})
\ No newline at end of file diff --git a/node_modules/exponential-backoff/src/jitter/no/no.jitter.ts b/node_modules/exponential-backoff/src/jitter/no/no.jitter.ts new file mode 100644 index 0000000..bc1b56f --- /dev/null +++ b/node_modules/exponential-backoff/src/jitter/no/no.jitter.ts @@ -0,0 +1,3 @@ +export function noJitter(delay: number) { + return delay; +}
\ No newline at end of file diff --git a/node_modules/exponential-backoff/src/options.ts b/node_modules/exponential-backoff/src/options.ts new file mode 100644 index 0000000..ccdee3a --- /dev/null +++ b/node_modules/exponential-backoff/src/options.ts @@ -0,0 +1,76 @@ +/** + * Type of jitter to apply to the delay. + * - `"none"`: no jitter is applied + * - `"full"`: full jitter is applied (random value between `0` and `delay`) + */ +export type JitterType = "none" | "full"; + +export type BackoffOptions = Partial<IBackOffOptions>; + +export interface IBackOffOptions { + /** + * Decides whether the `startingDelay` should be applied before the first call. + * If `false`, the first call will occur without a delay. + * @defaultValue `false` + */ + delayFirstAttempt: boolean; + /** + * Decides whether a [jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) + * should be applied to the delay. Possible values are `"full"` and `"none"`. + * @defaultValue `"none"` + */ + jitter: JitterType; + /** + * The maximum delay, in milliseconds, between two consecutive attempts. + * @defaultValue `Infinity` + */ + maxDelay: number; + /** + * The maximum number of times to attempt the function. + * Must be at least `1`. + * @defaultValue `10` + */ + numOfAttempts: number; + /** + * The `retry` function can be used to run logic after every failed attempt (e.g. logging a message, + * assessing the last error, etc.). + * It is called with the last error and the upcoming attempt number. + * Returning `true` will retry the function as long as the `numOfAttempts` has not been exceeded. + * Returning `false` will end the execution. + * @defaultValue a function that always returns `true`. + * @param e The last error thrown by the function. + * @param attemptNumber The upcoming attempt number. + * @returns `true` to retry the function, `false` to end the execution + */ + retry: (e: any, attemptNumber: number) => boolean | Promise<boolean>; + /** + * The delay, in milliseconds, before executing the function for the first time. + * @defaultValue `100` + */ + startingDelay: number; + /** + * The `startingDelay` is multiplied by the `timeMultiple` to increase the delay between reattempts. + * @defaultValue `2` + */ + timeMultiple: number; +} + +const defaultOptions: IBackOffOptions = { + delayFirstAttempt: false, + jitter: "none", + maxDelay: Infinity, + numOfAttempts: 10, + retry: () => true, + startingDelay: 100, + timeMultiple: 2 +}; + +export function getSanitizedOptions(options: BackoffOptions) { + const sanitized: IBackOffOptions = { ...defaultOptions, ...options }; + + if (sanitized.numOfAttempts < 1) { + sanitized.numOfAttempts = 1; + } + + return sanitized; +} |