I am trying to write a promise that will be consumed by a react component. I am using lodash
for throttling.
The idea is the component using this function should transparently invoke this function as many time as it wants without causing too many invocations.
let throttledApi = throttle(() => {
// tracking number of attempts
let attempts = 0;
const retryableFn = () => {
// do I need a return before api here?
api()
.then((response) => {
return response;
})
.catch((error) => {
attempts++;
if (attempts === 3) throw error; // throw to the component using this functino
// same question as above for below line
else retryableFn();
});
};
}, 60 * 1000); // 1 minute
// I am using it like this
throttledFn = throttledApi();
// How do I properly use the throttled instance
// How do I try/catch properly.
// Is this thenable?
throttledFn();
There are a couple of issues, but even when those are fixed, you actually still need a promise from the throttled function, which lodash does not provide.
The issues:
retryableFn
is never called. The function you have passed to throttle
only defines that function, but then does nothing with it.
throttledApi
is the throttled function, so doing throttledFn = throttledApi()
is actually executing that throttled function and assigning its return value (which is undefined
) to throttledFn
. You should just work with throttledApi
and forget about throttledFn
The function that lodash's _.throttle
creates for you doesn't return a value, so it will not be a promise (thenable) as you understandably prefer.
The function returned by the lodash throttle
function does not return a value, yet in your scenario it would be good to still get a promise from it. If the request comes too fast after another one, then this promise should take some time (including the delay) to resolve.
You can write your own throttleAsync
function. Here is one way it could be done (I've used 4 seconds as cooldown):
// Generic function
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// Generic function
const retryAsync = (asyncFunc, maxNumAttempts, timeout) =>
asyncFunc().catch(async (error) => {
if (--maxNumAttempts <= 0) throw error;
await delay(timeout);
return retryAsync(asyncFunc, maxNumAttempts, timeout);
});
// Generic function
const throttleAsync = (asyncFunc, coolDown) => {
let coolDownPromise = Promise.resolve();
let responsePromise = null;
return async function(...args) {
console.log("Verify cooldown");
await coolDownPromise;
if (responsePromise) return responsePromise;
console.log("Cooldown is over...");
coolDownPromise = delay(coolDown);
responsePromise = asyncFunc.call(this, ...args);
responsePromise.finally(() => responsePromise = null).catch(() => 0);
return responsePromise;
};
}
// Specific function (mock)
const api = async function () {
console.log("API called");
await delay(100);
if (Math.random() < .5) {
console.log("API bumped into an error.");
throw "API bumped into an error.";
}
console.log("Response received from API!");
return { data: new Date().toLocaleTimeString() };
}
// throttle the specific function to have 4 seconds cooldown
const throttledRequest = throttleAsync(() => retryAsync(api, 3, 200), 4_000);
async function refresh() {
try {
const response = await throttledRequest();
output.textContent = "The response is: " + JSON.stringify(response);
} catch (error) {
output.textContent = "Giving up. The error is: " + JSON.stringify(error.message ?? error);
}
}
// I/O handling
const [button, output] = document.querySelectorAll("button, div");
button.addEventListener("click", refresh);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
<button>Make Request</button>
<div></div>