fix(notifications): clear thread receipts on mark-read; cap avatar-decoration refetch
Two federated-room bugs surfaced by the desktop build: 1. markAsRead only sent one unthreaded receipt at the main-timeline tail. With threadSupport enabled, thread replies leave the main timeline, so a reply newer than that tail was never covered — its per-thread notification count (which the room dot sums) lingered, so the unread dot never cleared even after reading. It also early-returned when the main timeline was already read. Now also send a threaded receipt at each unread thread's latest reply. 2. useAvatarDecoration never cached non-404 failures, so every avatar mount re-requested io.lotus.avatar_decoration for federated users whose homeserver 403s/502s the field — a refetch storm that spammed the console and hammered our homeserver's federation. Now cache definitive rejections (400/403/404) and give up after ~2 transient (429/5xx) attempts per session. Gates: tsc/eslint/prettier clean, build OK, 665 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,9 @@ const PROFILE_FIELD = 'io.lotus.avatar_decoration';
|
||||
const cache = new Map<string, string | null>();
|
||||
// Callbacks waiting for a userId's result
|
||||
const pending = new Map<string, Array<(val: string | null) => void>>();
|
||||
// Transient-failure attempt counts (userId → n) so a flaky federated lookup
|
||||
// can retry a couple of times, then gives up for the session.
|
||||
const failures = new Map<string, number>();
|
||||
|
||||
function fetchDecoration(
|
||||
authedRequest: (method: Method, path: string) => Promise<Record<string, string>>,
|
||||
@@ -33,16 +36,23 @@ function fetchDecoration(
|
||||
return val;
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
// A 404 (M_NOT_FOUND) means the field is genuinely unset → cache "no
|
||||
// decoration". A transient failure (429 rate-limit, 5xx, network) must
|
||||
// NOT be cached: doing so permanently hides the user's decoration for the
|
||||
// whole session. This matters most for the member list and timeline, which
|
||||
// mount many avatars at once and can trip homeserver rate limits — a
|
||||
// single 429 in that burst would otherwise wipe the decoration until a
|
||||
// full reload. Leaving the cache unset lets the next mount retry.
|
||||
const status = err instanceof MatrixError ? err.httpStatus : undefined;
|
||||
if (status === 404) {
|
||||
// Definitive rejections — the field is unset (404) or the server won't
|
||||
// serve it (400/403). This is the common case for FEDERATED users whose
|
||||
// homeserver doesn't support extended profiles / rejects the field. Cache
|
||||
// "no decoration" so we never refetch: otherwise every avatar mount
|
||||
// re-requests and floods our homeserver with failing federated profile
|
||||
// lookups (the 403/502 console storm + real HS load).
|
||||
if (status === 404 || status === 403 || status === 400) {
|
||||
cache.set(userId, null);
|
||||
} else {
|
||||
// Transient (429 rate-limit / 5xx / network). Allow a couple of retries
|
||||
// — a single 429 in a member-list burst shouldn't permanently hide a
|
||||
// decoration — then give up for the session so a persistently-failing
|
||||
// federated link (e.g. a 502'ing remote server) can't loop forever.
|
||||
const attempts = (failures.get(userId) ?? 0) + 1;
|
||||
failures.set(userId, attempts);
|
||||
if (attempts >= 2) cache.set(userId, null);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user