/**
## /utils/xhr
Export the xhr method to mapp.utils{}.
@module /utils/xhr
*/
/**
@function xhr
@description
The params object/string for the xhr utility method is required.
The params are assumed to the request URL if provided as a string argument.
The request params and response are stored in a Map() if the cache flag is set in the params object argument.
If the debounce flag is set, requests will be delayed until after the specified time has elapsed since the last call in the same group.
Unlike throttle which aborts previous requests, debounce delays execution of the latest request.
The method is assumed to be 'POST' if a params.body is provided.
@param {Object} params The object containing the parameters.
@property {string} params.url The request URL.
@property {string} [params.method=GET] The request method.
@property {string} [params.responseType=json] The XHR responseType.
@property {Object} [params.requestHeader={'Content-Type': 'application/json'}] The XHR requestHeader.
@property {string} [params.body] A stringified request body for a 'POST' request.
@property {boolean} [params.resolveTarget] Whether the target instead of target.response should be resolved.
@property {boolean} [params.cache] Whether the response should be cached in a Map().
@property {boolean|number|object} [params.debounce] Whether the request should be debounced. Can be true (300ms default), a number (milliseconds), or an object (group name with 300ms default). Format: "{key: "name", delay: 300}.
@returns {Promise} A promise that resolves with the XHR.
*/
const requestMap = new Map();
export function xhr(params) {
return new Promise((resolve, reject) => {
if (!params) {
console.error('xhr params are falsy.');
return reject(new Error('xhr params are required'));
}
params = typeof params === 'string' ? { url: params } : { ...params };
if (!params.url) {
console.error('no xhr request url has been provided.');
return reject(new Error('no xhr request url has been provided.'));
}
params.method ??= params.body ? 'POST' : 'GET';
params.responseType ??= 'json';
const xhr = new XMLHttpRequest();
xhr.open(params.method, params.url, true);
if (params.requestHeader !== null) {
const headers = {
'Content-Type': 'application/json',
...params.requestHeader,
};
Object.entries(headers).forEach(([k, v]) => xhr.setRequestHeader(k, v));
}
xhr.responseType = params.responseType;
if (params.cache && requestMap.has(params.url)) {
const cached = requestMap.get(params.url);
return resolve(params.resolveTarget ? cached : cached.response);
}
xhr.onload = () => {
if (xhr.status >= 400) {
return reject(new Error(`HTTP ${xhr.status}`));
}
const result = params.resolveTarget ? xhr : xhr.response;
if (params.cache) {
requestMap.set(
params.url,
params.resolveTarget ? xhr : { response: result },
);
}
resolve(result);
};
if (!debounce(xhr, params, reject)) {
// Send request if not debounced.
xhr.send(params.body);
}
});
}
/**
@function debounce
@description
A request will not be debounced if the params.debounce property is falsy [undefined].
The debounce property will be assigned as delay [ms] value if numeric.
The debounce property will be assigned as key for the debounce.
The delay will be provided to timeout sending the xhr request.
The timeout will be stored in the debounceMap with the associated params.key property. The timeout will be cleared should a request with the same key property be debounced before the timeout function is executed.
@param {object} xhr XMLHttpRequest.
@param {object} params Request parameters.
@property {boolean} [params.debounce] Request should be debounced.
@property {string} [params.key='global'] Key for request in debounceMap.
@property {boolean} [params.delay=300] Delay in ms for debounce timeout.
@param {function} reject The request promise will be rejected if debounced.
@returns {boolean} true if request will be debounced.
*/
const debounceMap = new Map();
function debounce(xhr, params, reject) {
if (!params.debounce) return;
if (typeof params.debounce === 'number') {
params.delay = params.debounce;
}
params.delay ??= 300;
if (typeof params.debounce === 'string') {
params.key ??= params.debounce;
}
params.key ??= 'global';
const existing = debounceMap.get(params.key);
if (existing) {
clearTimeout(existing.timeoutId);
// Debounced requests should not be rejected with an Error.
existing.reject('Request Debounced');
}
const timeoutId = setTimeout(() => {
debounceMap.delete(params.key);
xhr.send(params.body);
}, params.delay);
debounceMap.set(params.key, { timeoutId, reject });
// This request will be debounced.
return true;
}