Enable <StrictMode> + make App.tsx renderer init idempotent

App.tsx's prepare() useEffect ran four .init() calls
(SessionDataManager, RoomSessionManager, RoomEngine, Communication)
without any guard, plus an immediate heartbeat ping and a legacy
authentication track. Under StrictMode dev double-invoke, those
fire twice — risking duplicate session/communication state.

- Gate the four .init() chain behind gameInitPromiseRef: both the
  first and the simulated second invocation await the same promise.
- Gate the legacy track + immediate heartbeat behind bootstrapDoneRef.
- Heartbeat and remember-rotate intervals were already idempotent
  (clearInterval before setInterval); ticker registration was already
  guarded by tickersStartedRef; renderer/warmup were already gated by
  rendererPromiseRef/warmupPromiseRef. No change needed there.

Wrap <App /> in <StrictMode> in src/index.tsx now that the renderer
init path is double-invoke safe.

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
This commit is contained in:
simoleo89
2026-05-11 16:31:50 +00:00
parent 535fa71020
commit 25d51aff3f
2 changed files with 34 additions and 18 deletions
+15 -1
View File
@@ -74,6 +74,8 @@ export const App: FC<{}> = props =>
const [ prepareTrigger, setPrepareTrigger ] = useState(0); const [ prepareTrigger, setPrepareTrigger ] = useState(0);
const warmupPromiseRef = useRef<Promise<void>>(null); const warmupPromiseRef = useRef<Promise<void>>(null);
const rendererPromiseRef = useRef<Promise<any>>(null); const rendererPromiseRef = useRef<Promise<any>>(null);
const gameInitPromiseRef = useRef<Promise<void> | null>(null);
const bootstrapDoneRef = useRef(false);
const tickersStartedRef = useRef(false); const tickersStartedRef = useRef(false);
const heartbeatIntervalRef = useRef<number>(null); const heartbeatIntervalRef = useRef<number>(null);
const rememberRotateIntervalRef = useRef<number>(null); const rememberRotateIntervalRef = useRef<number>(null);
@@ -385,14 +387,26 @@ export const App: FC<{}> = props =>
const renderer = await startRenderer(width, height); const renderer = await startRenderer(width, height);
await startWarmup(width, height); await startWarmup(width, height);
if(!gameInitPromiseRef.current)
{
gameInitPromiseRef.current = (async () =>
{
await GetSessionDataManager().init(); await GetSessionDataManager().init();
await GetRoomSessionManager().init(); await GetRoomSessionManager().init();
await GetRoomEngine().init(); await GetRoomEngine().init();
await GetCommunication().init(); await GetCommunication().init();
})();
}
await gameInitPromiseRef.current;
if(!bootstrapDoneRef.current)
{
bootstrapDoneRef.current = true;
if(LegacyExternalInterface.available) LegacyExternalInterface.call('legacyTrack', 'authentication', 'authok', []); if(LegacyExternalInterface.available) LegacyExternalInterface.call('legacyTrack', 'authentication', 'authok', []);
HabboWebTools.sendHeartBeat(); HabboWebTools.sendHeartBeat();
}
if(heartbeatIntervalRef.current !== null) window.clearInterval(heartbeatIntervalRef.current); if(heartbeatIntervalRef.current !== null) window.clearInterval(heartbeatIntervalRef.current);
heartbeatIntervalRef.current = window.setInterval(() => HabboWebTools.sendHeartBeat(), 10000); heartbeatIntervalRef.current = window.setInterval(() => HabboWebTools.sendHeartBeat(), 10000);
+3 -1
View File
@@ -1,4 +1,4 @@
import { Suspense } from 'react'; import { StrictMode, Suspense } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { App } from './App'; import { App } from './App';
@@ -44,6 +44,7 @@ import './css/toolbar/ToolBar.css';
import './css/widgets/FurnitureWidgets.css'; import './css/widgets/FurnitureWidgets.css';
createRoot(document.getElementById('root')).render( createRoot(document.getElementById('root')).render(
<StrictMode>
<ErrorBoundary <ErrorBoundary
fallbackRender={ ({ error }) => ( fallbackRender={ ({ error }) => (
<LoadingView <LoadingView
@@ -55,4 +56,5 @@ createRoot(document.getElementById('root')).render(
<App /> <App />
</Suspense> </Suspense>
</ErrorBoundary> </ErrorBoundary>
</StrictMode>
); );