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
JavaScript
;
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 };
});
};