UNPKG

flechette

Version:

A highly configurable wrapper for Fetch API that supports programmatic retries and completely obfuscates promise handling.

320 lines (319 loc) 12.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const flechette_1 = require("./flechette"); const utils_1 = require("./utils"); exports.send = (args, successFunc, failureFunc, waitingFunc) => { if (Array.isArray(args)) { var timeout = 0; // eslint-disable-line no-var var maxRetries = 0; // eslint-disable-line no-var var sendFunc; // eslint-disable-line no-var var abortFunc; // eslint-disable-line no-var const instancesRef = []; args.forEach(a => { const flechetteInstance = flechette_1.getFlechetteInstance(a.instanceName); instancesRef.push(flechetteInstance); exports.initialArgSetup(flechetteInstance, a); // find the biggest timeout and retry count amoung each flechette instance if (flechetteInstance.timeout > timeout) { timeout = flechetteInstance.timeout; } if (flechetteInstance.maxTimeoutRetryCount > maxRetries) { maxRetries = flechetteInstance.maxTimeoutRetryCount; } }); sendFunc = exports.sendAndEvaluateMultiple; abortFunc = () => { instancesRef.forEach(f => { // perform all abort actions f.abortCurrentFetch(); }); }; } else { const flechetteInstance = flechette_1.getFlechetteInstance(args.instanceName); exports.initialArgSetup(flechetteInstance, args); timeout = flechetteInstance.timeout; maxRetries = flechetteInstance.maxTimeoutRetryCount; sendFunc = exports.sendAndEvaluate; abortFunc = () => { flechetteInstance.abortCurrentFetch(); }; } exports.sendRetryOrAbort(args, timeout, maxRetries, abortFunc, sendFunc, successFunc, failureFunc, waitingFunc); }; exports.initialArgSetup = (flechetteInstance, args) => { args.signal = flechetteInstance.abortController.signal; if (args.path && !args.path.includes(flechetteInstance.baseUrl)) { args.path = flechetteInstance.baseUrl + args.path; } // if the SendArgs has headers, use those. Otherwise, use flechette's args.headers = utils_1.combineHeaders(args.headers, flechetteInstance.headers); }; const buildAbortResponse = (args) => { return { response: "Request Timed Out", sent: args, statusCode: 0, success: false }; }; exports.sendRetryOrAbort = (args, timeout, maxRetryCount, abortFunc, sendAndEvalFunc, successFunc, failureFunc, waitingFunc) => { const t = []; // will be the timeout handlers const sf = res => { // wrap this so our timeout is cleared if sendAndEvaluate finishes t.forEach(o => { clearTimeout(o); }); successFunc(res); }; const ff = res => { t.forEach(o => { clearTimeout(o); }); failureFunc(res); }; if (typeof timeout === "number" && timeout > 0) { // need to create all timeouts upfront so they can easily be cleared on success of one // first, create the final timeout t.push(setTimeout(() => { abortFunc(); // since this is the final timeout, run failureFunc var failureResponse; if (Array.isArray(args)) { failureResponse = []; args.forEach(a => { failureResponse.push(buildAbortResponse(a)); }); } else { failureResponse = buildAbortResponse(args); } waitingFunc && waitingFunc(false); failureFunc(failureResponse); }, timeout * maxRetryCount + timeout)); for (var i = 1; i <= maxRetryCount; ++i) { // then create the abort + retry scenarios t.push(setTimeout(() => { abortFunc(); console.warn("Timeout limit reached without a network response. Retrying..."); sendAndEvalFunc(args, sf, ff, waitingFunc); }, timeout * i)); } } sendAndEvalFunc(args, sf, ff, waitingFunc); }; exports.sendAndEvaluate = (args, successFunc, failureFunc, waitingFunc) => { if (!Array.isArray(args)) { // here we must retrieve the flechette instance again to decouple the values // that we use for initial setup from the ones e send with. This is to help // support multiSends waitingFunc && waitingFunc(true); const response = exports.fetchWrap(args); Promise.resolve(response).then(res => { const flechetteInstance = flechette_1.getFlechetteInstance(args.instanceName); const reponse = exports.evaluateResponse(res, flechetteInstance); if (reponse.success) { waitingFunc && waitingFunc(false); successFunc(reponse); } else { // retry func encapsulates the whole retry process // if it returns false, it means we're not retrying so move onto failure if (!exports.retry(reponse, flechetteInstance, successFunc, failureFunc, waitingFunc)) { waitingFunc && waitingFunc(false); failureFunc(reponse); } } }); } else { throw new Error("Cannot use array of SendArgs"); } }; exports.sendAndEvaluateMultiple = (args, successFunc, failureFunc, waitingFunc) => { if (Array.isArray(args)) { const promises = []; const responses = []; // var since retry may change waitingFunc && waitingFunc(true); args.forEach(a => { promises.push(exports.fetchWrap(a)); }); Promise.all(promises).then(res => { res.forEach(r => { const flechetteInstance = flechette_1.getFlechetteInstance(r.sent.instanceName); const ev = exports.evaluateResponse(r, flechetteInstance); responses.push(ev); }); if (responses.every(r => r.success)) { waitingFunc && waitingFunc(false); successFunc(responses); } else { if (!exports.retryMultiple(responses, successFunc, failureFunc, waitingFunc)) { waitingFunc && waitingFunc(false); failureFunc(responses); } } }); } else { throw new Error("Must use array of SendArgs"); } }; const replaceOriginalPath = (path, ra) => { var originalPathsToIgnore; if (Array.isArray(ra.pathsToIgnore)) { originalPathsToIgnore = [...ra.pathsToIgnore]; ra.pathsToIgnore.push(path); } else { originalPathsToIgnore = undefined; ra.pathsToIgnore = []; ra.pathsToIgnore.push(path); } return originalPathsToIgnore; }; exports.retry = (response, flechetteInstance, successFunc, failureFunc, waitingFunc) => { // this returns a array containing the desired retry action, if one exists, // and its index if it comes from the global retryActions. const ra = utils_1.determineRetryAction(response.statusCode, response.sent, flechetteInstance.retryActions); if (ra[0] !== undefined) { var finalize = () => { waitingFunc && waitingFunc(false); }; if (ra[1] > -1) { // this indicates it was in the global retry list, // so we'll need to remove it and add it back in when all steps are done. const originalPathsToIgnore = replaceOriginalPath(response.sent.path, ra[0]); finalize = () => { waitingFunc && waitingFunc(false); flechetteInstance.retryActions[ra[1]].pathsToIgnore = originalPathsToIgnore; }; } const sf = (rVal) => { finalize(); successFunc(rVal); }; const ff = (rVal) => { finalize(); failureFunc(rVal); }; // var retryAction determine when waiting is over ra[0].action(response, sf, ff); return true; } return false; }; exports.retryMultiple = (responses, successFunc, failureFunc, waitingFunc) => { var retVal = false; const promises = []; const localResponses = [...responses]; // make a copy since we'll mutate the index const actionsToRebuild = []; localResponses.forEach(r => { if (!r.success) { const fi = flechette_1.getFlechetteInstance(r.sent.instanceName); const ra = utils_1.determineRetryAction(r.statusCode, r.sent, fi.retryActions); if (ra[0] !== undefined) { retVal = true; // splice out the retry from the responses const i = responses.findIndex(res => { return (res.sent.path === r.sent.path && res.sent.instanceName === r.sent.instanceName); }); responses.splice(i, 1); if (ra[1] > -1) { // this means we've got a global action. // we need to add this path to the action's pathsToIgnore const originalPathsToIgnore = replaceOriginalPath(r.sent.path, ra[0]); // now add this to a list to change back later const atr = actionsToRebuild.find(a => { return (a.code === ra[0].code && a.instanceName === fi.instanceName); }); if (!atr) { actionsToRebuild.push({ code: ra[0].code, instanceName: fi.instanceName, pathsToIgnore: originalPathsToIgnore }); } } // we can't guarantee that all RetryActions will do another send // so retries need to be wrapped in a promise that resolves // when all the success or failure funcs are fired. promises.push(new Promise(resolve => { const sf = (rVal) => { resolve(rVal); }; const ff = (rVal) => { resolve(rVal); }; ra[0].action(r, sf, ff); })); } } }); if (promises.length > 0) { Promise.all(promises).then(res => { // add the new responses from retry actions back to existing list responses = responses.concat(res); // rebuild any actions we've removed actionsToRebuild.forEach(atr => { const fi = flechette_1.getFlechetteInstance(atr.instanceName); const retA = fi.retryActions.find(ra => { return ra.code === atr.code; }); retA && (retA.pathsToIgnore = atr.pathsToIgnore); }); // finally, call the success or failure func waitingFunc && waitingFunc(false); if (responses.every(r => r.success)) { successFunc(responses); } else { failureFunc(responses); } }); } return retVal; }; exports.evaluateResponse = (resp, f) => { var s = false; var r; try { // try to parse it to an object if it is json r = JSON.parse(resp.response); } catch (_a) { r = resp.response; } f.successCodes.some(sc => { s = utils_1.checkCodes(resp.statusCode, sc); return s; }); return { response: r, sent: resp.sent, statusCode: resp.statusCode, success: s }; }; exports.fetchWrap = (args) => { return fetch(args.path, args) .then(response => { return Promise.resolve(response.text()).then(res => ({ d: res, sc: response.status })); }) .then(res => { return { response: res.d, statusCode: res.sc, sent: args }; }) .catch(err => { var r = "Unknown Error: " + err; if (err.name === "AbortError") { r = "Request Aborted"; } return { response: r, statusCode: 0, sent: args }; }); };