If you're writing some code on a time-delayed-loop in JavaScript, there are 2 main options:

However, each has a potential downside:

  • setInterval schedules the code to run at the given interval, regardless of whether the last invocation has completed. This means that, if the function takes longer than the delay to execute, it'll schedule the next invocation before the current invocation is finished, which can lead to an avalanche of calls being piled on without bound if the functions continue to take equivalent amounts of time to run and aren't being delegated to truly concurrent threads.
  • Recursive setTimeout is a memory leak when run for unbounded time on Firefox and Chrome (though, interestingly: not in Safari et. al.!).

If you want "the best of both worlds": code that waits for invocation n to finish before starting invocation n+1, but does not leak memory on Firefox and Chrome, you'll have to take advantage of await to logically emulate the tail-recursion iteratively:

var [setRepeatedTimeout, clearRepeatedTimeout] = (() => {
	const asleep = (delay => new Promise(resolve => setTimeout(resolve, delay)));
	const repeatedTimeoutIntervals = [];

	function setRepeatedTimeout(f, delay, ...arguments) {
		//Like setInterval, but waits for an invocation to complete before scheduling the next one
	 	//(Bonus: supports both classic and async functions)
		const mySemaphores = {notAborted: true};
		const intervalID = repeatedTimeoutIntervals.push(mySemaphores) - 1;
		(async () => {
			await asleep(delay);
			while(mySemaphores.notAborted) {
				await f(...arguments);
				await asleep(delay);
			}
			delete repeatedTimeoutIntervals[intervalID];
		})();
		return intervalID;
	}

	function clearRepeatedTimeout(intervalID) {
		//Clears loops set by setInterval()
		repeatedTimeoutIntervals[intervalID].notAborted = false;
	}

	return [setRepeatedTimeout, clearRepeatedTimeout];
})();

Leave a Reply

Your email address will not be published. Required fields are marked *