Files
Nitro_Render_V3/packages/session/src/RoomSessionManager.ts
T
simoleo89 87cf47847c feat(events,session): add React-friendly subscribe APIs and snapshot getters
Adds backwards-compatible primitives needed to consume the renderer from
React 19 hooks (useSyncExternalStore, use(), TanStack Query) without
re-architecting the event bus.

- EventDispatcher.subscribe(type, cb): () => void — unsubscriber-returning
  wrapper matching the useSyncExternalStore subscribe signature.
- CommunicationManager.subscribeMessage(eventCtor, handler): () => void —
  packet-stream equivalent.
- SessionDataManager.getUserDataSnapshot() and
  RoomSessionManager.getActiveRoomSessionSnapshot() — referentially-stable
  read-only views invalidated through new SESSION_DATA_UPDATED and
  ROOM_SESSION_UPDATED events.

All additive; existing addEventListener/removeEventListener / IRoomSession
APIs are unchanged. Bumps renderer to 2.1.0.
2026-05-10 19:16:32 +02:00

557 lines
16 KiB
TypeScript

import { IRoomHandlerListener, IRoomSession, IRoomSessionManager, IRoomSessionSnapshot } from '@nitrots/api';
import { GetCommunication, RoomEnterComposer, RoomUnitWalkComposer } from '@nitrots/communication';
import { GetEventDispatcher, NitroEvent, NitroEventType, RoomSessionEvent } from '@nitrots/events';
import { NitroLogger } from '@nitrots/utils';
import { RoomSession } from './RoomSession';
import { BaseHandler, GenericErrorHandler, PetPackageHandler, PollHandler, RoomChatHandler, RoomDataHandler, RoomDimmerPresetsHandler, RoomPermissionsHandler, RoomPresentHandler, RoomSessionHandler, RoomUsersHandler, WordQuizHandler } from './handler';
const STORAGE_KEY_ROOM_ID = 'nitro.session.lastRoomId';
const STORAGE_KEY_ROOM_PASSWORD = 'nitro.session.lastRoomPassword';
const STORAGE_KEY_POS_X = 'nitro.session.lastPosX';
const STORAGE_KEY_POS_Y = 'nitro.session.lastPosY';
export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerListener
{
private _handlers: BaseHandler[] = [];
private _sessions: Map<string, IRoomSession> = new Map();
private _pendingSession: IRoomSession = null;
private _sessionStarting: boolean = false;
private _viewerSession: IRoomSession = null;
private _lastRoomId: number = -1;
private _lastRoomPassword: string = null;
private _isReconnecting: boolean = false;
private _reconnectGuardTimer: ReturnType<typeof setTimeout> = null;
private _pendingRoomClear: ReturnType<typeof setTimeout> = null;
private _savedPosX: number = -1;
private _savedPosY: number = -1;
private _activeRoomSessionSnapshot: Readonly<IRoomSessionSnapshot> | null = null;
private invalidateRoomSessionSnapshot(): void
{
this._activeRoomSessionSnapshot = null;
GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.ROOM_SESSION_UPDATED));
}
public getActiveRoomSessionSnapshot(): Readonly<IRoomSessionSnapshot> | null
{
const session = this._viewerSession;
if(!session) return null;
if(this._activeRoomSessionSnapshot && this._activeRoomSessionSnapshot.session === session) return this._activeRoomSessionSnapshot;
this._activeRoomSessionSnapshot = Object.freeze<IRoomSessionSnapshot>({
roomId: session.roomId,
state: session.state,
isRoomOwner: session.isRoomOwner,
isSpectator: session.isSpectator,
isDecorating: session.isDecorating,
isGuildRoom: session.isGuildRoom,
isPrivateRoom: session.isPrivateRoom,
controllerLevel: session.controllerLevel,
doorMode: session.doorMode,
tradeMode: session.tradeMode,
allowPets: session.allowPets,
groupId: session.groupId,
session
});
return this._activeRoomSessionSnapshot;
}
public async init(): Promise<void>
{
this.createHandlers();
this.processPendingSession();
this.setupReconnectListener();
this.checkPersistedRoom();
}
private checkPersistedRoom(): void
{
try
{
const storedRoomId = sessionStorage.getItem(STORAGE_KEY_ROOM_ID);
if(!storedRoomId) return;
const roomId = parseInt(storedRoomId, 10);
if(isNaN(roomId) || roomId <= 0) return;
this._lastRoomId = roomId;
this._lastRoomPassword = sessionStorage.getItem(STORAGE_KEY_ROOM_PASSWORD) || null;
this._isReconnecting = true;
}
catch(e){}
}
private createHandlers(): void
{
const connection = GetCommunication().connection;
if(!connection) return;
this._handlers.push(
new RoomChatHandler(connection, this),
new RoomDataHandler(connection, this),
new RoomDimmerPresetsHandler(connection, this),
new RoomPermissionsHandler(connection, this),
new RoomSessionHandler(connection, this),
new RoomUsersHandler(connection, this),
new RoomPresentHandler(connection, this),
new GenericErrorHandler(connection, this),
new WordQuizHandler(connection, this),
new PollHandler(connection, this),
new PetPackageHandler(connection, this),
);
}
private setupReconnectListener(): void
{
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_RECONNECTING, () =>
{
this.cancelRoomIdClear();
if(this._lastRoomId > 0)
{
this.persistRoom(this._lastRoomId, this._lastRoomPassword);
}
this._isReconnecting = true;
});
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_RECONNECTED, () =>
{
this.clearGuardTimer();
this._reconnectGuardTimer = setTimeout(() =>
{
this._reconnectGuardTimer = null;
if(!this._isReconnecting) return;
this.attemptRoomReEntry();
}, 5000);
});
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_REAUTHENTICATED, () =>
{
this.snapshotSavedPosition();
this.clearGuardTimer();
this.attemptRoomReEntry();
});
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_RECONNECT_FAILED, () =>
{
NitroLogger.log('[RoomSessionManager] SOCKET_RECONNECT_FAILED - clearing state');
this.clearGuardTimer();
this._isReconnecting = false;
this._lastRoomId = -1;
this._lastRoomPassword = null;
this.clearPersistedRoom();
this.clearPersistedPosition();
});
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_CLOSED, () =>
{
this.clearGuardTimer();
this._isReconnecting = false;
this._lastRoomId = -1;
this._lastRoomPassword = null;
this.clearPersistedRoom();
this.clearPersistedPosition();
});
}
private clearGuardTimer(): void
{
if(this._reconnectGuardTimer)
{
clearTimeout(this._reconnectGuardTimer);
this._reconnectGuardTimer = null;
}
}
private scheduleRoomIdClear(): void
{
if(this._pendingRoomClear)
{
clearTimeout(this._pendingRoomClear);
}
this._pendingRoomClear = setTimeout(() =>
{
this._pendingRoomClear = null;
this._lastRoomId = -1;
this._lastRoomPassword = null;
}, 5000);
}
private cancelRoomIdClear(): void
{
if(this._pendingRoomClear)
{
clearTimeout(this._pendingRoomClear);
this._pendingRoomClear = null;
}
}
private attemptRoomReEntry(): void
{
const roomId = this._lastRoomId;
const password = this._lastRoomPassword;
if(roomId <= 0)
{
this._isReconnecting = false;
return;
}
const existingSession = this.getSession(roomId);
if(existingSession)
{
GetCommunication().connection.send(new RoomEnterComposer(roomId, password, this._savedPosX, this._savedPosY));
this.clearGuardTimer();
this._reconnectGuardTimer = setTimeout(() =>
{
this._reconnectGuardTimer = null;
if(this._isReconnecting)
{
this._isReconnecting = false;
}
}, 5000);
return;
}
this._sessions.clear();
this._viewerSession = null;
this.invalidateRoomSessionSnapshot();
this.createSession(roomId, password, this._savedPosX, this._savedPosY);
this.clearGuardTimer();
this._reconnectGuardTimer = setTimeout(() =>
{
this._reconnectGuardTimer = null;
if(this._isReconnecting)
{
this._isReconnecting = false;
}
}, 10000);
}
public tryRestoreSession(): boolean
{
try
{
const storedRoomId = sessionStorage.getItem(STORAGE_KEY_ROOM_ID);
if(!storedRoomId) return false;
const roomId = parseInt(storedRoomId, 10);
if(isNaN(roomId) || roomId <= 0) return false;
const password = sessionStorage.getItem(STORAGE_KEY_ROOM_PASSWORD) || null;
let spawnX = -1;
let spawnY = -1;
try
{
const posX = sessionStorage.getItem(STORAGE_KEY_POS_X);
const posY = sessionStorage.getItem(STORAGE_KEY_POS_Y);
if(posX && posY)
{
spawnX = parseInt(posX, 10);
spawnY = parseInt(posY, 10);
if(isNaN(spawnX) || isNaN(spawnY)) { spawnX = -1; spawnY = -1; }
}
}
catch(e) {}
this._isReconnecting = true;
this.createSession(roomId, password, spawnX, spawnY);
this.clearGuardTimer();
this._reconnectGuardTimer = setTimeout(() =>
{
this._reconnectGuardTimer = null;
if(this._isReconnecting)
{
this._isReconnecting = false;
}
}, 10000);
return true;
}
catch(e)
{
return false;
}
}
private persistRoom(roomId: number, password: string): void
{
try
{
if(roomId > 0)
{
sessionStorage.setItem(STORAGE_KEY_ROOM_ID, roomId.toString());
if(password)
{
sessionStorage.setItem(STORAGE_KEY_ROOM_PASSWORD, password);
}
else
{
sessionStorage.removeItem(STORAGE_KEY_ROOM_PASSWORD);
}
}
else
{
this.clearPersistedRoom();
}
}
catch(e) {}
}
private clearPersistedRoom(): void
{
try
{
sessionStorage.removeItem(STORAGE_KEY_ROOM_ID);
sessionStorage.removeItem(STORAGE_KEY_ROOM_PASSWORD);
}
catch(e) {}
}
private clearPersistedPosition(): void
{
try
{
sessionStorage.removeItem(STORAGE_KEY_POS_X);
sessionStorage.removeItem(STORAGE_KEY_POS_Y);
}
catch(e) {}
}
private snapshotSavedPosition(): void
{
try
{
const posX = sessionStorage.getItem(STORAGE_KEY_POS_X);
const posY = sessionStorage.getItem(STORAGE_KEY_POS_Y);
if(!posX || !posY) return;
this._savedPosX = parseInt(posX, 10);
this._savedPosY = parseInt(posY, 10);
}
catch(e)
{
this._savedPosX = -1;
this._savedPosY = -1;
}
}
private setHandlers(session: IRoomSession): void
{
if(!this._handlers || !this._handlers.length) return;
for(const handler of this._handlers)
{
if(!handler) continue;
handler.setRoomId(session.roomId);
}
}
private processPendingSession(): void
{
if(!this._pendingSession) return;
this.addSession(this._pendingSession);
this._pendingSession = null;
}
public getSession(id: number): IRoomSession
{
const existing = this._sessions.get(this.getRoomId(id));
if(!existing) return null;
return existing;
}
public createSession(roomId: number, password: string = null, spawnX: number = -1, spawnY: number = -1): boolean
{
const session = new RoomSession();
session.roomId = roomId;
session.password = password;
session.spawnX = spawnX;
session.spawnY = spawnY;
return this.addSession(session);
}
private addSession(roomSession: IRoomSession): boolean
{
this._sessionStarting = true;
if(this._sessions.get(this.getRoomId(roomSession.roomId))) this.removeSession(roomSession.roomId, false);
this._sessions.set(this.getRoomId(roomSession.roomId), roomSession);
GetEventDispatcher().dispatchEvent(new RoomSessionEvent(RoomSessionEvent.CREATED, roomSession));
this._viewerSession = roomSession;
this._lastRoomId = roomSession.roomId;
this._lastRoomPassword = roomSession.password;
this.persistRoom(roomSession.roomId, roomSession.password);
this.invalidateRoomSessionSnapshot();
this.startSession(this._viewerSession);
return true;
}
public startSession(session: IRoomSession): boolean
{
if(session.state === RoomSessionEvent.STARTED) return false;
this._sessionStarting = false;
if(!session.start())
{
this.removeSession(session.roomId);
return false;
}
GetEventDispatcher().dispatchEvent(new RoomSessionEvent(RoomSessionEvent.STARTED, session));
this.setHandlers(session);
this.invalidateRoomSessionSnapshot();
return true;
}
public removeSession(id: number, openLandingView: boolean = true): void
{
const session = this.getSession(id);
if(!session) return;
if(this._isReconnecting)
{
return;
}
this._sessions.delete(this.getRoomId(id));
if(openLandingView)
{
this.clearPersistedRoom();
this.scheduleRoomIdClear();
}
GetEventDispatcher().dispatchEvent(new RoomSessionEvent(RoomSessionEvent.ENDED, session, openLandingView));
if(this._viewerSession === session) this._viewerSession = null;
this.invalidateRoomSessionSnapshot();
}
public sessionUpdate(id: number, type: string): void
{
const session = this.getSession(id);
if(!session)
{
return;
}
switch(type)
{
case RoomSessionHandler.RS_CONNECTED:
return;
case RoomSessionHandler.RS_READY:
if(this._isReconnecting)
{
if(this._savedPosX >= 0 && this._savedPosY >= 0)
{
GetCommunication().connection.send(new RoomUnitWalkComposer(this._savedPosX, this._savedPosY));
this._savedPosX = -1;
this._savedPosY = -1;
}
this.clearGuardTimer();
this._reconnectGuardTimer = setTimeout(() =>
{
this._reconnectGuardTimer = null;
if(this._isReconnecting)
{
this._isReconnecting = false;
}
}, 3000);
}
return;
case RoomSessionHandler.RS_DISCONNECTED:
if(this._isReconnecting) return;
this.removeSession(id);
return;
}
}
public sessionReinitialize(fromRoomId: number, toRoomId: number): void
{
const existing = this.getSession(fromRoomId);
if(!existing) return;
this._sessions.delete(this.getRoomId(fromRoomId));
existing.reset(toRoomId);
this._sessions.set(this.getRoomId(toRoomId), existing);
this._lastRoomId = toRoomId;
this.persistRoom(toRoomId, existing.password);
this.setHandlers(existing);
}
private getRoomId(id: number): string
{
return 'hard_coded_room_id';
}
public get viewerSession(): IRoomSession
{
return this._viewerSession;
}
public get isReconnecting(): boolean
{
return this._isReconnecting;
}
}