mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
fix(MainView): collapse CREATED/ENDED listeners into a session-aware reducer
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".
This commit is contained in:
+10
-38
@@ -776,44 +776,6 @@ this repo.
|
|||||||
|
|
||||||
### Open
|
### 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
|
#### `LayoutFurniImageView` / `LayoutAvatarImageView` — async fetch race
|
||||||
|
|
||||||
In both files an effect kicks off an async `processAsImageUrl` /
|
In both files an effect kicks off an async `processAsImageUrl` /
|
||||||
@@ -832,6 +794,16 @@ data-corrupting.
|
|||||||
|
|
||||||
### Recently fixed (in this branch)
|
### 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
|
- **Doorbell close button didn't close** while users were pending
|
||||||
(`useEffect(() => setIsVisible(!!users.length))` overrode the close).
|
(`useEffect(() => setIsVisible(!!users.length))` overrode the close).
|
||||||
Fixed by `src/components/room/widgets/doorbell/DoorbellWidgetView.tsx`
|
Fixed by `src/components/room/widgets/doorbell/DoorbellWidgetView.tsx`
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { AddLinkEventTracker, GetCommunication, GetRoomSessionManager, HabboWebTools, ILinkEventTracker, RemoveLinkEventTracker, RoomSessionEvent } from '@nitrots/nitro-renderer';
|
import { AddLinkEventTracker, GetCommunication, GetRoomSessionManager, HabboWebTools, ILinkEventTracker, RemoveLinkEventTracker, RoomSessionEvent } from '@nitrots/nitro-renderer';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { FC, useEffect, useState } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { useNitroEvent } from '../hooks';
|
import { useNitroEventReducer } from '../hooks';
|
||||||
import { AchievementsView } from './achievements/AchievementsView';
|
import { AchievementsView } from './achievements/AchievementsView';
|
||||||
import { AvatarEditorView } from './avatar-editor';
|
import { AvatarEditorView } from './avatar-editor';
|
||||||
import { BadgeCreatorView } from './badge-creator';
|
import { BadgeCreatorView } from './badge-creator';
|
||||||
@@ -42,11 +42,33 @@ import { WiredCreatorToolsView } from './wired-tools/WiredCreatorToolsView';
|
|||||||
export const MainView: FC<{}> = props =>
|
export const MainView: FC<{}> = props =>
|
||||||
{
|
{
|
||||||
const [ isReady, setIsReady ] = useState(false);
|
const [ isReady, setIsReady ] = useState(false);
|
||||||
const [ landingViewVisible, setLandingViewVisible ] = useState(true);
|
|
||||||
const [ localizationVersion, setLocalizationVersion ] = useState(0);
|
const [ localizationVersion, setLocalizationVersion ] = useState(0);
|
||||||
|
|
||||||
useNitroEvent<RoomSessionEvent>(RoomSessionEvent.CREATED, event => setLandingViewVisible(false));
|
// CREATED and ENDED can arrive out of order under flaky reconnects.
|
||||||
useNitroEvent<RoomSessionEvent>(RoomSessionEvent.ENDED, event => setLandingViewVisible(event.openLandingView));
|
// 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(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user