20219: Improve useAsyncInterval testability, add unit test
[arvados-workbench2.git] / src / common / use-async-interval.ts
index 8951a9b0d3caa9d77d6f08e6eb9064eff6abbc79..3be7309a38d481ceb8eb0f5ac91007f8256641cf 100644 (file)
@@ -2,34 +2,44 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-var react = require("react");
+import React from "react";
 
 export const useAsyncInterval = function (callback, delay) {
-    const savedCallback = react.useRef();
-    const active = react.useRef(false);
+    const ref = React.useRef<{cb: () => Promise<any>, active: boolean}>({
+        cb: async () => {},
+        active: false}
+    );
 
     // Remember the latest callback.
-    react.useEffect(() => {
-        savedCallback.current = callback;
+    React.useEffect(() => {
+        ref.current.cb = callback;
     }, [callback]);
     // Set up the interval.
-    react.useEffect(() => {
-        // useEffect doesn't like async callbacks (https://github.com/facebook/react/issues/14326) so create nested async callback
-        (async () => {
-            // Make tick() async
-            async function tick() {
-                if (active.current) {
-                    // If savedCallback is not set yet, no-op until it is
-                    savedCallback.current && await savedCallback.current();
+    React.useEffect(() => {
+        function tick() {
+            if (ref.current.active) {
+                // Wrap execution chain with promise so that execution errors or
+                //   non-async callbacks still fall through to .finally, avoids breaking polling
+                new Promise((resolve) => {
+                    return resolve(ref.current.cb());
+                }).then(() => {
+                    // Promise succeeded
+                    // Possibly implement back-off reset
+                }).catch(() => {
+                    // Promise rejected
+                    // Possibly implement back-off in the future
+                }).finally(() => {
                     setTimeout(tick, delay);
-                }
+                });
             }
-            if (delay !== null) {
-                active.current = true;
-                setTimeout(tick, delay);
-            }
-        })(); // Call nested async function
-        // We return the teardown function here since we can't from inside the nested async callback
-        return () => {active.current = false;};
+        }
+        if (delay !== null) {
+            ref.current.active = true;
+            setTimeout(tick, delay);
+        }
+        // Suppress warning about cleanup function - can be ignored when variables are unrelated to dom elements
+        //   https://github.com/facebook/react/issues/15841#issuecomment-500133759
+        // eslint-disable-next-line
+        return () => {ref.current.active = false;};
     }, [delay]);
 };