Retry SSE connection unless max clients have been reached.

This switches from the prehistoric EventSource API to the more modern
fetch-event-source package. That packages gives us full control over the
retries.

It also gives us the opportunity to close the event source when the
browser tab is hidden, saving resources.

Signed-off-by: Julien <roidelapluie@o11y.eu>
This commit is contained in:
Julien 2024-09-27 15:58:41 +02:00
parent f9bbad1148
commit e34563bfe0
4 changed files with 48 additions and 20 deletions

View file

@ -1704,6 +1704,10 @@ func (api *API) notificationsSSE(w http.ResponseWriter, r *http.Request) {
return
}
// Flush the response to ensure the headers are immediately and eventSource
// onopen is triggered client-side.
flusher.Flush()
for {
select {
case notification := <-notifications:

View file

@ -25,6 +25,7 @@
"@mantine/dates": "^7.11.2",
"@mantine/hooks": "^7.11.2",
"@mantine/notifications": "^7.11.2",
"@microsoft/fetch-event-source": "^2.0.1",
"@nexucis/fuzzy": "^0.5.1",
"@nexucis/kvsearch": "^0.9.1",
"@prometheus-io/codemirror-promql": "0.300.0-beta.0",

View file

@ -3,6 +3,7 @@ import { useSettings } from '../state/settingsSlice';
import { NotificationsContext } from '../state/useNotifications';
import { Notification, NotificationsResult } from "../api/responseTypes/notifications";
import { useAPIQuery } from '../api/api';
import { fetchEventSource } from '@microsoft/fetch-event-source';
export const NotificationsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { pathPrefix } = useSettings();
@ -24,31 +25,47 @@ export const NotificationsProvider: React.FC<{ children: React.ReactNode }> = ({
}, [data, isError]);
useEffect(() => {
const eventSource = new EventSource(`${pathPrefix}/api/v1/notifications/live`);
eventSource.onmessage = (event) => {
const notification: Notification = JSON.parse(event.data);
setNotifications((prev: Notification[]) => {
const updatedNotifications = [...prev.filter((n: Notification) => n.text !== notification.text)];
if (notification.active) {
updatedNotifications.push(notification);
const controller = new AbortController();
fetchEventSource(`${pathPrefix}/api/v1/notifications/live`, {
signal: controller.signal,
async onopen(response) {
if (response.ok) {
if (response.status === 200) {
setNotifications([]);
setIsConnectionError(false);
} else if (response.status === 204) {
controller.abort();
setShouldFetchFromAPI(true);
}
} else {
setIsConnectionError(true);
throw new Error(`Unexpected response: ${response.status} ${response.statusText}`);
}
},
onmessage(event) {
const notification: Notification = JSON.parse(event.data);
return updatedNotifications;
});
};
setNotifications((prev: Notification[]) => {
const updatedNotifications = [...prev.filter((n: Notification) => n.text !== notification.text)];
eventSource.onerror = () => {
eventSource.close();
// We do not call setIsConnectionError(true), we only set it to true if
// the fallback API does not work either.
setShouldFetchFromAPI(true);
};
if (notification.active) {
updatedNotifications.push(notification);
}
return updatedNotifications;
});
},
onclose() {
throw new Error("Server closed the connection");
},
onerror() {
setIsConnectionError(true);
return 5000;
},
});
return () => {
eventSource.close();
controller.abort();
};
}, [pathPrefix]);

View file

@ -39,6 +39,7 @@
"@mantine/dates": "^7.11.2",
"@mantine/hooks": "^7.11.2",
"@mantine/notifications": "^7.11.2",
"@microsoft/fetch-event-source": "^2.0.1",
"@nexucis/fuzzy": "^0.5.1",
"@nexucis/kvsearch": "^0.9.1",
"@prometheus-io/codemirror-promql": "0.300.0-beta.0",
@ -2255,6 +2256,11 @@
"react": "^18.2.0"
}
},
"node_modules/@microsoft/fetch-event-source": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
},
"node_modules/@nexucis/fuzzy": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@nexucis/fuzzy/-/fuzzy-0.5.1.tgz",