Implement instant websocket room updates

This commit is contained in:
2026-08-25 22:23:38 +02:00
parent bfc93dceba
commit d6e5455e90
18 changed files with 303 additions and 130 deletions
+63 -51
View File
@@ -1,72 +1,73 @@
import { router } from '@inertiajs/vue3';
import { onScopeDispose, watch } from 'vue';
import { onScopeDispose, ref, shallowReadonly, watch } from 'vue';
import { getEcho } from '@/lib/echo';
import type { RoomStateChangedPayload } from '@/types/room';
import { home } from '@/routes';
import { join } from '@/routes/room';
import type {
CurrentRoomPlayer,
PokerRoom,
RoomStateChangedPayload,
} from '@/types/room';
const ROOM_EVENT = '.room.state.changed';
export function useRoomRealtime(
roomId: () => string,
stateVersion: () => number,
): void {
let reloadInProgress = false;
let pendingVersion = 0;
let disposed = false;
function reloadRoom(): void {
if (reloadInProgress || disposed) {
return;
}
reloadInProgress = true;
router.reload({
only: ['room', 'currentPlayer'],
onFinish: () => {
reloadInProgress = false;
if (disposed) {
return;
}
if (pendingVersion > stateVersion()) {
reloadRoom();
return;
}
pendingVersion = stateVersion();
},
});
}
initialRoom: () => PokerRoom,
initialCurrentPlayer: () => CurrentRoomPlayer,
) {
const room = ref<PokerRoom>({ ...initialRoom() });
const currentPlayer = ref<CurrentRoomPlayer>({ ...initialCurrentPlayer() });
function handleRoomChange(payload: RoomStateChangedPayload): void {
if (
typeof payload.version !== 'number' ||
payload.version <= Math.max(stateVersion(), pendingVersion)
payload.version <= room.value.state_version
) {
return;
}
pendingVersion = payload.version;
if (payload.change === 'room_deleted') {
router.visit('/', { replace: true });
router.visit(home().url, { replace: true });
return;
}
reloadRoom();
if (payload.change === 'room_closed') {
router.visit(
join({ query: { join_code: room.value.join_code } }).url,
{ replace: true },
);
return;
}
if (payload.state === null || payload.state === undefined) {
return;
}
room.value = {
...room.value,
...payload.state,
};
if (
payload.change === 'votes_reset' ||
payload.change === 'next_round'
) {
currentPlayer.value = {
...currentPlayer.value,
current_vote: null,
};
}
}
const stopWatchingRoom = watch(
roomId,
() => initialRoom().id,
(nextRoomId, _previousRoomId, onCleanup) => {
const echo = getEcho();
const channelName = `rooms.${nextRoomId}`;
pendingVersion = stateVersion();
echo.channel(channelName).listen(ROOM_EVENT, handleRoomChange);
onCleanup(() => {
@@ -76,19 +77,30 @@ export function useRoomRealtime(
{ immediate: true },
);
const stopWatchingVersion = watch(
stateVersion,
(version) => {
if (!reloadInProgress) {
pendingVersion = Math.max(pendingVersion, version);
}
const stopWatchingInitialState = watch(
[initialRoom, initialCurrentPlayer],
([nextRoom, nextCurrentPlayer]) => {
room.value = { ...nextRoom };
currentPlayer.value = { ...nextCurrentPlayer };
},
{ flush: 'sync' },
{ deep: true },
);
onScopeDispose(() => {
disposed = true;
stopWatchingRoom();
stopWatchingVersion();
stopWatchingInitialState();
});
function setCurrentVote(value: string | null): void {
currentPlayer.value = {
...currentPlayer.value,
current_vote: value,
};
}
return {
room: shallowReadonly(room),
currentPlayer: shallowReadonly(currentPlayer),
setCurrentVote,
};
}