fix(status): auto-clear survives a rate-limited presence write (#227)
The quick-status auto-clear removed its localStorage record BEFORE the presence write and swallowed any error, so a single failure dropped the timer permanently and the status stayed set forever. Synapse rate-limits presence to ~1 write / 10 s per user (#226) and the heartbeat spends that budget, so 429s here are routine — especially right after startup, when the monitor's first check runs. The clear now goes through setPresenceWithRetry (honours retry_after_ms) and only forgets the status once the server has taken it; a re-entry guard stops overlapping attempts and the poll is 15 s so a retry lands promptly. Reproduced and verified with three injected 429s: before, the status stayed on the server forever with the local record gone; now it clears. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -60,6 +60,7 @@ import {
|
||||
unmuteRoom,
|
||||
} from '../../features/room-nav/muteTimers';
|
||||
import { STATUS_EXPIRY_KEY, STATUS_MSG_KEY } from '../../features/settings/account/Profile';
|
||||
import { setPresenceWithRetry } from '../../utils/presenceWrite';
|
||||
import { useDeepLinkNavigate } from '../../hooks/useDeepLinkNavigate';
|
||||
import { toastQueueAtom } from '../../state/toast';
|
||||
import { useReminders } from '../../hooks/useReminders';
|
||||
@@ -389,21 +390,38 @@ function StatusExpiryMonitor() {
|
||||
const expiryKey = STATUS_EXPIRY_KEY(userId);
|
||||
const msgKey = STATUS_MSG_KEY(userId);
|
||||
|
||||
// [Gitea #227] The clear must survive a failed write. Synapse rate-limits
|
||||
// presence to ~1 write / 10 s per user (#226) and the heartbeat spends that
|
||||
// budget, so a bare setPresence here 429s often — and because the keys used
|
||||
// to be removed BEFORE the request, a single failure dropped the auto-clear
|
||||
// permanently and the status stayed set forever. Retry the write (honouring
|
||||
// Retry-After), and only forget the status once the server has taken it.
|
||||
let clearing = false;
|
||||
const check = () => {
|
||||
if (clearing) return;
|
||||
const stored = localStorage.getItem(expiryKey);
|
||||
if (!stored) return;
|
||||
const ts = parseInt(stored, 10);
|
||||
if (!ts || Date.now() < ts) return;
|
||||
localStorage.removeItem(msgKey);
|
||||
localStorage.removeItem(expiryKey);
|
||||
mx.setPresence({
|
||||
clearing = true;
|
||||
setPresenceWithRetry(mx, {
|
||||
presence: presenceStateFromSetting(presenceStatusRef.current, hidePresenceRef.current),
|
||||
status_msg: '',
|
||||
}).catch(() => undefined);
|
||||
})
|
||||
.then(() => {
|
||||
localStorage.removeItem(msgKey);
|
||||
localStorage.removeItem(expiryKey);
|
||||
})
|
||||
.catch(() => undefined)
|
||||
.finally(() => {
|
||||
clearing = false;
|
||||
});
|
||||
};
|
||||
|
||||
check();
|
||||
const interval = setInterval(check, 30_000);
|
||||
// Poll often enough that a rate-limited retry lands promptly, but not so
|
||||
// often that a persistent failure hammers the server.
|
||||
const interval = setInterval(check, 15_000);
|
||||
const onVisible = () => {
|
||||
if (document.visibilityState === 'visible') check();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user