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.
This commit is contained in:
simoleo89
2026-05-10 19:16:32 +02:00
parent 98b03aa0be
commit 87cf47847c
13 changed files with 193 additions and 5 deletions
@@ -7,4 +7,5 @@ export interface IEventDispatcher
removeEventListener(type: string, callback: Function): void;
removeAllListeners(): void;
dispatchEvent<T extends INitroEvent>(event: T): boolean;
subscribe<T extends INitroEvent>(type: string | string[], callback: (event: T) => void): () => void;
}
@@ -6,5 +6,6 @@ export interface ICommunicationManager
init(): Promise<void>;
registerMessageEvent(event: IMessageEvent): IMessageEvent;
removeMessageEvent(event: IMessageEvent): void;
subscribeMessage<T extends IMessageEvent>(eventCtor: new (callback: (event: T) => void) => T, handler: (event: T) => void): () => void;
connection: IConnection;
}
@@ -1,4 +1,5 @@
import { IRoomSession } from './IRoomSession';
import { IRoomSessionSnapshot } from './IRoomSessionSnapshot';
export interface IRoomSessionManager
{
@@ -8,5 +9,6 @@ export interface IRoomSessionManager
startSession(session: IRoomSession): boolean;
removeSession(id: number, openLandingView?: boolean): void;
tryRestoreSession(): boolean;
getActiveRoomSessionSnapshot(): Readonly<IRoomSessionSnapshot> | null;
viewerSession: IRoomSession;
}
@@ -0,0 +1,18 @@
import { IRoomSession } from './IRoomSession';
export interface IRoomSessionSnapshot
{
roomId: number;
state: string;
isRoomOwner: boolean;
isSpectator: boolean;
isDecorating: boolean;
isGuildRoom: boolean;
isPrivateRoom: boolean;
controllerLevel: number;
doorMode: number;
tradeMode: number;
allowPets: boolean;
groupId: number;
session: IRoomSession;
}
@@ -3,6 +3,7 @@ import { IFurnitureData } from './IFurnitureData';
import { IGroupInformationManager } from './IGroupInformationManager';
import { IIgnoredUsersManager } from './IIgnoredUsersManager';
import { IProductData } from './IProductData';
import { IUserDataSnapshot } from './IUserDataSnapshot';
export interface ISessionDataManager
{
@@ -53,4 +54,5 @@ export interface ISessionDataManager
isCameraFollowDisabled: boolean;
uiFlags: number;
tags: string[];
getUserDataSnapshot(): Readonly<IUserDataSnapshot>;
}
@@ -0,0 +1,22 @@
export interface IUserDataSnapshot
{
userId: number;
userName: string;
figure: string;
gender: string;
realName: string;
respectsReceived: number;
respectsLeft: number;
respectsPetLeft: number;
canChangeName: boolean;
clubLevel: number;
securityLevel: number;
isAmbassador: boolean;
isEmailVerified: boolean;
isNoob: boolean;
isAuthenticHabbo: boolean;
isSystemOpen: boolean;
isSystemShutdown: boolean;
uiFlags: number;
tags: ReadonlyArray<string>;
}
+2
View File
@@ -18,6 +18,8 @@ export * from './IRoomSessionManager';
export * from './IRoomUserData';
export * from './ISessionDataManager';
export * from './IUserDataManager';
export * from './IUserDataSnapshot';
export * from './IRoomSessionSnapshot';
export * from './PetBreedingResultData';
export * from './PetCustomPart';
export * from './PetFigureData';
@@ -203,6 +203,17 @@ export class CommunicationManager implements ICommunicationManager
this._connection.removeMessageEvent(event);
}
public subscribeMessage<T extends IMessageEvent>(eventCtor: new (callback: (event: T) => void) => T, handler: (event: T) => void): () => void
{
if(!eventCtor || !handler) return () => {};
const event = new eventCtor(handler);
this.registerMessageEvent(event);
return () => this.removeMessageEvent(event);
}
public get connection(): IConnection
{
return this._connection;
+19
View File
@@ -101,4 +101,23 @@ export class EventDispatcher implements IEventDispatcher
{
this._listeners.clear();
}
public subscribe<T extends INitroEvent>(type: string | string[], callback: (event: T) => void): () => void
{
if(!type || !callback) return () => {};
if(Array.isArray(type))
{
for(const t of type) this.addEventListener<T>(t, callback);
return () =>
{
for(const t of type) this.removeEventListener(t, callback);
};
}
this.addEventListener<T>(type, callback);
return () => this.removeEventListener(type, callback);
}
}
+2
View File
@@ -17,4 +17,6 @@ export class NitroEventType
public static readonly AVATAR_EFFECT_DOWNLOADED = 'AVATAR_EFFECT_DOWNLOADED';
public static readonly AVATAR_EFFECT_LOADED = 'AVATAR_EFFECT_LOADED';
public static readonly FURNITURE_DATA_LOADED = 'FURNITURE_DATA_LOADED';
public static readonly SESSION_DATA_UPDATED = 'SESSION_DATA_UPDATED';
public static readonly ROOM_SESSION_UPDATED = 'ROOM_SESSION_UPDATED';
}
+46 -2
View File
@@ -1,6 +1,6 @@
import { IRoomHandlerListener, IRoomSession, IRoomSessionManager } from '@nitrots/api';
import { IRoomHandlerListener, IRoomSession, IRoomSessionManager, IRoomSessionSnapshot } from '@nitrots/api';
import { GetCommunication, RoomEnterComposer, RoomUnitWalkComposer } from '@nitrots/communication';
import { GetEventDispatcher, NitroEventType, RoomSessionEvent } from '@nitrots/events';
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';
@@ -26,6 +26,41 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
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>
{
@@ -196,6 +231,7 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
this._sessions.clear();
this._viewerSession = null;
this.invalidateRoomSessionSnapshot();
this.createSession(roomId, password, this._savedPosX, this._savedPosY);
this.clearGuardTimer();
this._reconnectGuardTimer = setTimeout(() =>
@@ -384,6 +420,8 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
this._lastRoomPassword = roomSession.password;
this.persistRoom(roomSession.roomId, roomSession.password);
this.invalidateRoomSessionSnapshot();
this.startSession(this._viewerSession);
return true;
@@ -406,6 +444,8 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
this.setHandlers(session);
this.invalidateRoomSessionSnapshot();
return true;
}
@@ -429,6 +469,10 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
}
GetEventDispatcher().dispatchEvent(new RoomSessionEvent(RoomSessionEvent.ENDED, session, openLandingView));
if(this._viewerSession === session) this._viewerSession = null;
this.invalidateRoomSessionSnapshot();
}
public sessionUpdate(id: number, type: string): void
+66 -2
View File
@@ -1,8 +1,8 @@
import { IFurnitureData, IGroupInformationManager, IMessageComposer, IMessageEvent, IProductData, ISessionDataManager, NoobnessLevelEnum, SecurityLevel } from '@nitrots/api';
import { IFurnitureData, IGroupInformationManager, IMessageComposer, IMessageEvent, IProductData, ISessionDataManager, IUserDataSnapshot, NoobnessLevelEnum, SecurityLevel } from '@nitrots/api';
import { AccountSafetyLockStatusChangeMessageEvent, AccountSafetyLockStatusChangeParser, AvailabilityStatusMessageEvent, ChangeUserNameResultMessageEvent, EmailStatusResultEvent, FigureUpdateEvent, GetCommunication, GetUserTagsComposer, InClientLinkEvent, MysteryBoxKeysEvent, NoobnessLevelMessageEvent, PetRespectComposer, PetScratchFailedMessageEvent, RoomReadyMessageEvent, RoomUnitChatComposer, UserInfoEvent, UserNameChangeMessageEvent, UserPermissionsEvent, UserRespectComposer, UserTagsMessageEvent } from '@nitrots/communication';
import { GetConfiguration } from '@nitrots/configuration';
import { GetLocalizationManager } from '@nitrots/localization';
import { GetEventDispatcher, MysteryBoxKeysUpdateEvent, NitroSettingsEvent, SessionDataPreferencesEvent, UserNameUpdateEvent } from '@nitrots/events';
import { GetEventDispatcher, MysteryBoxKeysUpdateEvent, NitroEvent, NitroEventType, NitroSettingsEvent, SessionDataPreferencesEvent, UserNameUpdateEvent } from '@nitrots/events';
import { CreateLinkEvent, HabboWebTools } from '@nitrots/utils';
import { Texture } from 'pixi.js';
import { GroupInformationManager } from './GroupInformationManager';
@@ -52,11 +52,49 @@ export class SessionDataManager implements ISessionDataManager
private _badgeImageManager: BadgeImageManager = new BadgeImageManager();
private _userDataSnapshot: Readonly<IUserDataSnapshot> | null = null;
constructor()
{
this.resetUserInfo();
}
private invalidateUserDataSnapshot(): void
{
this._userDataSnapshot = null;
GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SESSION_DATA_UPDATED));
}
public getUserDataSnapshot(): Readonly<IUserDataSnapshot>
{
if(this._userDataSnapshot) return this._userDataSnapshot;
this._userDataSnapshot = Object.freeze<IUserDataSnapshot>({
userId: this._userId,
userName: this._name,
figure: this._figure,
gender: this._gender,
realName: this._realName,
respectsReceived: this._respectsReceived,
respectsLeft: this._respectsLeft,
respectsPetLeft: this._respectsPetLeft,
canChangeName: this._canChangeName,
clubLevel: this._clubLevel,
securityLevel: this._securityLevel,
isAmbassador: this._isAmbassador,
isEmailVerified: this._isEmailVerified,
isNoob: (this._noobnessLevel !== NoobnessLevelEnum.OLD_IDENTITY),
isAuthenticHabbo: this._isAuthenticHabbo,
isSystemOpen: this._systemOpen,
isSystemShutdown: this._systemShutdown,
uiFlags: this._uiFlags,
tags: Object.freeze<string[]>([...this._tags]) as ReadonlyArray<string>
});
return this._userDataSnapshot;
}
public async init(): Promise<void>
{
await Promise.all([
@@ -75,6 +113,8 @@ export class SessionDataManager implements ISessionDataManager
this._gender = event.getParser().gender;
HabboWebTools.updateFigure(this._figure);
this.invalidateUserDataSnapshot();
})),
GetCommunication().registerMessageEvent(new UserInfoEvent(this.onUserInfoEvent.bind(this))),
GetCommunication().registerMessageEvent(new UserPermissionsEvent(this.onUserPermissionsEvent.bind(this))),
@@ -98,6 +138,8 @@ export class SessionDataManager implements ISessionDataManager
this._uiFlags = event.flags;
GetEventDispatcher().dispatchEvent(new SessionDataPreferencesEvent(this._uiFlags));
this.invalidateUserDataSnapshot();
};
GetEventDispatcher().addEventListener<NitroSettingsEvent>(NitroSettingsEvent.SETTINGS_UPDATED, this._settingsEventCallback);
@@ -189,6 +231,8 @@ export class SessionDataManager implements ISessionDataManager
this._safetyLocked = userInfo.safetyLocked;
this._ignoredUsersManager.requestIgnoredUsers(userInfo.username);
this.invalidateUserDataSnapshot();
}
private onUserPermissionsEvent(event: UserPermissionsEvent): void
@@ -198,6 +242,8 @@ export class SessionDataManager implements ISessionDataManager
this._clubLevel = event.getParser().clubLevel;
this._securityLevel = event.getParser().securityLevel;
this._isAmbassador = event.getParser().isAmbassador;
this.invalidateUserDataSnapshot();
}
private onAvailabilityStatusMessageEvent(event: AvailabilityStatusMessageEvent): void
@@ -211,6 +257,8 @@ export class SessionDataManager implements ISessionDataManager
this._systemOpen = parser.isOpen;
this._systemShutdown = parser.onShutdown;
this._isAuthenticHabbo = parser.isAuthenticUser;
this.invalidateUserDataSnapshot();
}
private onPetRespectFailed(event: PetScratchFailedMessageEvent): void
@@ -218,6 +266,8 @@ export class SessionDataManager implements ISessionDataManager
if(!event || !event.connection) return;
this._respectsPetLeft++;
this.invalidateUserDataSnapshot();
}
private onChangeNameUpdateEvent(event: ChangeUserNameResultMessageEvent): void
@@ -233,6 +283,8 @@ export class SessionDataManager implements ISessionDataManager
this._canChangeName = false;
GetEventDispatcher().dispatchEvent(new UserNameUpdateEvent(parser.name));
this.invalidateUserDataSnapshot();
}
private onUserNameChangeMessageEvent(event: UserNameChangeMessageEvent): void
@@ -249,6 +301,8 @@ export class SessionDataManager implements ISessionDataManager
this._canChangeName = false;
GetEventDispatcher().dispatchEvent(new UserNameUpdateEvent(this._name));
this.invalidateUserDataSnapshot();
}
private onUserTags(event: UserTagsMessageEvent): void
@@ -260,6 +314,8 @@ export class SessionDataManager implements ISessionDataManager
if(!parser) return;
this._tags = parser.tags;
this.invalidateUserDataSnapshot();
}
private onRoomModelNameEvent(event: RoomReadyMessageEvent): void
@@ -300,6 +356,8 @@ export class SessionDataManager implements ISessionDataManager
this._noobnessLevel = event.getParser().noobnessLevel;
if(this._noobnessLevel !== NoobnessLevelEnum.OLD_IDENTITY) GetConfiguration().setValue<number>('new.identity', 1);
this.invalidateUserDataSnapshot();
}
private onAccountSafetyLockStatusChangeMessageEvent(event: AccountSafetyLockStatusChangeMessageEvent): void
@@ -316,6 +374,8 @@ export class SessionDataManager implements ISessionDataManager
private onEmailStatus(event: EmailStatusResultEvent): void
{
this._isEmailVerified = event?.getParser()?.isVerified ?? false;
this.invalidateUserDataSnapshot();
}
public getFloorItemData(id: number): IFurnitureData
@@ -476,6 +536,8 @@ export class SessionDataManager implements ISessionDataManager
this.send(new UserRespectComposer(userId));
this._respectsLeft--;
this.invalidateUserDataSnapshot();
}
public givePetRespect(petId: number): void
@@ -485,6 +547,8 @@ export class SessionDataManager implements ISessionDataManager
this.send(new PetRespectComposer(petId));
this._respectsPetLeft--;
this.invalidateUserDataSnapshot();
}
public sendSpecialCommandMessage(text: string, styleId: number = 0): void