fix: exponential backoff for 429 retries, cover thumbnails, 2 more modal dialogs

- matrix.ts: rateLimitedActions fallback delay now uses capped exponential
  backoff (min(1000 * 2^n, 30s)) instead of flat 3000ms when server omits
  Retry-After; server header still takes precedence
- RenderMessageContent: add objectFit:cover + 100% fill to video thumbnail
  <Image> so thumbnails fill their container without letterboxing (P5-6)
- CreateRoomModal, CreateSpaceModal: apply useModalStyle(480) for fullscreen
  on mobile
- LOTUS_BUGS: mark usePan memory leak + httpStatus check as FALSE POSITIVE;
  mark rateLimitedActions backoff as FIXED

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 15:32:17 -04:00
parent 8c711f5f4a
commit baa12823f7
5 changed files with 18 additions and 7 deletions
+3 -3
View File
@@ -121,16 +121,16 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
| State Sync | Fire-and-forget network call `setPresence().catch(...)` suppresses errors, meaning the app may falsely assume presence update success. | `cinny/src/app/hooks/usePresenceUpdater.ts` | OPEN |
| Memory Leak | Decrypted Media Memory Leak (Gallery & Lightbox) due to missing virtualization and blob revocation. | `cinny/src/app/features/room/MediaGallery.tsx` | OPEN |
| Data Persistence | Scheduled Messages are ephemeral (lost on refresh) due to fragile `localStorage` parsing. | `cinny/src/app/state/scheduledMessages.ts` | FIXED — now uses `atomWithStorage` + `createJSONStorage` (Jotai's built-in persistence with error-safe JSON parsing) |
| Memory Leak | Potential memory leak due to uncleaned `handleMouseMove` listener in `usePan`. | `cinny/src/app/hooks/usePan.ts` | OPEN |
| Memory Leak | Potential memory leak due to uncleaned `handleMouseMove` listener in `usePan`. | `cinny/src/app/hooks/usePan.ts` | FALSE POSITIVE — `usePan` already uses `attachedRef` to track listeners and cleans them up in an unmount `useEffect`. No change needed. |
| Asset Optimization | Large unoptimized media asset (213KB) found in `public/res`. | `public/res/Lotus.png` | OPEN |
| Data Persistence | Non-atomic `localStorage` updates in session management can lead to inconsistent state. | `cinny/src/app/state/sessions.ts` | OPEN |
| Data Persistence | Lack of cross-tab synchronization for `localStorage` updates in session management risks race conditions. | `cinny/src/app/state/sessions.ts` | OPEN |
| Network Resilience | `uploadContent` lacks retry logic, failing immediately upon network error. | `cinny/src/app/utils/matrix.ts` | OPEN |
| Network Resilience | `rateLimitedActions` uses basic retry logic without exponential backoff, which may exacerbate 429 issues. | `cinny/src/app/utils/matrix.ts` | OPEN |
| Network Resilience | `rateLimitedActions` uses basic retry logic without exponential backoff, which may exacerbate 429 issues. | `cinny/src/app/utils/matrix.ts` | FIXED — fallback delay now uses capped exponential backoff (`min(1000 * 2^retryCount, 30_000)ms`) when server doesn't send `Retry-After`; server header still takes precedence via `getRetryAfterMs()`. |
| Matrix Event Robustness | `useMatrixEventRenderer` handles unknown events gracefully by returning `null`, which may hide potentially important unrendered data. | `cinny/src/app/hooks/useMatrixEventRenderer.ts` | OPEN |
| Data Contract | `MatrixError` instantiation with `UploadResponse` might be brittle. | `cinny/src/app/utils/matrix.ts` | OPEN |
| Type Safety | `addRoomIdToMDirect` uses `as any` cast for `AccountDataEvent.Direct`, bypassing type contract validation. | `cinny/src/app/utils/matrix.ts` | OPEN |
| Robustness | `rateLimitedActions` relies on `MatrixError.httpStatus` which might not exist on all error variants. | `cinny/src/app/utils/matrix.ts` | OPEN |
| Robustness | `rateLimitedActions` relies on `MatrixError.httpStatus` which might not exist on all error variants. | `cinny/src/app/utils/matrix.ts` | FALSE POSITIVE — `MatrixError.httpStatus` is defined as `readonly httpStatus?: number` in `matrix-js-sdk/lib/http-api/errors.d.ts`. It is optional (not on all instances) but the `?.` optional chain already guards against undefined. No change needed. |
| Type Contract | Custom types in `cinny/src/types/matrix` mirror SDK types instead of using them, risking drift and contract mismatches. | `cinny/src/types/matrix/` | OPEN |
## 🏗️ Architectural & Hygiene Audit
+7 -1
View File
@@ -232,7 +232,13 @@ export function RenderMessageContent({
<ThumbnailContent
info={info}
renderImage={(src) => (
<Image alt={body} title={body} src={src} loading="lazy" />
<Image
alt={body}
title={body}
src={src}
loading="lazy"
style={{ objectFit: 'cover', width: '100%', height: '100%' }}
/>
)}
/>
)
@@ -24,6 +24,7 @@ import {
import { CreateRoomModalState } from '../../state/createRoomModal';
import { stopPropagation } from '../../utils/keyboard';
import { CreateRoomType } from '../../components/create-room/types';
import { useModalStyle } from '../../hooks/useModalStyle';
type CreateRoomModalProps = {
state: CreateRoomModalState;
@@ -31,6 +32,7 @@ type CreateRoomModalProps = {
function CreateRoomModal({ state }: CreateRoomModalProps) {
const { spaceId, type } = state;
const closeDialog = useCloseCreateRoomModal();
const modalStyle = useModalStyle(480);
const allJoinedRooms = useAllJoinedRoomsSet();
const getRoom = useGetRoom(allJoinedRooms);
@@ -48,7 +50,7 @@ function CreateRoomModal({ state }: CreateRoomModalProps) {
escapeDeactivates: stopPropagation,
}}
>
<Modal size="300" flexHeight>
<Modal size="300" flexHeight style={modalStyle}>
<Box direction="Column">
<Header
size="500"
@@ -23,6 +23,7 @@ import {
} from '../../state/hooks/createSpaceModal';
import { CreateSpaceModalState } from '../../state/createSpaceModal';
import { stopPropagation } from '../../utils/keyboard';
import { useModalStyle } from '../../hooks/useModalStyle';
type CreateSpaceModalProps = {
state: CreateSpaceModalState;
@@ -30,6 +31,7 @@ type CreateSpaceModalProps = {
function CreateSpaceModal({ state }: CreateSpaceModalProps) {
const { spaceId } = state;
const closeDialog = useCloseCreateSpaceModal();
const modalStyle = useModalStyle(480);
const allJoinedRooms = useAllJoinedRoomsSet();
const getRoom = useGetRoom(allJoinedRooms);
@@ -47,7 +49,7 @@ function CreateSpaceModal({ state }: CreateSpaceModalProps) {
escapeDeactivates: stopPropagation,
}}
>
<Modal size="300" flexHeight>
<Modal size="300" flexHeight style={modalStyle}>
<Box direction="Column">
<Header
size="500"
+2 -1
View File
@@ -364,7 +364,8 @@ export const rateLimitedActions = async <T, R = void>(
return;
}
const waitMS = err.getRetryAfterMs() ?? 3000;
// Respect server Retry-After header; fall back to capped exponential backoff.
const waitMS = err.getRetryAfterMs() ?? Math.min(1000 * 2 ** retryCount, 30_000);
actionInterval = waitMS * 1.5;
await sleepForMs(waitMS);
retryCount += 1;