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,28 +66,41 @@ export default function NotificationCenter() {
}, [projectId]);
useEffect(() => {
let cancelled = false;
let unlisten: (() => void) | null = null;
const setup = async () => {
unlisten = await listen<NewNotificationEvent>("new-notification", (event) => {
const incoming = event.payload.notification;
try {
const cleanup = await listen<NewNotificationEvent>("new-notification", (event) => {
const incoming = event.payload.notification;
if (projectId && incoming.project_id !== projectId) {
if (projectId && incoming.project_id !== projectId) {
return;
}
setNotifications((prev) => {
const withoutDuplicate = prev.filter((n) => n.id !== incoming.id);
return [incoming, ...withoutDuplicate];
});
void showSystemNotification(incoming);
});
if (cancelled) {
cleanup();
return;
}
setNotifications((prev) => {
const withoutDuplicate = prev.filter((n) => n.id !== incoming.id);
return [incoming, ...withoutDuplicate];
});
void showSystemNotification(incoming);
});
unlisten = cleanup;
} catch (error: unknown) {
console.error("Failed to subscribe to notifications", error);
}
};
void setup();
return () => {
cancelled = true;
if (unlisten) {
unlisten();
}

View file

@ -160,74 +160,93 @@ export default function ProjectLiveAgent() {
useEffect(() => {
if (!projectId) return;
let cancelled = false;
let stop: (() => void) | null = null;
void (async () => {
const [unlistenMessage, unlistenStarted, unlistenChunk, unlistenFinished, unlistenError] =
await Promise.all([
listen<LiveEventPayload>("live-agent-message", (event) => {
const payload = event.payload;
if (payload.project_id !== projectId) return;
if (payload.session_id !== selectedSessionId) return;
const setup = async () => {
try {
const [unlistenMessage, unlistenStarted, unlistenChunk, unlistenFinished, unlistenError] =
await Promise.all([
listen<LiveEventPayload>("live-agent-message", (event) => {
const payload = event.payload;
if (payload.project_id !== projectId) return;
if (payload.session_id !== selectedSessionId) return;
setMessages((prev) => {
const existingIndex = prev.findIndex((msg) => msg.id === payload.message.id);
if (existingIndex === -1) {
return [...prev, payload.message];
setMessages((prev) => {
const existingIndex = prev.findIndex((msg) => msg.id === payload.message.id);
if (existingIndex === -1) {
return [...prev, payload.message];
}
const next = [...prev];
next[existingIndex] = payload.message;
return next;
});
if (payload.message.sender === "agent" && payload.message.content.trim() !== "") {
setStreamingAgentResponse(null);
}
const next = [...prev];
next[existingIndex] = payload.message;
return next;
});
if (payload.message.sender === "agent" && payload.message.content.trim() !== "") {
}),
listen<LiveStreamStatusPayload>("live-agent-stream-started", (event) => {
const payload = event.payload;
if (payload.project_id !== projectId) return;
if (payload.session_id !== selectedSessionId) return;
setStreamingAgentResponse("");
}),
listen<LiveStreamChunkPayload>("live-agent-stream-chunk", (event) => {
const payload = event.payload;
if (payload.project_id !== projectId) return;
if (payload.session_id !== selectedSessionId) return;
setStreamingAgentResponse((prev) => `${prev ?? ""}${payload.chunk}`);
}),
listen<LiveStreamStatusPayload>("live-agent-stream-finished", (event) => {
const payload = event.payload;
if (payload.project_id !== projectId) return;
if (payload.session_id !== selectedSessionId) return;
setStreamingAgentResponse(null);
}
}),
listen<LiveStreamStatusPayload>("live-agent-stream-started", (event) => {
const payload = event.payload;
if (payload.project_id !== projectId) return;
if (payload.session_id !== selectedSessionId) return;
setStreamingAgentResponse("");
}),
listen<LiveStreamChunkPayload>("live-agent-stream-chunk", (event) => {
const payload = event.payload;
if (payload.project_id !== projectId) return;
if (payload.session_id !== selectedSessionId) return;
setStreamingAgentResponse((prev) => `${prev ?? ""}${payload.chunk}`);
}),
listen<LiveStreamStatusPayload>("live-agent-stream-finished", (event) => {
const payload = event.payload;
if (payload.project_id !== projectId) return;
if (payload.session_id !== selectedSessionId) return;
setStreamingAgentResponse(null);
}),
listen<LiveStreamStatusPayload>("live-agent-stream-error", (event) => {
const payload = event.payload;
if (payload.project_id !== projectId) return;
if (payload.session_id !== selectedSessionId) return;
setStreamingAgentResponse(null);
setMessages((prev) =>
prev.filter((msg) => !(msg.sender === "agent" && msg.content.trim() === ""))
);
if (payload.error) {
setError(payload.error);
}
}),
]);
}),
listen<LiveStreamStatusPayload>("live-agent-stream-error", (event) => {
const payload = event.payload;
if (payload.project_id !== projectId) return;
if (payload.session_id !== selectedSessionId) return;
setStreamingAgentResponse(null);
setMessages((prev) =>
prev.filter((msg) => !(msg.sender === "agent" && msg.content.trim() === ""))
);
if (payload.error) {
setError(payload.error);
}
}),
]);
stop = () => {
unlistenMessage();
unlistenStarted();
unlistenChunk();
unlistenFinished();
unlistenError();
};
})();
const cleanup = () => {
unlistenMessage();
unlistenStarted();
unlistenChunk();
unlistenFinished();
unlistenError();
};
if (cancelled) {
cleanup();
return;
}
stop = cleanup;
} catch (err: unknown) {
if (!cancelled) {
setError(getErrorMessage(err));
}
}
};
void setup();
return () => {
if (stop) stop();
cancelled = true;
if (stop) {
stop();
}
};
}, [projectId, selectedSessionId]);

View file

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