From 22a1713c605afacaa4e867a0076c2912cd7b9459 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Wed, 5 Aug 2026 17:04:45 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(frontend)=20fix=20concurrent=20PAT?= =?UTF-8?q?CH=20races=20on=20room=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rapid toggles could persist a stale configuration: each PATCH replaces the full room config, and every call site built it from a render-time snapshot. A toggle issued before the previous one resolved therefore overwrote the newer value with an older one. Handle the cache centrally in usePatchRoom so the next toggle always reads an up-to-date configuration. --- CHANGELOG.md | 1 + .../src/features/rooms/api/patchRoom.tsx | 18 +++++++++++++++++- .../rooms/livekit/components/Admin.tsx | 7 +------ .../livekit/hooks/usePermissionsManager.ts | 6 +----- .../livekit/hooks/usePublishSourcesManager.ts | 6 +----- .../src/features/sdk/routes/SettingsPopup.tsx | 13 ++----------- 6 files changed, 23 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b198d70f..092600b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ and this project adheres to - 💄(frontend) show pointer cursor on interactive switches - 🐛(frontend) fix icon centering in the Switch primitive - 🐛(frontend) keep Unicode initials intact in avatar +- 🐛(frontend) prevent concurrent settings updates from overwriting each other ## [1.24.0] - 2026-07-21 diff --git a/src/frontend/src/features/rooms/api/patchRoom.tsx b/src/frontend/src/features/rooms/api/patchRoom.tsx index 0ce59009..b04cc8e0 100644 --- a/src/frontend/src/features/rooms/api/patchRoom.tsx +++ b/src/frontend/src/features/rooms/api/patchRoom.tsx @@ -2,6 +2,8 @@ import { type ApiRoom } from './ApiRoom' import { fetchApi } from '@/api/fetchApi' import { useMutation, type UseMutationOptions } from '@tanstack/react-query' import type { ApiError } from '@/api/ApiError' +import { queryClient } from '@/api/queryClient' +import { keys } from '@/api/queryKeys' export type PatchRoomParams = { roomId: string @@ -15,11 +17,25 @@ export const patchRoom = ({ roomId, room }: PatchRoomParams) => { }) } +export const patchRoomMutationKey = ['patchRoom'] + export function usePatchRoom( options?: UseMutationOptions ) { return useMutation({ + mutationKey: patchRoomMutationKey, mutationFn: patchRoom, - onSuccess: options?.onSuccess, + onMutate: async ({ roomId, room: partialRoom }) => { + await queryClient.cancelQueries({ queryKey: [keys.room, roomId] }) + queryClient.setQueryData([keys.room, roomId], (previous) => + previous ? { ...previous, ...partialRoom } : previous + ) + }, + onSettled: (_data, _error, { roomId }) => { + if (queryClient.isMutating({ mutationKey: patchRoomMutationKey }) === 1) { + queryClient.invalidateQueries({ queryKey: [keys.room, roomId] }) + } + }, + ...options, }) } diff --git a/src/frontend/src/features/rooms/livekit/components/Admin.tsx b/src/frontend/src/features/rooms/livekit/components/Admin.tsx index 8942de3b..86142d90 100644 --- a/src/frontend/src/features/rooms/livekit/components/Admin.tsx +++ b/src/frontend/src/features/rooms/livekit/components/Admin.tsx @@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next' import { usePatchRoom } from '@/features/rooms/api/patchRoom' import { fetchRoom } from '@/features/rooms/api/fetchRoom' import { ApiAccessLevel } from '@/features/rooms/api/ApiRoom' -import { queryClient } from '@/api/queryClient' import { keys } from '@/api/queryKeys' import { useQuery } from '@tanstack/react-query' import { useParams } from 'wouter' @@ -206,11 +205,7 @@ export const Admin = () => { patchRoom({ roomId, room: { access_level: value as ApiAccessLevel }, - }) - .then((room) => { - queryClient.setQueryData([keys.room, roomId], room) - }) - .catch((e) => console.error(e)) + }).catch((e) => console.error(e)) } items={[ { diff --git a/src/frontend/src/features/rooms/livekit/hooks/usePermissionsManager.ts b/src/frontend/src/features/rooms/livekit/hooks/usePermissionsManager.ts index a59c3068..2cf292a9 100644 --- a/src/frontend/src/features/rooms/livekit/hooks/usePermissionsManager.ts +++ b/src/frontend/src/features/rooms/livekit/hooks/usePermissionsManager.ts @@ -1,8 +1,6 @@ import { usePatchRoom } from '@/features/rooms/api/patchRoom' import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData' import { useCallback } from 'react' -import { queryClient } from '@/api/queryClient' -import { keys } from '@/api/queryKeys' export const usePermissionsManager = () => { const { mutateAsync: patchRoom } = usePatchRoom() @@ -23,13 +21,11 @@ export const usePermissionsManager = () => { everyone_can_mute: enabled, } - const room = await patchRoom({ + await patchRoom({ roomId, room: { configuration: newConfiguration }, }) - queryClient.setQueryData([keys.room, roomId], room) - return { configuration: newConfiguration } } catch (error) { console.error('Failed to update muting permission:', error) diff --git a/src/frontend/src/features/rooms/livekit/hooks/usePublishSourcesManager.ts b/src/frontend/src/features/rooms/livekit/hooks/usePublishSourcesManager.ts index f5bd1666..dae46921 100644 --- a/src/frontend/src/features/rooms/livekit/hooks/usePublishSourcesManager.ts +++ b/src/frontend/src/features/rooms/livekit/hooks/usePublishSourcesManager.ts @@ -1,7 +1,5 @@ import { RoomEvent, Track } from 'livekit-client' import { useCallback, useMemo } from 'react' -import { queryClient } from '@/api/queryClient' -import { keys } from '@/api/queryKeys' import { useConfig } from '@/api/useConfig' import { usePatchRoom } from '@/features/rooms/api/patchRoom' import { useRemoteParticipants } from '@livekit/components-react' @@ -79,13 +77,11 @@ export const usePublishSourcesManager = () => { can_publish_sources: newSources, } - const room = await patchRoom({ + await patchRoom({ roomId, room: { configuration: newConfiguration }, }) - queryClient.setQueryData([keys.room, roomId], room) - await updateParticipantsPermissions( unprivilegedRemoteParticipants, newSources diff --git a/src/frontend/src/features/sdk/routes/SettingsPopup.tsx b/src/frontend/src/features/sdk/routes/SettingsPopup.tsx index cf3dfa9d..4246a003 100644 --- a/src/frontend/src/features/sdk/routes/SettingsPopup.tsx +++ b/src/frontend/src/features/sdk/routes/SettingsPopup.tsx @@ -7,7 +7,6 @@ import { css } from '@/styled-system/css' import { Button, Field, H, Text } from '@/primitives' import { Spinner } from '@/primitives/Spinner' import { keys } from '@/api/queryKeys' -import { queryClient } from '@/api/queryClient' import { useConfig } from '@/api/useConfig' import { useUser } from '@/features/auth/api/useUser' import { authUrl } from '@/features/auth/utils/authUrl' @@ -109,11 +108,7 @@ const SettingsPopup = () => { patchRoom({ roomId: roomSlug, room: { configuration: newConfiguration }, - }) - .then((updatedRoom) => { - queryClient.setQueryData([keys.room, roomSlug], updatedRoom) - }) - .catch((e) => console.error(e)) + }).catch((e) => console.error(e)) } const updateSource = (sources: Source[], enabled: boolean) => { @@ -334,11 +329,7 @@ const SettingsPopup = () => { patchRoom({ roomId: roomSlug, room: { access_level: value as ApiAccessLevel }, - }) - .then((updatedRoom) => { - queryClient.setQueryData([keys.room, roomSlug], updatedRoom) - }) - .catch((e) => console.error(e)) + }).catch((e) => console.error(e)) } items={[ {