import * as plugins from './webrequest.plugins.js'; export interface IWebrequestContructorOptions { logging?: boolean; } /** * web request */ export class WebRequest { public cacheStore = new plugins.webstore.WebStore({ dbName: 'webrequest', storeName: 'webrequest', }); public options: IWebrequestContructorOptions; constructor(public optionsArg: IWebrequestContructorOptions = {}) { this.options = { logging: true, ...optionsArg, }; } public async getJson(urlArg: string, useCacheArg: boolean = false) { const response: Response = await this.request(urlArg, { method: 'GET', useCache: useCacheArg, }); const responseText = await response.text(); const responseResult = plugins.smartjson.parse(responseText); return responseResult; } /** * postJson */ public async postJson(urlArg: string, requestBody?: any, useCacheArg: boolean = false) { const response: Response = await this.request(urlArg, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: plugins.smartjson.stringify(requestBody), useCache: useCacheArg, }); const responseText = await response.text(); const responseResult = plugins.smartjson.parse(responseText); return responseResult; } /** * put js */ public async putJson(urlArg: string, requestBody?: any, useStoreAsFallback: boolean = false) { const response: Response = await this.request(urlArg, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: plugins.smartjson.stringify(requestBody), }); const responseText = await response.text(); const responseResult = plugins.smartjson.parse(responseText); return responseResult; } /** * put js */ public async deleteJson(urlArg: string, useStoreAsFallback: boolean = false) { const response: Response = await this.request(urlArg, { headers: { 'Content-Type': 'application/json', }, method: 'GET', }); const responseText = await response.text(); const responseResult = plugins.smartjson.parse(responseText); return responseResult; } public async request( urlArg: string, optionsArg: { method: 'GET' | 'POST' | 'PUT' | 'DELETE'; body?: any; headers?: HeadersInit; useCache?: boolean; timeoutMs?: number; } ) { optionsArg = { timeoutMs: 60000, useCache: false, ...optionsArg, }; let controller = new AbortController(); plugins.smartdelay.delayFor(optionsArg.timeoutMs).then(() => { controller.abort(); }); let cachedResponseDeferred = plugins.smartpromise.defer(); let cacheUsed = false; if (optionsArg.useCache && (await this.cacheStore.check(urlArg))) { const responseBuffer: ArrayBuffer = await this.cacheStore.get(urlArg); cachedResponseDeferred.resolve(new Response(responseBuffer, {})); } else { cachedResponseDeferred.resolve(null); } let response: Response = await fetch(urlArg, { signal: controller.signal, method: optionsArg.method, headers: { ...(optionsArg.headers || {}), }, body: optionsArg.body, }) .catch(async (err) => { if (optionsArg.useCache && (await cachedResponseDeferred.promise)) { cacheUsed = true; const cachedResponse = cachedResponseDeferred.promise; return cachedResponse; } else { return err; } }); if (optionsArg.useCache && (await cachedResponseDeferred.promise) && response.status === 500) { cacheUsed = true; response = await cachedResponseDeferred.promise; } if (!cacheUsed && optionsArg.useCache && response.status < 300) { const buffer = await response.clone().arrayBuffer(); await this.cacheStore.set(urlArg, buffer); } this.log(`${urlArg} answers with status: ${response.status}`); return response; } /** * a multi endpoint, fault tolerant request function */ public async requestMultiEndpoint( urlArg: string | string[], optionsArg: { method: 'GET' | 'POST' | 'PUT' | 'DELETE'; body?: any; headers?: HeadersInit; } ): Promise { let allUrls: string[]; let usedUrlIndex = 0; // determine what we got if (Array.isArray(urlArg)) { allUrls = urlArg; } else { allUrls = [urlArg]; } const requestHistory: string[] = []; // keep track of the request history const doHistoryCheck = async ( // check history for a historyEntryTypeArg: string ) => { requestHistory.push(historyEntryTypeArg); if (historyEntryTypeArg === '429') { console.log('got 429, so waiting a little bit.'); await plugins.smartdelay.delayFor(Math.floor(Math.random() * (2000 - 1000 + 1)) + 1000); // wait between 1 and 10 seconds } let numOfHistoryType = 0; for (const entry of requestHistory) { if (entry === historyEntryTypeArg) numOfHistoryType++; } if (numOfHistoryType > 2 * allUrls.length * usedUrlIndex) { usedUrlIndex++; } }; // lets go recursive const doRequest = async (urlToUse: string): Promise => { if (!urlToUse) { throw new Error('request failed permanently'); } this.log(`Getting ${urlToUse} with method ${optionsArg.method}`); const response = await fetch(urlToUse, { method: optionsArg.method, headers: { 'Content-Type': 'application/json', ...(optionsArg.headers || {}), }, body: optionsArg.body, }); this.log(`${urlToUse} answers with status: ${response.status}`); if (response.status >= 200 && response.status < 300) { return response; } else { // lets perform a history check to determine failed urls await doHistoryCheck(response.status.toString()); // lets fire the request const result = await doRequest(allUrls[usedUrlIndex]); return result; } }; const finalResponse: Response = await doRequest(allUrls[usedUrlIndex]); return finalResponse; } public log(logArg: string) { if (this.options.logging) { console.log(logArg); } } }