fix(ui): prevent async listener leaks on back navigation
This commit is contained in:
parent
2618c2ce77
commit
b6e31f9312
3 changed files with 123 additions and 78 deletions
|
|
@ -66,28 +66,41 @@ 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 incoming = event.payload.notification;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setNotifications((prev) => {
|
unlisten = cleanup;
|
||||||
const withoutDuplicate = prev.filter((n) => n.id !== incoming.id);
|
} catch (error: unknown) {
|
||||||
return [incoming, ...withoutDuplicate];
|
console.error("Failed to subscribe to notifications", error);
|
||||||
});
|
}
|
||||||
|
|
||||||
void showSystemNotification(incoming);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void setup();
|
void setup();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
if (unlisten) {
|
if (unlisten) {
|
||||||
unlisten();
|
unlisten();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,74 +160,93 @@ 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 () => {
|
||||||
const [unlistenMessage, unlistenStarted, unlistenChunk, unlistenFinished, unlistenError] =
|
try {
|
||||||
await Promise.all([
|
const [unlistenMessage, unlistenStarted, unlistenChunk, unlistenFinished, unlistenError] =
|
||||||
listen<LiveEventPayload>("live-agent-message", (event) => {
|
await Promise.all([
|
||||||
const payload = event.payload;
|
listen<LiveEventPayload>("live-agent-message", (event) => {
|
||||||
if (payload.project_id !== projectId) return;
|
const payload = event.payload;
|
||||||
if (payload.session_id !== selectedSessionId) return;
|
if (payload.project_id !== projectId) return;
|
||||||
|
if (payload.session_id !== selectedSessionId) return;
|
||||||
|
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
const existingIndex = prev.findIndex((msg) => msg.id === payload.message.id);
|
const existingIndex = prev.findIndex((msg) => msg.id === payload.message.id);
|
||||||
if (existingIndex === -1) {
|
if (existingIndex === -1) {
|
||||||
return [...prev, payload.message];
|
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];
|
listen<LiveStreamStatusPayload>("live-agent-stream-started", (event) => {
|
||||||
next[existingIndex] = payload.message;
|
const payload = event.payload;
|
||||||
return next;
|
if (payload.project_id !== projectId) return;
|
||||||
});
|
if (payload.session_id !== selectedSessionId) return;
|
||||||
|
setStreamingAgentResponse("");
|
||||||
if (payload.message.sender === "agent" && payload.message.content.trim() !== "") {
|
}),
|
||||||
|
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);
|
setStreamingAgentResponse(null);
|
||||||
}
|
}),
|
||||||
}),
|
listen<LiveStreamStatusPayload>("live-agent-stream-error", (event) => {
|
||||||
listen<LiveStreamStatusPayload>("live-agent-stream-started", (event) => {
|
const payload = event.payload;
|
||||||
const payload = event.payload;
|
if (payload.project_id !== projectId) return;
|
||||||
if (payload.project_id !== projectId) return;
|
if (payload.session_id !== selectedSessionId) return;
|
||||||
if (payload.session_id !== selectedSessionId) return;
|
setStreamingAgentResponse(null);
|
||||||
setStreamingAgentResponse("");
|
setMessages((prev) =>
|
||||||
}),
|
prev.filter((msg) => !(msg.sender === "agent" && msg.content.trim() === ""))
|
||||||
listen<LiveStreamChunkPayload>("live-agent-stream-chunk", (event) => {
|
);
|
||||||
const payload = event.payload;
|
if (payload.error) {
|
||||||
if (payload.project_id !== projectId) return;
|
setError(payload.error);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
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]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue