import { MatrixClient, MatrixError } from 'matrix-js-sdk'; /** * Synapse rate-limits `PUT /presence/{user}/status` to ONE request per 10 s per * user by default (`rc_presence.per_user`: 0.1/s, burst 1) — and the limit is * shared across all of a user's devices. Our presence heartbeat (online/away on * visibility and activity changes) can therefore make a manual status save 429. * * `setPresenceWithRetry` waits out `Retry-After` (bounded) instead of failing, * and `PresenceDeduper` lets the heartbeat skip writes that would not change * anything, so it stops spending the budget in the first place. */ export type PresenceWrite = { presence: 'online' | 'offline' | 'unavailable'; status_msg?: string }; const sleep = (ms: number) => new Promise((r) => { setTimeout(r, ms); }); export async function setPresenceWithRetry( mx: Pick, write: PresenceWrite, opts: { maxWaitMs?: number; sleepFn?: (ms: number) => Promise } = {}, ): Promise { const maxWaitMs = opts.maxWaitMs ?? 25_000; const sleepFn = opts.sleepFn ?? sleep; let waited = 0; for (;;) { try { // eslint-disable-next-line no-await-in-loop await mx.setPresence(write); return; } catch (e) { if (!(e instanceof MatrixError) || e.httpStatus !== 429) throw e; const wait = Math.min(e.getRetryAfterMs() ?? 10_500, 15_000); if (waited + wait > maxWaitMs) throw e; waited += wait; // eslint-disable-next-line no-await-in-loop await sleepFn(wait); } } } const presenceKey = (write: PresenceWrite): string => `${write.presence}|${write.status_msg ?? ''}`; /** Remembers the last write that succeeded so identical heartbeats are skipped. */ export class PresenceDeduper { private last: string | null = null; shouldSend(write: PresenceWrite): boolean { return presenceKey(write) !== this.last; } sent(write: PresenceWrite): void { this.last = presenceKey(write); } reset(): void { this.last = null; } } /** A user-facing sentence for a failed status save. */ export const describePresenceError = (e: unknown): string => { if (e instanceof MatrixError) { if (e.httpStatus === 429) { return 'The server limits how often presence can change (once every 10 seconds, shared by all your devices). Please wait a moment and try again.'; } const serverText = (e.data as { error?: string } | undefined)?.error; if (serverText) return `Failed to save status: ${serverText}`; } return "Failed to save status — couldn't reach the server. Try again."; };