From 9d10e52a55d19cd7eeb5f51c8fdcc4f6a4b2e0f1 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Thu, 14 May 2026 20:09:23 +0200 Subject: [PATCH] fix(MainView): collapse CREATED/ENDED listeners into a session-aware reducer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent useNitroEvent listeners updated landingViewVisible from RoomSessionEvent.CREATED and ENDED with no notion of which session was active. Under flaky websocket reconnects the events can land out of order: a stale ENDED for the previous room arrives after CREATED for the new one, flips landingViewVisible back to true, and the user is left at the hotel view inside a room (or vice versa) until the next room change. Folds both events into one useNitroEventReducer that carries the tracked sessionId. CREATED sets the id and closes the landing view; ENDED is applied only when its event.session.roomId matches the tracked id (or no session is active) — otherwise it's a stale ENDED for a previous session and is ignored. The reducer companion is the existing useNitroEventReducer from src/hooks/events, so no new infrastructure. Moves the entry in docs/ARCHITECTURE.md from "Open" to "Recently fixed". --- docs/ARCHITECTURE.md | 48 ++++++++----------------------------- src/components/MainView.tsx | 30 +++++++++++++++++++---- 2 files changed, 36 insertions(+), 42 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 57fca54..fdbfcec 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -776,44 +776,6 @@ this repo. ### Open -#### `MainView` — race between `RoomSessionEvent.CREATED` and `ENDED` - -`src/components/MainView.tsx:47-48` writes the same `landingViewVisible` -state from two independent listeners with no session-token guard: - -```ts -useNitroEvent(RoomSessionEvent.CREATED, () => setLandingViewVisible(false)); -useNitroEvent(RoomSessionEvent.ENDED, e => setLandingViewVisible(e.openLandingView)); -``` - -If the events arrive out of order (fast reconnect, network reordering), -the final state contradicts the actual session state — landing view stuck -open inside a room, or stuck closed at the hotel view. Resolves on next -room change. - -**Fix shape** (deferred until `useNitroEventReducer` companion lands — -see proposal #1): - -```ts -// One reducer owns both events + the active session token -const { sessionId, landingViewVisible } = useNitroEventReducer<...>( - [RoomSessionEvent.CREATED, RoomSessionEvent.ENDED], - (state, e) => { - if (e.type === RoomSessionEvent.CREATED) { - return { sessionId: e.session.roomId, landingViewVisible: false }; - } - if (state.sessionId !== null && e.session.roomId !== state.sessionId) { - return state; // stale ENDED for old session, ignore - } - return { sessionId: null, landingViewVisible: e.openLandingView }; - }, - { sessionId: null, landingViewVisible: true } -); -``` - -**Severity**: edge case, observed only after unstable websocket -reconnects. UX-degrading, not data-corrupting. - #### `LayoutFurniImageView` / `LayoutAvatarImageView` — async fetch race In both files an effect kicks off an async `processAsImageUrl` / @@ -832,6 +794,16 @@ data-corrupting. ### Recently fixed (in this branch) +- **`MainView` CREATED/ENDED race fixed.** Two independent + `useNitroEvent` listeners on `RoomSessionEvent.CREATED` / + `RoomSessionEvent.ENDED` could land out of order under flaky + reconnects, leaving `landingViewVisible` contradicting the actual + session state. Replaced with a single `useNitroEventReducer` that + carries the active session's `roomId`: a CREATED bumps the tracked + id and closes the landing view; an ENDED is honored only if its + `event.session.roomId` matches the tracked id (or no session is + active), otherwise it's a stale ENDED for a previous session and + gets ignored. - **Doorbell close button didn't close** while users were pending (`useEffect(() => setIsVisible(!!users.length))` overrode the close). Fixed by `src/components/room/widgets/doorbell/DoorbellWidgetView.tsx` diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index 8df9234..c620ad7 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -1,7 +1,7 @@ import { AddLinkEventTracker, GetCommunication, GetRoomSessionManager, HabboWebTools, ILinkEventTracker, RemoveLinkEventTracker, RoomSessionEvent } from '@nitrots/nitro-renderer'; import { AnimatePresence, motion } from 'framer-motion'; import { FC, useEffect, useState } from 'react'; -import { useNitroEvent } from '../hooks'; +import { useNitroEventReducer } from '../hooks'; import { AchievementsView } from './achievements/AchievementsView'; import { AvatarEditorView } from './avatar-editor'; import { BadgeCreatorView } from './badge-creator'; @@ -42,11 +42,33 @@ import { WiredCreatorToolsView } from './wired-tools/WiredCreatorToolsView'; export const MainView: FC<{}> = props => { const [ isReady, setIsReady ] = useState(false); - const [ landingViewVisible, setLandingViewVisible ] = useState(true); const [ localizationVersion, setLocalizationVersion ] = useState(0); - useNitroEvent(RoomSessionEvent.CREATED, event => setLandingViewVisible(false)); - useNitroEvent(RoomSessionEvent.ENDED, event => setLandingViewVisible(event.openLandingView)); + // CREATED and ENDED can arrive out of order under flaky reconnects. + // Treating them as two independent setters left landingViewVisible + // contradicting the actual session state (stuck open in-room or + // stuck closed at the hotel view). The reducer carries the active + // session's roomId so a stale ENDED for a previous session is + // ignored — only an ENDED matching the tracked session (or when + // no session is active) is honored. + const { landingViewVisible } = useNitroEventReducer<{ sessionId: number | null; landingViewVisible: boolean }, RoomSessionEvent>( + [ RoomSessionEvent.CREATED, RoomSessionEvent.ENDED ], + (state, event) => + { + if(event.type === RoomSessionEvent.CREATED) + { + return { sessionId: event.session.roomId, landingViewVisible: false }; + } + + if((state.sessionId !== null) && (event.session.roomId !== state.sessionId)) + { + return state; + } + + return { sessionId: null, landingViewVisible: event.openLandingView }; + }, + { sessionId: null, landingViewVisible: true } + ); useEffect(() => {