From 2cfafdae0204d5099c6d636ad791d7b3c22895f2 Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Fri, 27 Mar 2026 14:01:06 -0400 Subject: [PATCH] fix: eliminate SW session race causing initial media 401s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve stash conflict in DevelopTools.tsx — keep the improved Rotate Encryption Sessions description and success message text from the stashed changes. Add an immediate synchronous sendSessionToSW() call in index.tsx before React mounts. When the SW is already active (normal page reload), navigator.serviceWorker.controller is set and the postMessage is sent before createRoot().render() runs and before any elements fire fetch events. This discards the stale preloadedSession in the SW (which could be from a previous token that is no longer current) before the first thumbnail/media fetches arrive, preventing the race condition that produced 401 errors on initial load with Retry buttons. --- .../settings/developer-tools/DevelopTools.tsx | 81 ++++++++++++++++++- src/index.tsx | 6 ++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/app/features/settings/developer-tools/DevelopTools.tsx b/src/app/features/settings/developer-tools/DevelopTools.tsx index b717f2261..d786159b3 100644 --- a/src/app/features/settings/developer-tools/DevelopTools.tsx +++ b/src/app/features/settings/developer-tools/DevelopTools.tsx @@ -1,5 +1,6 @@ import { useCallback, useState } from 'react'; -import { Box, Text, IconButton, Icon, Icons, Scroll, Switch, Button } from 'folds'; +import { Box, Text, IconButton, Icon, Icons, Scroll, Switch, Button, Spinner, color } from 'folds'; +import { KnownMembership } from 'matrix-js-sdk/lib/types'; import { Page, PageContent, PageHeader } from '$components/page'; import { SequenceCard } from '$components/sequence-card'; import { SettingTile } from '$components/setting-tile'; @@ -9,6 +10,7 @@ import { useMatrixClient } from '$hooks/useMatrixClient'; import { AccountDataEditor, AccountDataSubmitCallback } from '$components/AccountDataEditor'; import { copyToClipboard } from '$utils/dom'; import { SequenceCardStyle } from '$features/settings/styles.css'; +import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import { AccountData } from './AccountData'; import { SyncDiagnostics } from './SyncDiagnostics'; import { DebugLogViewer } from './DebugLogViewer'; @@ -23,6 +25,33 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) { const [expand, setExpend] = useState(false); const [accountDataType, setAccountDataType] = useState(); + const [rotateState, rotateAllSessions] = useAsyncCallback< + { rotated: number; total: number }, + Error, + [] + >( + useCallback(async () => { + const crypto = mx.getCrypto(); + if (!crypto) throw new Error('Crypto module not available'); + + const encryptedRooms = mx + .getRooms() + .filter( + (room) => + room.getMyMembership() === KnownMembership.Join && mx.isRoomEncrypted(room.roomId) + ); + + await Promise.all(encryptedRooms.map((room) => crypto.forceDiscardSession(room.roomId))); + const rotated = encryptedRooms.length; + + // Proactively start session creation + key sharing with all devices + // (including bridge bots). fire-and-forget per room. + encryptedRooms.forEach((room) => crypto.prepareToEncrypt(room)); + + return { rotated, total: encryptedRooms.length }; + }, [mx]) + ); + const submitAccountData: AccountDataSubmitCallback = useCallback( async (type, content) => { // TODO: remove cast once account data typing is unified. @@ -115,6 +144,56 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) { )} {developerTools && } + {developerTools && ( + + Encryption + + + ) + } + > + + {rotateState.status === AsyncStatus.Loading ? 'Rotating…' : 'Rotate'} + + + } + > + {rotateState.status === AsyncStatus.Success && ( + + Sessions discarded for {rotateState.data.rotated} of{' '} + {rotateState.data.total} encrypted rooms. Key sharing is starting in the + background — send a message in an affected room to confirm delivery to + bridges. + + )} + {rotateState.status === AsyncStatus.Error && ( + + {rotateState.error.message} + + )} + + + + )} {developerTools && ( fetch events arrive. If navigator.serviceWorker.controller + // is already set (normal page reload), this eliminates the race where + // preloadedSession (potentially stale) would be used for early thumbnail fetches. + sendSessionToSW(); + navigator.serviceWorker .register(swUrl) .then(sendSessionToSW)