Skip to content

Commit

Permalink
feat: Enhance xhr (#10)
Browse files Browse the repository at this point in the history
* feat: add request/response interceptors

* chore: update node

* chore: update .gitignore

* feat: add retry options

* feat: add more apis

* chore: use from options

* chore: fix typo

* feat: add timeout support

* chore: add retry and timeout to request interceptors

* chore: update types

* chore: revert node

* chore: fix typo

* chore: fix typo

* fix: do not call load 2 times

* chore: revert timeout

* chore: add tests

* chore: update documentation

* chore: fix typos

---------

Co-authored-by: Dzianis Dashkevich <[email protected]>
  • Loading branch information
dzianis-dashkevich and Dzianis Dashkevich authored Jun 6, 2024
1 parent b6a39f2 commit 2497d4c
Show file tree
Hide file tree
Showing 10 changed files with 856 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ node_modules
*.log
*.err
.DS_Store
.idea
lib/
133 changes: 133 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,139 @@ xhr({
true)
```
### `xhr.requestInterceptorsStorage` and `xhr.responseInterceptorsStorage`
have the following API:
```typescript
export interface NetworkRequest {
headers: Record<string, string>;
uri: string;
metadata: Record<string, unknown>;
body?: unknown;
retry?: Retry;
timeout?: number;
}

export interface NetworkResponse {
headers: Record<string, string>;
responseUrl: string;
body?: unknown;
responseType?: XMLHttpRequestResponseType;
}

export type Interceptor<T> = (payload: T) => T;

export interface InterceptorsStorage<T> {
enable(): void;
disable(): void;
getIsEnabled(): boolean;
reset(): void;
addInterceptor(type: string, interceptor: Interceptor<T>): boolean;
removeInterceptor(type: string, interceptor: Interceptor<T>): boolean;
clearInterceptorsByType(type: string): boolean;
clear(): boolean;
getForType(type: string): Set<Interceptor<T>>;
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
Expand Down
68 changes: 68 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export interface XhrBaseConfig {
password?: string;
withCredentials?: boolean;
responseType?: '' | 'arraybuffer' | 'blob' | 'document' | 'json' | 'text';
requestType?: string;
metadata?: Record<string, unknown>;
retry?: Retry;
beforeSend?: (xhrObject: XMLHttpRequest) => void;
xhr?: XMLHttpRequest;
}
Expand All @@ -57,13 +60,78 @@ 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<string, string>;
uri: string;
metadata: Record<string, unknown>;
body?: unknown;
retry?: Retry;
timeout?: number;
}

export interface NetworkResponse {
headers: Record<string, string>;
responseUrl: string;
body?: unknown;
responseType?: XMLHttpRequestResponseType;
}

export type Interceptor<T> = (payload: T) => T;

export interface InterceptorsStorage<T> {
enable(): void;
disable(): void;
getIsEnabled(): boolean;
reset(): void;
addInterceptor(type: string, interceptor: Interceptor<T>): boolean;
removeInterceptor(type: string, interceptor: Interceptor<T>): boolean;
clearInterceptorsByType(type: string): boolean;
clear(): boolean;
getForType(type: string): Set<Interceptor<T>>;
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;
head: XhrInstance;
patch: XhrInstance;
post: XhrInstance;
put: XhrInstance;
requestInterceptorsStorage: InterceptorsStorage<NetworkRequest>;
responseInterceptorsStorage: InterceptorsStorage<NetworkResponse>;
retryManager: RetryManager;
}

declare const Xhr: XhrStatic;
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
77 changes: 76 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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){
Expand All @@ -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)
}
}
Expand All @@ -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)
}

Expand All @@ -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
Expand All @@ -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)
}

Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 2497d4c

Please sign in to comment.