import { useCallback, useEffect, useRef, useState } from 'react' import { api } from './api.js' // usePolled runs `fn` now and every `everyMs`, and is careful about the two // things every screen would otherwise get wrong on its own: overlapping // requests when the server is slower than the interval, and setting state // after the component has gone. export function usePolled(fn, everyMs, deps = []) { const [data, setData] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(true) const inFlight = useRef(false) const alive = useRef(true) const run = useCallback(async () => { if (inFlight.current) return inFlight.current = true try { const out = await fn() if (!alive.current) return setData(out) setError(null) } catch (err) { if (!alive.current) return setError(err) } finally { inFlight.current = false if (alive.current) setLoading(false) } // eslint-disable-next-line react-hooks/exhaustive-deps }, deps) useEffect(() => { alive.current = true run() if (!everyMs) return () => { alive.current = false } const t = setInterval(run, everyMs) return () => { alive.current = false; clearInterval(t) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [run, everyMs]) return { data, error, loading, reload: run } } // useAuthedImage loads a picture that needs the session's bearer token and // hands back a URL an can use. // // Two things it has to get right, and both were bugs the first time something // like it was written elsewhere in this app: // // * REVOKE. An object URL pins the blob in memory until it is revoked, and // this component re-renders on every poll. Without the cleanup a camera // screen left open for an afternoon holds hundreds of copies of the same // photograph. // * Key on the URL, not on the object. `snapshot` is a fresh object on every // poll, so an effect depending on it would re-fetch 90 KB per camera every // few seconds; the URL only changes when the picture actually does. export function useAuthedImage(url) { const [src, setSrc] = useState(null) useEffect(() => { if (!url) { setSrc(null); return } let alive = true let objectURL = null api.cameraSnapshot(url) .then(u => { if (!alive) { URL.revokeObjectURL(u); return } objectURL = u setSrc(u) }) .catch(() => { if (alive) setSrc(null) }) return () => { alive = false if (objectURL) URL.revokeObjectURL(objectURL) } }, [url]) return src }