// ==UserScript==
// @name HttpRequest Library
// @namespace hoehleg.userscripts.private
// @version 0.3
// @description HttpRequest for any type of request and HttpRequestHTML to request webpage. Supports caching of responses for a given period and paging.
// @author Gerrit Höhle
//
// @grant GM_xmlhttpRequest
//
// ==/UserScript==
/* jslint esversion: 9 */
/**
* @typedef HttpRequestHtmlResponse
* @param {number} status
* @param {HTMLDocument} html
*/
/**
* @callback ResponseTransformer
* @param {Response} response
* @param {HttpRequestHtmlParams} httpRequestHtml
* @return {any}
*/
/**
* @callback ResponsePredicate
* @param {Response} response
* @param {HttpRequestHtmlParams} httpRequestHtml
* @return {boolean} true if next page is available
*/
/**
* @callback UrlPageNrConfigurator
* @param {string} original - the original url and parameters given
* @param {number} pageNr - the actual page
* @returns {String} the new url to request the actual page
*/
/**
* @callback ParamsPageNrConfigurator
* @param {Object.<string, string>} original - the original url and parameters given
* @param {number} pageNr - the actual page
* @returns {Object.<string, string>} the new url to request the actual page
*/
/**
* @typedef {Object} HttpRequestHtmlParams
* @property {string} url - url including path but without queryParams
* @property {Object.<string, string|number>} [params] - queryParams
* @property {number} [keepInCacheTimoutMs] - ms a request shall be cached
* @property {number} [pageNr] - pageNr to start with
* @property {number} [pagesMaxCount] - max number of pages to request
* @property {ResponseTransformer} [resultTransformer] - transforms a response, typically from parsing the response.html - Document
* @property {ResponsePredicate} [hasNextPage] - checks with the current response available, if a request for the next page shall be made
* @property {UrlPageNrConfigurator} [urlConfiguratorForPageNr] - rewrites the 'url' string that the actual page with given number is requested
* @property {ParamsPageNrConfigurator} [paramsConfiguratorForPageNr] - rewrites the params
*/
/**
* @typedef {Object} HttpRequestHtml
* @property {string} url
* @property {Object.<string, string>} params
* @property {number} keepInCacheTimoutMs
* @property {number} pageNr
* @property {number} pagesMaxCount - max number of pages to request
* @property {ResponseTransformer} resultTransformer
* @property {ResponsePredicate} hasNextPage
* @property {UrlPageNrConfigurator} [urlConfiguratorForPageNr]
* @property {ParamsPageNrConfigurator} [paramsConfiguratorForPageNr]
*/
const HttpRequest = (() => {
const urlWithParams = (url, paramsObject) => {
const params = Object.entries(paramsObject).map(([key, value]) => key + '=' + value).join('&');
return params.length ? url + '?' + params : url;
};
const responsesCache = new Map();
const requestKey = ({ method, url, params, data }) => `${method}:${urlWithParams(url, params)}:DATA:${data}`;
return class HttpRequest {
constructor({
method,
url,
headers = {},
data = '',
keepInCacheTimoutMs = 0,
params = {}
} = {}) {
/**
* @type {HttpRequestHtml}
* @public
*/
const thisParams = { method, url, headers, data, params, keepInCacheTimoutMs };
Object.assign(this, thisParams);
}
async send() {
if (!this.method || !this.url) {
return await Promise.reject("invalid request");
}
return await new Promise((resolve, reject) => {
let method, url, onload, onerror, headers, data;
method = this.method.toUpperCase();
url = this.url;
headers = this.headers;
data = this.data;
onload = (response) => {
switch (response.status) {
case 200:
if (this.keepInCacheTimoutMs) {
const key = requestKey(this);
responsesCache.set(key, response);
if (this.keepInCacheTimoutMs > 0) {
setTimeout(() => responsesCache.delete(key), this.keepInCacheTimoutMs);
}
}
break;
case 304:
if (this.isCached()) {
response = this.readFromCache();
response.status = 304;
}
break;
default:
reject(`Status: ${response.status}, Error: ${response.statusText}`);
return;
}
resolve(response);
};
onerror = (errorEvent) => {
reject("network error");
};
switch (method) {
case 'GET':
if (this.params) {
url = urlWithParams(url, this.params);
}
break;
case 'POST':
case 'PUT':
headers = Object.assign({ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, headers || {});
if (this.params) {
data = JSON.stringify({ ...data, ...this.params });
}
break;
}
url = encodeURI(url);
GM_xmlhttpRequest({ method, url, onload, onerror, headers, data });
});
}
isCached() {
return responsesCache.has(requestKey(this));
}
readFromCache() {
return responsesCache.get(requestKey(this));
}
static async send(...args) {
return await new HttpRequest(...args).send();
}
};
})();
class HttpRequestHtml extends HttpRequest {
/**
* @param {HttpRequestHtmlParams} param0
*/
constructor({
url,
params = {},
keepInCacheTimoutMs = 0,
pageNr = 0,
pagesMaxCount = 1,
resultTransformer = (resp, _httpRequestHtml) => resp,
hasNextPage = (_resp, _httpRequestHtml) => false,
urlConfiguratorForPageNr = (url, _pageNr) => url,
paramsConfiguratorForPageNr = (params, _pageNr) => params,
} = {}) {
super({ method: 'GET', url, params, keepInCacheTimoutMs });
Object.assign(this, {
pageNr,
pagesMaxCount: Math.max(0, pagesMaxCount),
resultTransformer,
hasNextPage,
urlConfiguratorForPageNr,
paramsConfiguratorForPageNr,
});
}
clone() {
return new HttpRequestHtml({ ...this });
}
/**
* @returns {Promise<HttpRequestHtmlResponse|object|Array<object>}
*/
async send() {
const results = [];
let response = null, requestForPage = null;
for (let pageNr = this.pageNr; pageNr < this.pageNr + this.pagesMaxCount; pageNr++) {
if (requestForPage && !this.hasNextPage(response, requestForPage)) {
break;
}
requestForPage = Object.assign(this.clone(), {
url: this.urlConfiguratorForPageNr(this.url, pageNr),
params: this.paramsConfiguratorForPageNr({ ...this.params }, pageNr)
});
response = await HttpRequest.prototype.send.call(requestForPage);
if (response.status == 200 || response.status == 304) {
response.html = new DOMParser().parseFromString(response.responseText, 'text/html');
}
const resultForPage = this.resultTransformer(response, requestForPage);
results.push(resultForPage);
}
return this.pagesMaxCount > 1 ? results : results[0];
}
/**
* @param {HttpRequestHtmlParams} param0
* @returns {Promise<HttpRequestHtmlResponse|object|Array<object>}
*/
static async send(...args) {
return await new HttpRequestHtml(...args).send();
}
}