fix(ui): prevent async listener leaks on back navigation

This commit is contained in:
thibaud-lclr 2026-04-21 10:33:12 +02:00
parent 2618c2ce77
commit b6e31f9312
3 changed files with 123 additions and 78 deletions

View file

@ -66,10 +66,12 @@ export default function NotificationCenter() {
}, [projectId]); }, [projectId]);
useEffect(() => { useEffect(() => {
let cancelled = false;
let unlisten: (() => void) | null = null; let unlisten: (() => void) | null = null;
const setup = async () => { const setup = async () => {
unlisten = await listen<NewNotificationEvent>("new-notification", (event) => { try {
const cleanup = await listen<NewNotificationEvent>("new-notification", (event) => {
const incoming = event.payload.notification; const incoming = event.payload.notification;
if (projectId && incoming.project_id !== projectId) { if (projectId && incoming.project_id !== projectId) {
@ -83,11 +85,22 @@ export default function NotificationCenter() {
void showSystemNotification(incoming); void showSystemNotification(incoming);
}); });
if (cancelled) {
cleanup();
return;
}
unlisten = cleanup;
} catch (error: unknown) {
console.error("Failed to subscribe to notifications", error);
}
}; };
void setup(); void setup();
return () => { return () => {
cancelled = true;
if (unlisten) { if (unlisten) {
unlisten(); unlisten();
} }

View file

@ -160,9 +160,11 @@ export default function ProjectLiveAgent() {
useEffect(() => { useEffect(() => {
if (!projectId) return; if (!projectId) return;
let cancelled = false;
let stop: (() => void) | null = null; let stop: (() => void) | null = null;
void (async () => { const setup = async () => {
try {
const [unlistenMessage, unlistenStarted, unlistenChunk, unlistenFinished, unlistenError] = const [unlistenMessage, unlistenStarted, unlistenChunk, unlistenFinished, unlistenError] =
await Promise.all([ await Promise.all([
listen<LiveEventPayload>("live-agent-message", (event) => { listen<LiveEventPayload>("live-agent-message", (event) => {
@ -217,17 +219,34 @@ export default function ProjectLiveAgent() {
}), }),
]); ]);
stop = () => { const cleanup = () => {
unlistenMessage(); unlistenMessage();
unlistenStarted(); unlistenStarted();
unlistenChunk(); unlistenChunk();
unlistenFinished(); unlistenFinished();
unlistenError(); unlistenError();
}; };
})();
if (cancelled) {
cleanup();
return;
}
stop = cleanup;
} catch (err: unknown) {
if (!cancelled) {
setError(getErrorMessage(err));
}
}
};
void setup();
return () => { return () => {
if (stop) stop(); cancelled = true;
if (stop) {
stop();
}
}; };
}, [projectId, selectedSessionId]); }, [projectId, selectedSessionId]);

View file

@ -76,18 +76,31 @@ export default function ProjectTasks() {
useEffect(() => { useEffect(() => {
if (!projectId) return; if (!projectId) return;
let stop: (() => void) | null = null; let cancelled = false;
let unlisten: (() => void) | null = null;
void (async () => { void listen<TaskEventPayload>("agent-task-updated", (event) => {
const unlisten = await listen<TaskEventPayload>("agent-task-updated", (event) => {
if (event.payload.project_id !== projectId) return; if (event.payload.project_id !== projectId) return;
void refresh(); void refresh();
})
.then((cleanup) => {
if (cancelled) {
cleanup();
return;
}
unlisten = cleanup;
})
.catch((err: unknown) => {
if (!cancelled) {
setError(getErrorMessage(err));
}
}); });
stop = unlisten;
})();
return () => { return () => {
if (stop) stop(); cancelled = true;
if (unlisten) {
unlisten();
}
}; };
}, [projectId]); }, [projectId]);