fix(notifications): don't lose live alerts until a page refresh
CI/CD / CI · API (dotnet build + test) (push) Successful in 44s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 29s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m6s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 46s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 2m50s
CI/CD / CI · API (dotnet build + test) (push) Successful in 44s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 29s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m6s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 46s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 2m50s
The SignalR connection used the default auto-reconnect, which gives up after ~30s and, even when it did reconnect, never re-ran JoinCafe — so the client dropped out of the café group and silently stopped receiving notifications until a manual refresh. Now it retries forever (capped backoff), re-joins the group on reconnect (and catches up via invalidate), and re-establishes the connection when the network returns or the tab is refocused. As a safety net, the unread/bell and tab-badge polls now run in background tabs too (refetchIntervalInBackground). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,9 @@ export function useNotificationsFeed(options: UseNotificationsFeedOptions = {})
|
||||
queryFn: () => fetchNotifications(cafeId!, unreadOnly, limit),
|
||||
enabled: !!cafeId,
|
||||
refetchInterval: 60_000,
|
||||
// Keep polling even when the tab is in the background, so the unread count
|
||||
// stays current if the live socket missed something — no refresh needed.
|
||||
refetchIntervalInBackground: true,
|
||||
});
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
|
||||
@@ -94,6 +94,7 @@ export function useTabBadge() {
|
||||
queryFn: () => fetchNotifications(cafeId!, false, 50),
|
||||
enabled: !!cafeId,
|
||||
refetchInterval: 60_000,
|
||||
refetchIntervalInBackground: true, // update the tab badge even when unfocused
|
||||
});
|
||||
|
||||
const unread = data?.unreadCount ?? 0;
|
||||
|
||||
@@ -54,10 +54,23 @@ export function useOrderAlerts() {
|
||||
accessTokenFactory: () =>
|
||||
(typeof window !== "undefined" ? localStorage.getItem("meezi_access_token") : null) ?? "",
|
||||
})
|
||||
.withAutomaticReconnect()
|
||||
// Retry FOREVER (capped backoff). The default policy gives up after ~30s,
|
||||
// leaving the connection dead until a page refresh → missed notifications.
|
||||
.withAutomaticReconnect({
|
||||
nextRetryDelayInMilliseconds: (ctx) =>
|
||||
Math.min(30000, 1000 * 2 ** Math.min(ctx.previousRetryCount, 5)),
|
||||
})
|
||||
.build();
|
||||
|
||||
let stopped = false;
|
||||
const joinCafe = () => connection.invoke("JoinCafe", cafeId).catch(() => {});
|
||||
|
||||
// On reconnect the server group membership is gone — re-join or we silently
|
||||
// stop receiving notifications. Also catch up anything missed while down.
|
||||
connection.onreconnected(() => {
|
||||
void joinCafe();
|
||||
void qc.invalidateQueries({ queryKey: ["notifications", cafeId] });
|
||||
});
|
||||
|
||||
const severityFor = (type: string) => {
|
||||
if (type === "table_call_waiter") return notify.warning;
|
||||
@@ -104,14 +117,36 @@ export function useOrderAlerts() {
|
||||
void connection
|
||||
.start()
|
||||
.then(() => {
|
||||
if (!stopped) return connection.invoke("JoinCafe", cafeId);
|
||||
if (!stopped) return joinCafe();
|
||||
})
|
||||
.catch(() => {
|
||||
// connection/auth failed — alerts simply won't fire; no UI breakage
|
||||
});
|
||||
|
||||
// If the connection fully dropped (gave up, or the device slept), bring it
|
||||
// back when the network returns or the tab is focused again.
|
||||
const ensureConnected = () => {
|
||||
if (stopped) return;
|
||||
if (connection.state === signalR.HubConnectionState.Disconnected) {
|
||||
void connection
|
||||
.start()
|
||||
.then(() => {
|
||||
if (!stopped) return joinCafe();
|
||||
})
|
||||
.then(() => qc.invalidateQueries({ queryKey: ["notifications", cafeId] }))
|
||||
.catch(() => {});
|
||||
}
|
||||
};
|
||||
const onVisible = () => {
|
||||
if (document.visibilityState === "visible") ensureConnected();
|
||||
};
|
||||
window.addEventListener("online", ensureConnected);
|
||||
document.addEventListener("visibilitychange", onVisible);
|
||||
|
||||
return () => {
|
||||
stopped = true;
|
||||
window.removeEventListener("online", ensureConnected);
|
||||
document.removeEventListener("visibilitychange", onVisible);
|
||||
void connection.stop();
|
||||
};
|
||||
}, [cafeId, locale, qc]);
|
||||
|
||||
Reference in New Issue
Block a user