cancel requests if unmounted before completion

Massimo Melina committed Feb 3, 2023 at 18:43 UTC 06612c230e5c4df6114415e2bb014c80d41b55b9
1 file changed +18 -7
admin/src/api.ts
+18 -7
@@ -27,7 +27,7 @@ const PREFIX = '/~/api/'
27 const timeoutByApi: Dict = {
28 get_status: 20 // can be lengthy on slow machines because of the find-process-on-busy-port feature
29 }
30 -export function apiCall(cmd: string, params?: Dict, { timeout=undefined }={}) : Promise<any> {
30 +export function apiCall(cmd: string, params?: Dict, { timeout=undefined }={}) {
31 const csrf = getCsrf()
32 if (csrf)
33 params = { csrf, ...params }
@@ -35,7 +35,7 @@ export function apiCall(cmd: string, params?: Dict, { timeout=undefined }={}) :
35 const controller = new AbortController()
36 if (timeout !== false)
37 setTimeout(() => controller.abort('timeout'), 1000*(timeoutByApi[cmd] ?? timeout ?? 10))
38 - return fetch(PREFIX+cmd, {
38 + return Object.assign(fetch(PREFIX+cmd, {
39 method: 'POST',
40 headers: { 'content-type': 'application/json' },
41 signal: controller.signal,
@@ -55,6 +55,10 @@ export function apiCall(cmd: string, params?: Dict, { timeout=undefined }={}) :
55 if (err?.message?.includes('fetch'))
56 throw Error("Network error")
57 throw err
58 + }), {
59 + abort() {
60 + controller.abort('cancel')
61 + }
62 })
63 }
64
@@ -68,15 +72,22 @@ export function useApi<T=any>(cmd: string | Falsy, params?: object) : [T | undef
72 const [ret, setRet] = useStateMounted<T | undefined>(undefined)
73 const [err, setErr] = useStateMounted<Error | undefined>(undefined)
74 const [forcer, setForcer] = useStateMounted(0)
71 - const loadingRef = useRef(false)
75 + const loadingRef = useRef<ReturnType<typeof apiCall>>()
76 useEffect(()=>{
77 + loadingRef.current?.abort()
78 setRet(undefined)
79 setErr(undefined)
80 if (!cmd) return
76 - loadingRef.current = true
77 - apiCall(cmd, params)
78 - .then(setRet, setErr)
79 - .finally(()=> loadingRef.current = false)
81 + let aborted = false
82 + const req = apiCall(cmd, params)
83 + const wholePromise = req.then(x => aborted || setRet(x), x => aborted || setErr(x))
84 + .finally(()=> loadingRef.current = undefined)
85 + loadingRef.current = Object.assign(wholePromise, {
86 + abort() {
87 + aborted = true
88 + req.abort()
89 + }
90 + })
91 }, [cmd, JSON.stringify(params), forcer]) //eslint-disable-line -- json-ize to detect deep changes
92 const reload = useCallback(()=> loadingRef.current || setForcer(v => v+1), [setForcer])
93 return [ret, err, reload]