lion/packages/ajax/src/Ajax.js
Goffert van Gool 73d4e222f6
Update public API of ajax & refactor package (#1371)
feat(ajax): align / break public API

Co-authored-by: Ahmet Yesil <yesil.ahmet@ing.com>
2021-05-12 15:21:18 +02:00

206 lines
6.7 KiB
JavaScript

/* eslint-disable consistent-return */
import {
acceptLanguageRequestInterceptor,
createXsrfRequestInterceptor,
createCacheInterceptors,
} from './interceptors/index.js';
import { AjaxFetchError } from './AjaxFetchError.js';
import './typedef.js';
/**
* HTTP Client which acts as a small wrapper around `fetch`. Allows registering hooks which
* intercept request and responses, for example to add authorization headers or logging. A
* request can also be prevented from reaching the network at all by returning the Response directly.
*/
export class Ajax {
/**
* @param {Partial<AjaxConfig>} config
*/
constructor(config = {}) {
/**
* @type {Partial<AjaxConfig>}
* @private
*/
this.__config = {
addAcceptLanguage: true,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
jsonPrefix: '',
...config,
cacheOptions: {
getCacheIdentifier: () => '_default',
...config.cacheOptions,
},
};
/** @type {Array.<RequestInterceptor|CachedRequestInterceptor>} */
this._requestInterceptors = [];
/** @type {Array.<ResponseInterceptor|CachedResponseInterceptor>} */
this._responseInterceptors = [];
if (this.__config.addAcceptLanguage) {
this.addRequestInterceptor(acceptLanguageRequestInterceptor);
}
const { xsrfCookieName, xsrfHeaderName } = this.__config;
if (xsrfCookieName && xsrfHeaderName) {
this.addRequestInterceptor(createXsrfRequestInterceptor(xsrfCookieName, xsrfHeaderName));
}
const { cacheOptions } = this.__config;
if (cacheOptions?.useCache) {
const [cacheRequestInterceptor, cacheResponseInterceptor] = createCacheInterceptors(
cacheOptions.getCacheIdentifier,
cacheOptions,
);
this.addRequestInterceptor(/** @type {RequestInterceptor} */ (cacheRequestInterceptor));
this.addResponseInterceptor(/** @type {ResponseInterceptor} */ (cacheResponseInterceptor));
}
}
/**
* Sets the config for the instance
* @param {Partial<AjaxConfig>} config configuration for the AjaxClass instance
*/
set options(config) {
this.__config = config;
}
get options() {
return this.__config;
}
/** @param {RequestInterceptor} requestInterceptor */
addRequestInterceptor(requestInterceptor) {
this._requestInterceptors.push(requestInterceptor);
}
/** @param {RequestInterceptor} requestInterceptor */
removeRequestInterceptor(requestInterceptor) {
this._requestInterceptors = this._requestInterceptors.filter(
interceptor => interceptor !== requestInterceptor,
);
}
/** @param {ResponseInterceptor} responseInterceptor */
addResponseInterceptor(responseInterceptor) {
this._responseInterceptors.push(responseInterceptor);
}
/** @param {ResponseInterceptor} responseInterceptor */
removeResponseInterceptor(responseInterceptor) {
this._responseInterceptors = this._responseInterceptors.filter(
interceptor => interceptor !== responseInterceptor,
);
}
/**
* Makes a fetch request, calling the registered fetch request and response
* interceptors.
*
* @param {RequestInfo} info
* @param {RequestInit & Partial<CacheRequestExtension>} [init]
* @returns {Promise<Response>}
*/
async fetch(info, init) {
const request = /** @type {CacheRequest} */ (new Request(info, { ...init }));
request.cacheOptions = init?.cacheOptions;
request.params = init?.params;
// run request interceptors, returning directly and skipping the network
const interceptedRequestOrResponse = await this.__interceptRequest(request);
if (interceptedRequestOrResponse instanceof Response) {
// prevent network request, return cached response
return interceptedRequestOrResponse;
}
const response = /** @type {CacheResponse} */ (await fetch(interceptedRequestOrResponse));
response.request = interceptedRequestOrResponse;
const interceptedResponse = await this.__interceptResponse(response);
if (interceptedResponse.status >= 400 && interceptedResponse.status < 600) {
throw new AjaxFetchError(request, interceptedResponse);
}
return interceptedResponse;
}
/**
* Makes a fetch request, calling the registered fetch request and response
* interceptors. Encodes/decodes the request and response body as JSON.
*
* @param {RequestInfo} info
* @param {LionRequestInit} [init]
* @template T
* @returns {Promise<{ response: Response, body: T }>}
*/
async fetchJson(info, init) {
const lionInit = {
...init,
headers: {
...init?.headers,
accept: 'application/json',
},
};
if (lionInit?.body) {
// eslint-disable-next-line no-param-reassign
lionInit.headers['content-type'] = 'application/json';
lionInit.body = JSON.stringify(lionInit.body);
}
// Now that we stringified lionInit.body, we can safely typecast LionRequestInit back to RequestInit
const jsonInit = /** @type {RequestInit} */ (lionInit);
const response = await this.fetch(info, jsonInit);
let responseText = await response.text();
const { jsonPrefix } = this.__config;
if (typeof jsonPrefix === 'string' && responseText.startsWith(jsonPrefix)) {
responseText = responseText.substring(jsonPrefix.length);
}
try {
return {
response,
body: JSON.parse(responseText),
};
} catch (error) {
throw new Error(`Failed to parse response from ${response.url} as JSON.`);
}
}
/**
* @param {Request} request
* @returns {Promise<Request | Response>}
*/
async __interceptRequest(request) {
// run request interceptors, returning directly and skipping the network
// if a interceptor returns a Response
let interceptedRequest = request;
for (const intercept of this._requestInterceptors) {
// In this instance we actually do want to await for each sequence
// eslint-disable-next-line no-await-in-loop
const interceptedRequestOrResponse = await intercept(interceptedRequest);
if (interceptedRequestOrResponse instanceof Request) {
interceptedRequest = interceptedRequestOrResponse;
} else {
return this.__interceptResponse(interceptedRequestOrResponse);
}
}
return interceptedRequest;
}
/**
* @param {Response} response
* @returns {Promise<Response>}
*/
async __interceptResponse(response) {
let interceptedResponse = response;
for (const intercept of this._responseInterceptors) {
// In this instance we actually do want to await for each sequence
// eslint-disable-next-line no-await-in-loop
interceptedResponse = await intercept(interceptedResponse);
}
return interceptedResponse;
}
}