🐛(frontend) fix concurrent PATCH races on room settings

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.
This commit is contained in:
lebaudantoine
2026-08-05 17:04:45 +02:00
committed by aleb_the_flash
parent c23f449520
commit 22a1713c60
6 changed files with 23 additions and 28 deletions
+1
View File
@@ -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
@@ -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<ApiRoom, ApiError, PatchRoomParams>
) {
return useMutation<ApiRoom, ApiError, PatchRoomParams>({
mutationKey: patchRoomMutationKey,
mutationFn: patchRoom,
onSuccess: options?.onSuccess,
onMutate: async ({ roomId, room: partialRoom }) => {
await queryClient.cancelQueries({ queryKey: [keys.room, roomId] })
queryClient.setQueryData<ApiRoom>([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,
})
}
@@ -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={[
{
@@ -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)
@@ -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
@@ -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={[
{