diff --git a/.gitignore b/.gitignore index 87ebd25..52ac12d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules *.log *.err .DS_Store +.idea lib/ diff --git a/README.md b/README.md index 6d3e94c..80a4551 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,139 @@ xhr({ true) ``` +### `xhr.requestInterceptorsStorage` and `xhr.responseInterceptorsStorage` + +have the following API: +```typescript +export interface NetworkRequest { + headers: Record; + uri: string; + metadata: Record; + body?: unknown; + retry?: Retry; + timeout?: number; +} + +export interface NetworkResponse { + headers: Record; + responseUrl: string; + body?: unknown; + responseType?: XMLHttpRequestResponseType; +} + +export type Interceptor = (payload: T) => T; + +export interface InterceptorsStorage { + enable(): void; + disable(): void; + getIsEnabled(): boolean; + reset(): void; + addInterceptor(type: string, interceptor: Interceptor): boolean; + removeInterceptor(type: string, interceptor: Interceptor): boolean; + clearInterceptorsByType(type: string): boolean; + clear(): boolean; + getForType(type: string): Set>; + execute(type: string, payload: T): T; +} + +``` +Usage: + +```js +xhr.requestInterceptorsStorage.enable(); +xhr.responseInterceptorsStorage.enable(); + +xhr.requestInterceptorsStorage.addInterceptor('segment', (request) => { + // read / update NetworkRequest + return request; +}); + +xhr.responseInterceptorsStorage.addInterceptor('segement', (response) => { + // read / update NetworkResponse + return response; +}); + +xhr({ + uri: 'https://host/segment', + responseType: 'arraybuffer', + requestType: 'segement' +}, function(err, response, responseBody) { + // your callback +}); + +``` + +### `xhr.retryManager` + +has the following API + +```typescript +export interface Retry { + getCurrentDelay(): number; + getCurrentMinPossibleDelay(): number; + getCurrentMaxPossibleDelay(): number; + getCurrentFuzzedDelay(): number; + shouldRetry(): boolean; + moveToNextAttempt(): void; +} + +export interface RetryOptions { + maxAttempts?: number; + delayFactor?: number; + fuzzFactor?: number; + initialDelay?: number; +} + +export interface RetryManager { + enable(): void; + disable(): void; + reset(): void; + getMaxAttempts(): number; + setMaxAttempts(maxAttempts: number): void; + getDelayFactor(): number; + setDelayFactor(delayFactor: number): void; + getFuzzFactor(): number; + setFuzzFactor(fuzzFactor: number): void; + getInitialDelay(): number; + setInitialDelay(initialDelay: number): void; + createRetry(options?: RetryOptions): Retry; +} +``` + +Usage: + +```js +xhr.retryManager.enable(); + +xhr.retryManager.setMaxAttempts(2); + +xhr({ + uri: 'https://host/segment', + responseType: 'arraybuffer', + retry: xhr.retryManager.createRetry(), +}, function(err, response, responseBody) { + // your callback +}); + +// or override values for specific request: +xhr({ + uri: 'https://host/segment', + responseType: 'arraybuffer', + retry: xhr.retryManager.createRetry({ maxAttempts: 3 }), +}, function(err, response, responseBody) { + // your callback +}); + + +// you can combine interceptors/retry APIs if you dont have direct acces to the request: +xhr.requestInterceptorsStorage.addInterceptor('segment', (request) => { + // read / update NetworkRequest + request.retry = xhr.retryManager.createRetry(); + return request; +}); + + +``` ## FAQ diff --git a/index.d.ts b/index.d.ts index 812d756..c79da2a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -39,6 +39,9 @@ export interface XhrBaseConfig { password?: string; withCredentials?: boolean; responseType?: '' | 'arraybuffer' | 'blob' | 'document' | 'json' | 'text'; + requestType?: string; + metadata?: Record; + retry?: Retry; beforeSend?: (xhrObject: XMLHttpRequest) => void; xhr?: XMLHttpRequest; } @@ -57,6 +60,68 @@ export interface XhrInstance { (url: string, options: XhrBaseConfig, callback: XhrCallback): any; } +export interface Retry { + getCurrentDelay(): number; + getCurrentMinPossibleDelay(): number; + getCurrentMaxPossibleDelay(): number; + getCurrentFuzzedDelay(): number; + shouldRetry(): boolean; + moveToNextAttempt(): void; +} + +export interface NetworkRequest { + headers: Record; + uri: string; + metadata: Record; + body?: unknown; + retry?: Retry; + timeout?: number; +} + +export interface NetworkResponse { + headers: Record; + responseUrl: string; + body?: unknown; + responseType?: XMLHttpRequestResponseType; +} + +export type Interceptor = (payload: T) => T; + +export interface InterceptorsStorage { + enable(): void; + disable(): void; + getIsEnabled(): boolean; + reset(): void; + addInterceptor(type: string, interceptor: Interceptor): boolean; + removeInterceptor(type: string, interceptor: Interceptor): boolean; + clearInterceptorsByType(type: string): boolean; + clear(): boolean; + getForType(type: string): Set>; + execute(type: string, payload: T): T; +} + +export interface RetryOptions { + maxAttempts?: number; + delayFactor?: number; + fuzzFactor?: number; + initialDelay?: number; +} + +export interface RetryManager { + enable(): void; + disable(): void; + reset(): void; + getMaxAttempts(): number; + setMaxAttempts(maxAttempts: number): void; + getDelayFactor(): number; + setDelayFactor(delayFactor: number): void; + getFuzzFactor(): number; + setFuzzFactor(fuzzFactor: number): void; + getInitialDelay(): number; + setInitialDelay(initialDelay: number): void; + createRetry(options?: RetryOptions): Retry; +} + export interface XhrStatic extends XhrInstance { del: XhrInstance; get: XhrInstance; @@ -64,6 +129,9 @@ export interface XhrStatic extends XhrInstance { patch: XhrInstance; post: XhrInstance; put: XhrInstance; + requestInterceptorsStorage: InterceptorsStorage; + responseInterceptorsStorage: InterceptorsStorage; + retryManager: RetryManager; } declare const Xhr: XhrStatic; diff --git a/package.json b/package.json index 3e72025..ad1df90 100644 --- a/package.json +++ b/package.json @@ -46,9 +46,11 @@ "start": "npm run build -- -w", "build": "babel-config-cjs src -d lib", "test:index": "run-browser test/index.js -b -m test/mock-server.js | tap-spec", + "test:interceptors": "run-browser test/interceptors.js -b -m test/mock-server.js | tap-spec", + "test:retry": "run-browser test/retry.js -b -m test/mock-server.js | tap-spec", "test:http-handler": "run-browser test/http-handler.js -b -m test/mock-server.js | tap-spec", "pretest": "npm run build", - "test": "npm run test:index && npm run test:http-handler", + "test": "npm run test:index && npm run test:http-handler && npm run test:interceptors && npm run test:retry", "browser": "run-browser -m test/mock-server.js test/index.js" } } diff --git a/src/index.js b/src/index.js index 7382544..f865c55 100644 --- a/src/index.js +++ b/src/index.js @@ -2,8 +2,13 @@ var window = require("global/window") var _extends = require("@babel/runtime/helpers/extends"); var isFunction = require('is-function'); +var InterceptorsStorage = require('./interceptors.js'); +var RetryManager = require("./retry.js"); createXHR.httpHandler = require('./http-handler.js'); +createXHR.requestInterceptorsStorage = new InterceptorsStorage(); +createXHR.responseInterceptorsStorage = new InterceptorsStorage(); +createXHR.retryManager = new RetryManager(); /** * @license @@ -90,6 +95,27 @@ function _createXHR(options) { throw new Error("callback argument missing") } + // call all registered request interceptors for a given request type: + if (options.requestType && createXHR.requestInterceptorsStorage.getIsEnabled()) { + const requestInterceptorPayload = { + uri: options.uri || options.url, + headers: options.headers || {}, + body: options.body, + metadata: options.metadata || {}, + retry: options.retry, + timeout: options.timeout, + } + + const updatedPayload = createXHR.requestInterceptorsStorage.execute(options.requestType, requestInterceptorPayload); + + options.uri = updatedPayload.uri; + options.headers = updatedPayload.headers; + options.body = updatedPayload.body; + options.metadata = updatedPayload.metadata; + options.retry = updatedPayload.retry; + options.timeout = updatedPayload.timeout; + } + var called = false var callback = function cbOnce(err, response, body){ if(!called){ @@ -99,7 +125,9 @@ function _createXHR(options) { } function readystatechange() { - if (xhr.readyState === 4) { + // do not call load 2 times when response interceptors are enabled + // why do we even need this 2nd load? + if (xhr.readyState === 4 && !createXHR.responseInterceptorsStorage.getIsEnabled()) { setTimeout(loadFunc, 0) } } @@ -125,10 +153,39 @@ function _createXHR(options) { function errorFunc(evt) { clearTimeout(timeoutTimer) + clearTimeout(options.retryTimeout) if(!(evt instanceof Error)){ evt = new Error("" + (evt || "Unknown XMLHttpRequest Error") ) } evt.statusCode = 0 + + // we would like to retry on error: + if (!aborted && createXHR.retryManager.getIsEnabled() && options.retry && options.retry.shouldRetry()) { + options.retryTimeout = setTimeout(function() { + options.retry.moveToNextAttempt(); + // we want to re-use the same options and the same xhr object: + options.xhr = xhr; + _createXHR(options); + }, options.retry.getCurrentFuzzedDelay()); + + return; + } + + // call all registered response interceptors for a given request type: + if (options.requestType && createXHR.responseInterceptorsStorage.getIsEnabled()) { + const responseInterceptorPayload = { + headers: failureResponse.headers || {}, + body: failureResponse.body, + responseUrl: xhr.responseURL, + responseType: xhr.responseType, + } + + const updatedPayload = createXHR.responseInterceptorsStorage.execute(options.requestType, responseInterceptorPayload); + + failureResponse.body = updatedPayload.body; + failureResponse.headers = updatedPayload.headers; + } + return callback(evt, failureResponse) } @@ -137,6 +194,7 @@ function _createXHR(options) { if (aborted) return var status clearTimeout(timeoutTimer) + clearTimeout(options.retryTimeout) if(options.useXDR && xhr.status===undefined) { //IE8 CORS GET successful response doesn't have a status field, but body is fine status = 200 @@ -161,6 +219,22 @@ function _createXHR(options) { } else { err = new Error("Internal XMLHttpRequest Error") } + + // call all registered response interceptors for a given request type: + if (options.requestType && createXHR.responseInterceptorsStorage.getIsEnabled()) { + const responseInterceptorPayload = { + headers: response.headers || {}, + body: response.body, + responseUrl: xhr.responseURL, + responseType: xhr.responseType, + } + + const updatedPayload = createXHR.responseInterceptorsStorage.execute(options.requestType, responseInterceptorPayload); + + response.body = updatedPayload.body; + response.headers = updatedPayload.headers; + } + return callback(err, response, response.body) } @@ -210,6 +284,7 @@ function _createXHR(options) { } xhr.onabort = function(){ aborted = true; + clearTimeout(options.retryTimeout) } xhr.ontimeout = errorFunc xhr.open(method, uri, !sync, options.username, options.password) diff --git a/src/interceptors.js b/src/interceptors.js new file mode 100644 index 0000000..de5c5e0 --- /dev/null +++ b/src/interceptors.js @@ -0,0 +1,95 @@ +class InterceptorsStorage { + constructor() { + this.typeToInterceptorsMap_ = new Map(); + this.enabled_ = false; + } + + getIsEnabled() { + return this.enabled_; + } + + enable() { + this.enabled_ = true; + } + + disable() { + this.enabled_ = false; + } + + reset() { + this.typeToInterceptorsMap_ = new Map(); + this.enabled_ = false; + } + + addInterceptor(type, interceptor) { + if (!this.typeToInterceptorsMap_.has(type)) { + this.typeToInterceptorsMap_.set(type, new Set()); + } + + const interceptorsSet = this.typeToInterceptorsMap_.get(type); + + if (interceptorsSet.has(interceptor)) { + // already have this interceptor + return false; + } + + interceptorsSet.add(interceptor); + + return true; + } + + removeInterceptor(type, interceptor) { + const interceptorsSet = this.typeToInterceptorsMap_.get(type); + + if (interceptorsSet && interceptorsSet.has(interceptor)) { + interceptorsSet.delete(interceptor); + return true; + } + + return false; + } + + clearInterceptorsByType(type) { + const interceptorsSet = this.typeToInterceptorsMap_.get(type); + + if (!interceptorsSet) { + return false; + } + + this.typeToInterceptorsMap_.delete(type); + this.typeToInterceptorsMap_.set(type, new Set()); + + return true; + } + + clear() { + if (!this.typeToInterceptorsMap_.size) { + return false; + } + + this.typeToInterceptorsMap_ = new Map(); + + return true; + } + + getForType(type) { + return this.typeToInterceptorsMap_.get(type) || new Set(); + } + + execute(type, payload) { + const interceptors = this.getForType(type); + + for (const interceptor of interceptors) { + try { + payload = interceptor(payload); + } catch (e) { + //ignore + } + } + + return payload; + } +} + + +module.exports = InterceptorsStorage; diff --git a/src/retry.js b/src/retry.js new file mode 100644 index 0000000..29514d9 --- /dev/null +++ b/src/retry.js @@ -0,0 +1,118 @@ +class RetryManager { + constructor() { + this.maxAttempts_ = 1; + this.delayFactor_ = 0.1; + this.fuzzFactor_ = 0.1; + this.initialDelay_ = 1000; + this.enabled_ = false; + } + + getIsEnabled() { + return this.enabled_; + } + + enable() { + this.enabled_ = true; + } + + disable() { + this.enabled_ = false; + } + + reset() { + this.maxAttempts_ = 1; + this.delayFactor_ = 0.1; + this.fuzzFactor_ = 0.1; + this.initialDelay_ = 1000; + this.enabled_ = false; + } + + getMaxAttempts() { + return this.maxAttempts_; + } + + setMaxAttempts(maxAttempts) { + this.maxAttempts_ = maxAttempts; + } + + getDelayFactor() { + return this.delayFactor_; + } + + setDelayFactor(delayFactor) { + this.delayFactor_ = delayFactor; + } + + getFuzzFactor() { + return this.fuzzFactor_; + } + + setFuzzFactor(fuzzFactor) { + this.fuzzFactor_ = fuzzFactor; + } + + getInitialDelay() { + return this.initialDelay_; + } + + setInitialDelay(initialDelay) { + this.initialDelay_ = initialDelay; + } + + createRetry({ maxAttempts, delayFactor, fuzzFactor, initialDelay } = {}) { + return new Retry({ + maxAttempts: maxAttempts || this.maxAttempts_, + delayFactor: delayFactor || this.delayFactor_, + fuzzFactor: fuzzFactor || this.fuzzFactor_, + initialDelay: initialDelay || this.initialDelay_, + }); + } +} + +class Retry { + constructor(options) { + this.maxAttempts_ = options.maxAttempts; + this.delayFactor_ = options.delayFactor; + this.fuzzFactor_ = options.fuzzFactor; + this.currentDelay_ = options.initialDelay; + + this.currentAttempt_ = 1; + } + + moveToNextAttempt() { + this.currentAttempt_++; + const delayDelta = this.currentDelay_ * this.delayFactor_; + this.currentDelay_ = this.currentDelay_ + delayDelta; + } + + shouldRetry() { + return this.currentAttempt_ < this.maxAttempts_; + } + + getCurrentDelay() { + return this.currentDelay_; + } + + getCurrentMinPossibleDelay() { + return (1 - this.fuzzFactor_) * this.currentDelay_; + } + + getCurrentMaxPossibleDelay() { + return (1 + this.fuzzFactor_) * this.currentDelay_; + } + + /** + * For example fuzzFactor is 0.1 + * This means ±10% deviation + * So if we have delay as 1000 + * This function can generate any value from 900 to 1100 + */ + getCurrentFuzzedDelay() { + const lowValue = this.getCurrentMinPossibleDelay(); + const highValue = this.getCurrentMaxPossibleDelay(); + + return lowValue + Math.random() * (highValue - lowValue); + } +} + +module.exports = RetryManager; diff --git a/test/index.js b/test/index.js index fe84126..c6ca9ba 100644 --- a/test/index.js +++ b/test/index.js @@ -11,7 +11,8 @@ test("constructs and calls callback without throwing", { timeout: 500 }, functio }) }) -test("[func] Can GET a url (cross-domain)", { timeout: 2000 }, function(assert) { +// skipping this test because test url is no longer valid +test.skip("[func] Can GET a url (cross-domain)", { timeout: 2000 }, function(assert) { xhr({ uri: "http://www.mocky.io/v2/55a02cb72651260b1a94f024", useXDR: true @@ -381,3 +382,18 @@ test('httpHandler is available on XHR', function(assert) { assert.ok(xhr.httpHandler) assert.end(); }); + +test('requestInterceptorStorage is available on XHR', function(assert) { + assert.ok(xhr.requestInterceptorsStorage); + assert.end(); +}); + +test('responseInterceptorStorage is available on XHR', function(assert) { + assert.ok(xhr.responseInterceptorsStorage); + assert.end(); +}); + +test('retryManager is available on XHR', function(assert) { + assert.ok(xhr.retryManager); + assert.end(); +}); diff --git a/test/interceptors.js b/test/interceptors.js new file mode 100644 index 0000000..985f5cc --- /dev/null +++ b/test/interceptors.js @@ -0,0 +1,163 @@ +var window = require("global/window") +var test = require("tape") + +var InterceptorsStorage = require('../lib/interceptors'); + +test("should ignore duplicate interceptors", function(assert) { + var interceptorsStorage = new InterceptorsStorage(); + + function interceptor(request) { + return request; + } + + assert.equal(interceptorsStorage.addInterceptor('license', interceptor), true); + assert.equal(interceptorsStorage.addInterceptor('license', interceptor), false); + assert.end(); +}); + +test("should execute registered interceptors", function(assert) { + var interceptorsStorage = new InterceptorsStorage(); + + var count = 0; + + function interceptor(request) { + assert.equal(request.data, 'license-data'); + count++; + return request; + } + + interceptorsStorage.addInterceptor('license', interceptor); + interceptorsStorage.execute('license', { data: 'license-data' }); + assert.equal(count, 1); + assert.end(); +}); + + +test("should not execute removed interceptors", function(assert) { + var interceptorsStorage = new InterceptorsStorage(); + + var count = 0; + + function interceptor(request) { + assert.equal(request.data, 'license-data'); + count++; + return request; + } + + interceptorsStorage.addInterceptor('license', interceptor); + interceptorsStorage.removeInterceptor('license', interceptor); + interceptorsStorage.execute('license', { data: 'license-data' }); + assert.equal(count, 0); + assert.end(); +}); + +test("should return interceptors by type", function(assert) { + var interceptorsStorage = new InterceptorsStorage(); + + function interceptorOne(request) { + return request; + } + + function interceptorTwo(request) { + return request; + } + + interceptorsStorage.addInterceptor('license', interceptorOne); + interceptorsStorage.addInterceptor('segment', interceptorTwo); + + var setLicense = interceptorsStorage.getForType('license'); + var setSegment = interceptorsStorage.getForType('segment'); + + assert.equal(setLicense.size, 1); + assert.equal(setSegment.size, 1); + assert.end(); +}); + +test("should clear interceptors by type", function(assert) { + var interceptorsStorage = new InterceptorsStorage(); + + function interceptorOne(request) { + return request; + } + + function interceptorTwo(request) { + return request; + } + + interceptorsStorage.addInterceptor('license', interceptorOne); + interceptorsStorage.addInterceptor('segment', interceptorTwo); + + interceptorsStorage.clearInterceptorsByType('license'); + + var setLicense = interceptorsStorage.getForType('license'); + var setSegment = interceptorsStorage.getForType('segment'); + + assert.equal(setLicense.size, 0); + assert.equal(setSegment.size, 1); + assert.end(); +}); + + +test("should clear all interceptors", function(assert) { + var interceptorsStorage = new InterceptorsStorage(); + + function interceptorOne(request) { + return request; + } + + function interceptorTwo(request) { + return request; + } + + interceptorsStorage.addInterceptor('license', interceptorOne); + interceptorsStorage.addInterceptor('segment', interceptorTwo); + + interceptorsStorage.clear(); + + var setLicense = interceptorsStorage.getForType('license'); + var setSegment = interceptorsStorage.getForType('segment'); + + assert.equal(setLicense.size, 0); + assert.equal(setSegment.size, 0); + assert.end(); +}); + + +test("should set enabled", function(assert) { + var interceptorsStorage = new InterceptorsStorage(); + + assert.equal(interceptorsStorage.getIsEnabled(), false); + interceptorsStorage.enable(); + assert.equal(interceptorsStorage.getIsEnabled(), true); + interceptorsStorage.disable(); + assert.equal(interceptorsStorage.getIsEnabled(), false); + assert.end(); +}); + + +test("should reset", function(assert) { + var interceptorsStorage = new InterceptorsStorage(); + + function interceptorOne(request) { + return request; + } + + function interceptorTwo(request) { + return request; + } + + interceptorsStorage.addInterceptor('license', interceptorOne); + interceptorsStorage.addInterceptor('segment', interceptorTwo); + interceptorsStorage.enable(); + + assert.equal( interceptorsStorage.getForType('license').size, 1); + assert.equal( interceptorsStorage.getForType('segment').size, 1); + assert.equal(interceptorsStorage.getIsEnabled(), true); + + interceptorsStorage.reset(); + + assert.equal( interceptorsStorage.getForType('license').size, 0); + assert.equal( interceptorsStorage.getForType('segment').size, 0); + assert.equal(interceptorsStorage.getIsEnabled(), false); + assert.end(); +}); diff --git a/test/retry.js b/test/retry.js new file mode 100644 index 0000000..ff7921e --- /dev/null +++ b/test/retry.js @@ -0,0 +1,182 @@ +var window = require("global/window") +var test = require("tape") + +var RetryManager = require('../lib/retry'); + +test("should set enabled", function(assert) { + var retryManager = new RetryManager(); + + assert.equal(retryManager.getIsEnabled(), false); + retryManager.enable(); + assert.equal(retryManager.getIsEnabled(), true); + retryManager.disable(); + assert.equal(retryManager.getIsEnabled(), false); + assert.end(); +}); + +test("should set maxAttempts", function(assert) { + var retryManager = new RetryManager(); + + assert.equal(retryManager.getMaxAttempts(), 1); + retryManager.setMaxAttempts(2); + assert.equal(retryManager.getMaxAttempts(), 2); + assert.end(); +}); + +test("should set delayFactor", function(assert) { + var retryManager = new RetryManager(); + + assert.equal(retryManager.getDelayFactor(), 0.1); + retryManager.setDelayFactor(0.2); + assert.equal(retryManager.getDelayFactor(), 0.2); + assert.end(); +}); + + +test("should set fuzzFactor", function(assert) { + var retryManager = new RetryManager(); + + assert.equal(retryManager.getFuzzFactor(), 0.1); + retryManager.setFuzzFactor(0.2); + assert.equal(retryManager.getFuzzFactor(), 0.2); + assert.end(); +}); + +test("should set initialDelay", function(assert) { + var retryManager = new RetryManager(); + + assert.equal(retryManager.getInitialDelay(), 1000); + retryManager.setInitialDelay(2000); + assert.equal(retryManager.getInitialDelay(), 2000); + assert.end(); +}); + +test("should reset", function(assert) { + var retryManager = new RetryManager(); + + assert.equal(retryManager.getInitialDelay(), 1000); + retryManager.setInitialDelay(2000); + assert.equal(retryManager.getInitialDelay(), 2000); + + assert.equal(retryManager.getFuzzFactor(), 0.1); + retryManager.setFuzzFactor(0.2); + assert.equal(retryManager.getFuzzFactor(), 0.2); + + assert.equal(retryManager.getDelayFactor(), 0.1); + retryManager.setDelayFactor(0.2); + assert.equal(retryManager.getDelayFactor(), 0.2); + + assert.equal(retryManager.getMaxAttempts(), 1); + retryManager.setMaxAttempts(2); + assert.equal(retryManager.getMaxAttempts(), 2); + + assert.equal(retryManager.getIsEnabled(), false); + retryManager.enable(); + assert.equal(retryManager.getIsEnabled(), true); + + retryManager.reset(); + + assert.equal(retryManager.getInitialDelay(), 1000); + assert.equal(retryManager.getFuzzFactor(), 0.1); + assert.equal(retryManager.getDelayFactor(), 0.1); + assert.equal(retryManager.getMaxAttempts(), 1); + assert.equal(retryManager.getIsEnabled(), false); + assert.end(); +}); + + +test("should create Retry instance", function(assert) { + var retryManager = new RetryManager(); + + retryManager.setMaxAttempts(3); + + var retry = retryManager.createRetry(); + + var shouldRetry; + var currentDelay; + var currentFuzzedDelay; + var currentMaxPossibleDelay; + var currentMinPossibleDelay; + + + shouldRetry = retry.shouldRetry(); + currentDelay = retry.getCurrentDelay(); + currentFuzzedDelay = retry.getCurrentFuzzedDelay(); + currentMinPossibleDelay = retry.getCurrentMinPossibleDelay(); + currentMaxPossibleDelay = retry.getCurrentMaxPossibleDelay(); + + assert.equal(shouldRetry, true); + assert.equal(currentDelay, 1000); + assert.equal(currentMinPossibleDelay, 900); + assert.equal(currentMaxPossibleDelay, 1100); + assert.ok( + currentFuzzedDelay >= currentMinPossibleDelay && currentFuzzedDelay <= currentMaxPossibleDelay, + "Current Fuzzed Received: " + currentFuzzedDelay); + + retry.moveToNextAttempt(); + + shouldRetry = retry.shouldRetry(); + currentDelay = retry.getCurrentDelay(); + currentFuzzedDelay = retry.getCurrentFuzzedDelay(); + currentMinPossibleDelay = retry.getCurrentMinPossibleDelay(); + currentMaxPossibleDelay = retry.getCurrentMaxPossibleDelay(); + + assert.equal(shouldRetry, true); + assert.equal(currentDelay, 1100); + assert.equal(currentMinPossibleDelay, 990); + assert.equal(currentMaxPossibleDelay, 1210); + assert.ok( + currentFuzzedDelay >= currentMinPossibleDelay && currentFuzzedDelay <= currentMaxPossibleDelay, + "Current Fuzzed Received: " + currentFuzzedDelay); + + retry.moveToNextAttempt(); + + shouldRetry = retry.shouldRetry(); + + assert.equal(shouldRetry, false); + + assert.end(); +}); + + +test("should create Retry instance with overwritten properties", function(assert) { + var retryManager = new RetryManager(); + + retryManager.setMaxAttempts(3); + + var retry = retryManager.createRetry({ + maxAttempts: 2, + delayFactor: 0.2, + fuzzFactor: 0.2, + initialDelay: 3000, + }); + + var shouldRetry; + var currentDelay; + var currentFuzzedDelay; + var currentMaxPossibleDelay; + var currentMinPossibleDelay; + + + shouldRetry = retry.shouldRetry(); + currentDelay = retry.getCurrentDelay(); + currentFuzzedDelay = retry.getCurrentFuzzedDelay(); + currentMinPossibleDelay = retry.getCurrentMinPossibleDelay(); + currentMaxPossibleDelay = retry.getCurrentMaxPossibleDelay(); + + assert.equal(shouldRetry, true); + assert.equal(currentDelay, 3000); + assert.equal(currentMinPossibleDelay, 2400); + assert.equal(currentMaxPossibleDelay, 3600); + assert.ok( + currentFuzzedDelay >= currentMinPossibleDelay && currentFuzzedDelay <= currentMaxPossibleDelay, + "Current Fuzzed Received: " + currentFuzzedDelay); + + retry.moveToNextAttempt(); + + shouldRetry = retry.shouldRetry(); + + assert.equal(shouldRetry, false); + + assert.end(); +});