Skip to content

Commit

Permalink
Merge pull request #14 from WillianAgostini/interceptor-manager
Browse files Browse the repository at this point in the history
feat: 🚀 Created interceptor manager
  • Loading branch information
WillianAgostini committed Aug 9, 2023
2 parents 36c8b18 + 5c7eb9b commit a175d8a
Show file tree
Hide file tree
Showing 7 changed files with 483 additions and 21 deletions.
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,64 @@ try {
}
```

## Interceptors

You can intercept requests or responses before they are handled by `then` or `catch`.

```js
// Add a request and response interceptor
await get("http://localhost:5000/todos/1", {
interceptors: {
request: async (err, request) => {
// Do something before request is sent
return request;
},
response: async (err, response) => {
// Do something with response data
return response;
}
},
});
```

You can add interceptors to a custom instance of nordus.

```js
const instance = nordus.create();
instance.interceptors.request.use(() => {/*...*/});
```

If you need to remove an interceptor later you can.

```js
const myInterceptor = instance.interceptors.request.use(() => {/*...*/});
instance.interceptors.request.eject(myInterceptor);
```

You can also clear all interceptors for requests or responses.
```js
const instance = nordus.create();
instance.interceptors.request.use(() => {/*...*/});
instance.interceptors.request.clear(); // Removes interceptors from requests
instance.interceptors.response.use(() => {/*...*/});
instance.interceptors.response.clear(); // Removes interceptors from responses
```

### Multiple Interceptors

Given you add multiple response interceptors
and when the response was fulfilled
- then each interceptor is executed
- then they are executed in the order they were added
- then only the last interceptor's result is returned
- then every interceptor receives the result of its predecessor
- and when the fulfillment-interceptor throws
- then the following fulfillment-interceptor is not called
- then the following rejection-interceptor is called
- once caught, another following fulfill-interceptor is called again (just like in a promise chain).

Read [the interceptor tests](./tests/interceptors.spec.ts) for seeing all this in code.

## License

[MIT](LICENSE)
58 changes: 51 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import {
NordusInterceptorManager,
createInterceptors,
requestInterptorsManager,
responseInterceptorsManager,
} from "./interceptors";
import {
InterceptorRequest,
InterceptorResponse,
NordusConfig,
NordusConfigApi,
NordusRequest,
NordusResponse,
} from "./request";
import { append } from "./utils";

Expand Down Expand Up @@ -89,34 +96,71 @@ function mergeConfig(
};
}

export function create(nordusConf?: NordusConfig) {
interface Nordus {
interceptors: {
request: NordusInterceptorManager<InterceptorRequest>;
response: NordusInterceptorManager<InterceptorResponse>;
};
get: <T = any>(
url: string,
nordusConfig?: NordusConfig,
) => Promise<NordusResponse<T>>;
post: <T = any, D = any>(
url: string,
body: D,
nordusConfig?: NordusConfig,
) => Promise<NordusResponse<T>>;
put: <T = any, D = any>(
url: string,
body: D,
nordusConfig?: NordusConfig,
) => Promise<NordusResponse<T>>;
patch: <T = any, D = any>(
url: string,
body: D,
nordusConfig?: NordusConfig,
) => Promise<NordusResponse<T>>;
del: <T = any>(
url: string,
nordusConfig?: NordusConfig,
) => Promise<NordusResponse<T>>;
}

export function create(nordusConfig?: NordusConfig) {
const nordusConfigApi = getDefaultConfig(nordusConfig);
return {
get: <T = any>(url: string, nordusConfig?: NordusConfig) =>
get<T>(url, mergeConfig(nordusConf, nordusConfig)),
get<T>(url, mergeConfig(nordusConfigApi, nordusConfig)),
post: <T = any, D = any>(
url: string,
body: D,
nordusConfig?: NordusConfig,
) => post<T, D>(url, body, mergeConfig(nordusConf, nordusConfig)),
) => post<T, D>(url, body, mergeConfig(nordusConfigApi, nordusConfig)),
put: <T = any, D = any>(
url: string,
body: D,
nordusConfig?: NordusConfig,
) => put<T, D>(url, body, mergeConfig(nordusConf, nordusConfig)),
) => put<T, D>(url, body, mergeConfig(nordusConfigApi, nordusConfig)),
patch: <T = any, D = any>(
url: string,
body: D,
nordusConfig?: NordusConfig,
) => patch<T, D>(url, body, mergeConfig(nordusConf, nordusConfig)),
) => patch<T, D>(url, body, mergeConfig(nordusConfigApi, nordusConfig)),
del: <T = any>(url: string, nordusConfig?: NordusConfig) =>
del<T>(url, mergeConfig(nordusConf, nordusConfig)),
};
del<T>(url, mergeConfig(nordusConfigApi, nordusConfig)),
interceptors: {
request: requestInterptorsManager(nordusConfigApi),
response: responseInterceptorsManager(nordusConfigApi),
},
} as Nordus;
}

function getDefaultConfig(nordusConfig?: NordusConfig) {
if (!nordusConfig) nordusConfig = {};

if (!nordusConfig.responseType) nordusConfig.responseType = "json";

createInterceptors(nordusConfig);

return nordusConfig as NordusConfigApi;
}
76 changes: 76 additions & 0 deletions src/interceptors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
InterceptorRequest,
InterceptorResponse,
NordusConfig,
NordusConfigApi,
} from "./request";
import { asArray, randomUUID, getValueByKey } from "./utils";

export interface NordusInterceptorManager<V> {
use(interceptor: V): string;
eject(id: string): void;
clear(): void;
}

export function requestInterptorsManager(
nordusConfigApi: NordusConfigApi,
): NordusInterceptorManager<InterceptorRequest> {
return {
use: (interceptor: InterceptorRequest) => {
return addInterceptor(nordusConfigApi.interceptors.request, interceptor);
},
eject: (id: string) => {
nordusConfigApi.interceptors.request = removeInterceptor(
nordusConfigApi.interceptors.request,
id,
);
},
clear: () => {
nordusConfigApi.interceptors.request = [];
},
};
}

export function responseInterceptorsManager(
nordusConfigApi: NordusConfigApi,
): NordusInterceptorManager<InterceptorResponse> {
return {
use: (interceptor: InterceptorResponse) => {
return addInterceptor(nordusConfigApi.interceptors.response, interceptor);
},
eject: (id: string) => {
nordusConfigApi.interceptors.response = removeInterceptor(
nordusConfigApi.interceptors.response,
id,
);
},
clear: () => {
nordusConfigApi.interceptors.response = [];
},
};
}

export function createInterceptors(nordusConfig: NordusConfig) {
const interceptorsRequest = asArray(nordusConfig.interceptors?.request);
const interceptorsResponse = asArray(nordusConfig.interceptors?.response);
if (!nordusConfig.interceptors)
nordusConfig.interceptors = {
request: interceptorsRequest,
response: interceptorsResponse,
};
}

export function addInterceptor<T>(list: T[], interceptor: T) {
list.push(interceptor);
const uuid = randomUUID();
Object.defineProperty(interceptor, "__interceptorId", {
value: uuid,
});
return uuid;
}

export function removeInterceptor<T>(interceptor: T[], id: string) {
return interceptor.filter((element) => {
return getValueByKey("__interceptorId", element) != id;
});
}
37 changes: 24 additions & 13 deletions src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ export interface NordusConfig extends RequestInit {
timeout?: number;
}

export interface NordusConfigApi extends NordusConfig {}
interface InterceptorsApi {
request: InterceptorRequest[];
response: InterceptorResponse[];
}

export interface NordusConfigApi extends NordusConfig {
interceptors: InterceptorsApi;
}

export interface NordusResponse<T = any> extends Response {
data?: T;
Expand Down Expand Up @@ -84,6 +91,7 @@ export class NordusRequest {
nordusConfigApi: NordusConfigApi,
abort: AbortTimeout,
) {
let error, request;
try {
const urlRequest = this.generateURL(url, nordusConfigApi);
const body = this.getBody(nordusConfigApi);
Expand All @@ -96,15 +104,16 @@ export class NordusRequest {
...nordusConfigApi,
};

const request = new Request(urlRequest, init);
request = new Request(urlRequest, init);

this.setHeaders(nordusConfigApi, request);
this.setRequestType(request, nordusConfigApi);
await this.executeLoopAsync(this.interceptorRequest, null, request);
return request;
} catch (error: any) {
await this.executeLoopAsync(this.interceptorRequest, error, null);
throw error;
} catch (err: any) {
error = err;
} finally {
await this.executeLoopAsync(this.interceptorRequest, error, request);
if (error) throw error;
return request!;
}
}

Expand All @@ -114,21 +123,23 @@ export class NordusRequest {
abort: AbortTimeout,
) {
const timeoutId = abort.start();
let error, response;
try {
const response = (await fetch(request)) as NordusResponse<T>;
response = (await fetch(request)) as NordusResponse<T>;
clearTimeout(timeoutId);
if (!response.ok) throw new Error(response.statusText);

response.data = await this.getResponseFromType<T>(
response,
nordusConfigApi,
);
await this.executeLoopAsync(this.interceptorsResponse, null, response);
return response;
} catch (error: any) {
} catch (err: any) {
error = err;
clearTimeout(timeoutId);
await this.executeLoopAsync(this.interceptorsResponse, error, null);
throw error;
} finally {
await this.executeLoopAsync(this.interceptorsResponse, error, response);
if (error) throw error;
return response!;
}
}

Expand Down
23 changes: 23 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
import crypto from "crypto";

export function append<T>(arr: T[], element: T | T[]) {
if (Array.isArray(element)) {
arr.push(...element);
} else {
arr.push(element);
}
return arr;
}

export function randomUUID() {
return crypto.randomUUID();
}

export function propertyOf<TObj>(name: keyof TObj) {
return name;
}

export function getValueByKey(key: string, obj: any) {
const attr = key as keyof typeof obj;
const __interceptorId = obj[attr];
return __interceptorId;
}

export function asArray(obj: any) {
if (!obj) return [];
if (Array.isArray(obj)) return obj;
return [obj];
}
3 changes: 3 additions & 0 deletions tests/helpers/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

0 comments on commit a175d8a

Please sign in to comment.