fix: patch memory leaks in event hooks and strengthen type guard

Fix three bugs found during code review:

1. useTauriEvent/useTauriEvents: If the component unmounts before the
   async listen() promise resolves, the unlisten function was lost,
   leaking the event subscription. Added a cancelled flag to call
   unlisten immediately when the promise resolves after cleanup.

2. useTauriEvents: The handlers object was used directly as a useEffect
   dependency, causing re-subscription on every render when callers
   pass an inline object literal. Replaced with a useRef for handler
   stability and a derived eventNames string as the dependency.

3. isMcError type guard: Only checked property existence via 'in'
   operator, not property types. An object with wrong-typed properties
   (e.g. code: 42) would pass the guard. Now validates that code and
   message are strings and recoverable is boolean.

4. AppShell global shortcut listener: Same race condition as (1), plus
   missing .catch() on the listen promise could produce unhandled
   rejections.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
teernisse
2026-02-26 10:12:20 -05:00
parent 378a173084
commit 29b44f1b4c
3 changed files with 69 additions and 44 deletions

View File

@@ -38,14 +38,30 @@ export function AppShell(): React.ReactElement {
// Listen for global shortcut events from the Rust backend
useEffect(() => {
const unlisten = listen<string>("global-shortcut-triggered", (event) => {
let cancelled = false;
let unlisten: (() => void) | undefined;
listen<string>("global-shortcut-triggered", (event) => {
if (event.payload === "quick-capture") {
useCaptureStore.getState().open();
}
});
})
.then((fn) => {
if (cancelled) {
fn();
} else {
unlisten = fn;
}
})
.catch((err: unknown) => {
console.warn("[AppShell] Failed to subscribe to global shortcut:", err);
});
return () => {
unlisten.then((fn) => fn());
cancelled = true;
if (unlisten) {
unlisten();
}
};
}, []);