{"id":667,"date":"2020-12-22T20:48:32","date_gmt":"2020-12-22T20:48:32","guid":{"rendered":"http:\/\/www.ishygddt.xyz\/~blog\/?p=667"},"modified":"2022-03-08T19:00:35","modified_gmt":"2022-03-08T19:00:35","slug":"js-tail-call-optimized-recursive-settimeout","status":"publish","type":"post","link":"http:\/\/www.ishygddt.xyz\/~blog\/2020\/12\/js-tail-call-optimized-recursive-settimeout","title":{"rendered":"JS: Tail-call optimized recursive setTimeout"},"content":{"rendered":"<p>If you're writing some code on a time-delayed-loop in JavaScript, there are 2 main options:<\/p>\n<ul>\n<li><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/WindowOrWorkerGlobalScope\/setInterval\"><code class=\"language-javascript\" data-line=\"\">setInterval<\/code><\/a><\/li>\n<li><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/WindowOrWorkerGlobalScope\/setTimeout\"><code class=\"language-javascript\" data-line=\"\">setTimeout<\/code><\/a>, <a href=\"https:\/\/medium.com\/@devinmpierce\/recursive-settimeout-8eb953b02b98\">recursively<\/a><\/li>\n<\/ul>\n<p>However, each has a potential downside:<\/p>\n<ul>\n<li><code class=\"language-javascript\" data-line=\"\">setInterval<\/code> <em>schedules<\/em> 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.<\/li>\n<li>Recursive <code class=\"language-javascript\" data-line=\"\">setTimeout<\/code> <a href=\"https:\/\/stackoverflow.com\/a\/37224563\/1874170\"><span style=\"text-decoration: underline\">is<\/span> a memory leak<\/a> when run for unbounded time on <a href=\"https:\/\/bugzilla.mozilla.org\/show_bug.cgi?id=723959\">Firefox<\/a> and <a href=\"https:\/\/bugs.chromium.org\/p\/v8\/issues\/detail?id=4698\">Chrome<\/a> (though, interestingly: not in <a href=\"https:\/\/bugs.webkit.org\/show_bug.cgi?id=148663\">Safari <em>et. al.<\/em><\/a>!).<\/li>\n<\/ul>\n<p>If you want \"the best of both worlds\": code that waits for invocation <em>n<\/em> to finish before starting invocation <em>n+1<\/em>, but does not leak memory on Firefox and Chrome, you'll have to take advantage of <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Operators\/await\"><code class=\"language-javascript\" data-line=\"\">await<\/code><\/a> to logically emulate the tail-recursion iteratively:<\/p>\n<pre><code class=\"language-javascript\" data-line=\"\">var [setRepeatedTimeout, clearRepeatedTimeout] = (() =&gt; {\n\tconst asleep = (delay =&gt; new Promise(resolve =&gt; setTimeout(resolve, delay)));\n\tconst repeatedTimeoutIntervals = [];\n\n\tfunction setRepeatedTimeout(f, delay, ...arguments) {\n\t\t\/\/Like setInterval, but waits for an invocation to complete before scheduling the next one\n\t \t\/\/(Bonus: supports both classic and async functions)\n\t\tconst intervalID = repeatedTimeoutIntervals.push(delay) - 1;\n\t\t(async () =&gt; {\n\t\t\tawait asleep(delay);\n\t\t\twhile(intervalID in repeatedTimeoutIntervals) {\n\t\t\t\tawait f(...arguments);\n\t\t\t\tawait asleep(delay);\n\t\t\t}\n\t\t})();\n\t\treturn intervalID;\n\t}\n\n\tfunction clearRepeatedTimeout(intervalID) {\n\t\t\/\/Clears loops set by setInterval()\n\t\tdelete repeatedTimeoutIntervals[intervalID];\n\t}\n\n\treturn [setRepeatedTimeout, clearRepeatedTimeout];\n})();<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>If you're writing some code on a time-delayed-loop in JavaScript, there are 2 main options: setInterval setTimeout, recursively 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, &hellip;<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[97],"tags":[40,144],"class_list":["post-667","post","type-post","status-publish","format-standard","hentry","category-original-content","tag-javascript","tag-programming"],"_links":{"self":[{"href":"http:\/\/www.ishygddt.xyz\/~blog\/wp-json\/wp\/v2\/posts\/667","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/www.ishygddt.xyz\/~blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.ishygddt.xyz\/~blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.ishygddt.xyz\/~blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"http:\/\/www.ishygddt.xyz\/~blog\/wp-json\/wp\/v2\/comments?post=667"}],"version-history":[{"count":26,"href":"http:\/\/www.ishygddt.xyz\/~blog\/wp-json\/wp\/v2\/posts\/667\/revisions"}],"predecessor-version":[{"id":1318,"href":"http:\/\/www.ishygddt.xyz\/~blog\/wp-json\/wp\/v2\/posts\/667\/revisions\/1318"}],"wp:attachment":[{"href":"http:\/\/www.ishygddt.xyz\/~blog\/wp-json\/wp\/v2\/media?parent=667"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.ishygddt.xyz\/~blog\/wp-json\/wp\/v2\/categories?post=667"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.ishygddt.xyz\/~blog\/wp-json\/wp\/v2\/tags?post=667"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}