fix(status): status save no longer fails on Synapse's 1-per-10s presence limit
Reported: 'Failed to save status — server may be rate limiting. Try again.'
on the first status change in a month. Root cause: Synapse rate-limits
PUT /presence/{user}/status to ONE request per 10 s per user by default
(rc_presence.per_user: per_second 0.1, burst_count 1), shared across all of
the user's devices, and our presence heartbeat (online/away on visibility and
activity changes, from every open tab/device) spends that budget — so a manual
save that lands within 10 s of a heartbeat gets a 429, which the form showed
as a dead end.
Two fixes: (1) the status save waits out Retry-After (bounded to ~25 s) via
setPresenceWithRetry instead of failing, and the error text now says what
actually happened (rate limit / server text / offline); (2) the heartbeat
dedupes — it only sends when presence or status actually changes (/sync
already keeps us online), so it stops burning the budget in the first place.
Unit-tested; reproduced headless with a routed 10 s limiter: heartbeat ok →
save 429 → retried 8 s later → saved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { PresenceDeduper, PresenceWrite } from '../utils/presenceWrite';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
@@ -33,9 +34,13 @@ export function usePresenceUpdater() {
|
||||
const idleTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
const isIdleRef = useRef(false);
|
||||
const lastActivityRef = useRef(0);
|
||||
const deduperRef = useRef(new PresenceDeduper());
|
||||
|
||||
useEffect(() => {
|
||||
const userId = mx.getUserId();
|
||||
const deduper = deduperRef.current;
|
||||
// A manual status save changes what the next heartbeat should carry.
|
||||
deduper.reset();
|
||||
|
||||
// Read status from localStorage at call time so manual updates from the
|
||||
// Profile settings are never overwritten by a stale closure value.
|
||||
@@ -49,28 +54,32 @@ export function usePresenceUpdater() {
|
||||
console.warn(`Failed to set presence to "${presence}":`, reason);
|
||||
};
|
||||
|
||||
// Synapse allows ONE presence write per 10 s per user (rc_presence, shared
|
||||
// by all devices) and /sync already keeps us online — only send when the
|
||||
// state or status actually changes, so a manual status save isn't 429'd
|
||||
// by a heartbeat that changed nothing.
|
||||
const send = (write: PresenceWrite, label: string) => {
|
||||
if (!deduper.shouldSend(write)) return Promise.resolve();
|
||||
return mx
|
||||
.setPresence(write)
|
||||
.then(() => deduper.sent(write))
|
||||
.catch((err) => warnPresenceFailure(label, err));
|
||||
};
|
||||
const setOnline = () => {
|
||||
const status = readStatus();
|
||||
return mx
|
||||
.setPresence({
|
||||
presence: 'online',
|
||||
...(status ? { status_msg: status } : {}),
|
||||
})
|
||||
.catch((err) => warnPresenceFailure('online', err));
|
||||
return send({ presence: 'online', ...(status ? { status_msg: status } : {}) }, 'online');
|
||||
};
|
||||
const setUnavailable = (statusMsg?: string) => {
|
||||
const status = readStatus();
|
||||
return mx
|
||||
.setPresence({
|
||||
return send(
|
||||
{
|
||||
presence: 'unavailable',
|
||||
...(statusMsg ? { status_msg: statusMsg } : status ? { status_msg: status } : {}),
|
||||
})
|
||||
.catch((err) => warnPresenceFailure('unavailable', err));
|
||||
},
|
||||
'unavailable',
|
||||
);
|
||||
};
|
||||
const setOffline = () =>
|
||||
mx
|
||||
.setPresence({ presence: 'offline', status_msg: '' })
|
||||
.catch((err) => warnPresenceFailure('offline', err));
|
||||
const setOffline = () => send({ presence: 'offline', status_msg: '' }, 'offline');
|
||||
|
||||
// Manual presence overrides — no activity tracking needed.
|
||||
if (hidePresence || presenceStatus === 'invisible') {
|
||||
|
||||
Reference in New Issue
Block a user