📌 Description
useAsync (src/hooks/useAsync.ts) implements refresh() by pushing a resolve callback onto refreshWaiters.current and bumping nonce to re-trigger the fetch effect; the effect's .finally() is supposed to drain and call all queued waiters once the fetch settles. However, that .finally() starts with if (controller.signal.aborted) return; — if the component unmounts (or reload/refresh fires again) before the in-flight fetch completes, the effect's cleanup aborts the controller, and the .finally() handler exits early WITHOUT resolving the waiters that were queued for that fetch. Any caller that did await refresh() — e.g. SettlementDetail's run() (src/components/SettlementDetail.tsx), which calls await refresh() after a successful execute/cancel — is left with a promise that never resolves if the component unmounts in that window, silently leaking a pending promise.
🧩 Requirements and context
refresh()'s returned promise should always eventually settle (resolve) — either when its triggered fetch completes, or, if the component/effect is torn down first (unmount, or a newer reload/refresh superseding it), resolve immediately at cleanup time rather than never.
- The existing "resolves when the fetch settles, never rejects" contract documented on
refresh should be preserved for the normal (non-aborted) case.
- No change to
reload's behavior (it doesn't return a promise and isn't affected).
🛠️ Suggested execution
- Update the effect's cleanup function in
src/hooks/useAsync.ts to drain and resolve any still-pending refreshWaiters (in addition to the abort call), so an unmount or a superseding reload/refresh doesn't strand earlier waiters.
- Extend
src/hooks/useAsync.test.ts with a test that calls refresh(), unmounts the hook before the mocked load promise resolves, and asserts the refresh() promise still settles.
✅ Acceptance criteria
🔒 Security notes
No new attack surface; a promise-lifecycle correctness fix preventing a dangling await.
📋 Guidelines
- Minimum 95% test coverage
- Clear documentation
- Timeframe: 96 hours
📌 Description
useAsync(src/hooks/useAsync.ts) implementsrefresh()by pushing aresolvecallback ontorefreshWaiters.currentand bumpingnonceto re-trigger the fetch effect; the effect's.finally()is supposed to drain and call all queued waiters once the fetch settles. However, that.finally()starts withif (controller.signal.aborted) return;— if the component unmounts (orreload/refreshfires again) before the in-flight fetch completes, the effect's cleanup aborts the controller, and the.finally()handler exits early WITHOUT resolving the waiters that were queued for that fetch. Any caller that didawait refresh()— e.g.SettlementDetail'srun()(src/components/SettlementDetail.tsx), which callsawait refresh()after a successful execute/cancel — is left with a promise that never resolves if the component unmounts in that window, silently leaking a pending promise.🧩 Requirements and context
refresh()'s returned promise should always eventually settle (resolve) — either when its triggered fetch completes, or, if the component/effect is torn down first (unmount, or a newerreload/refreshsuperseding it), resolve immediately at cleanup time rather than never.refreshshould be preserved for the normal (non-aborted) case.reload's behavior (it doesn't return a promise and isn't affected).🛠️ Suggested execution
src/hooks/useAsync.tsto drain and resolve any still-pendingrefreshWaiters(in addition to the abort call), so an unmount or a supersedingreload/refreshdoesn't strand earlier waiters.src/hooks/useAsync.test.tswith a test that callsrefresh(), unmounts the hook before the mockedloadpromise resolves, and asserts therefresh()promise still settles.✅ Acceptance criteria
refresh()'s returned promise always eventually resolves, even if the component unmounts before the triggered fetch settles.🔒 Security notes
No new attack surface; a promise-lifecycle correctness fix preventing a dangling
await.📋 Guidelines