From 87cf47847c76fb809e61bffe1acf5444b43df084 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 10 May 2026 19:16:32 +0200 Subject: [PATCH] feat(events,session): add React-friendly subscribe APIs and snapshot getters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- package.json | 2 +- packages/api/src/common/IEventDispatcher.ts | 1 + .../communication/ICommunicationManager.ts | 1 + .../src/nitro/session/IRoomSessionManager.ts | 2 + .../src/nitro/session/IRoomSessionSnapshot.ts | 18 +++++ .../src/nitro/session/ISessionDataManager.ts | 2 + .../src/nitro/session/IUserDataSnapshot.ts | 22 ++++++ packages/api/src/nitro/session/index.ts | 2 + .../communication/src/CommunicationManager.ts | 11 +++ packages/events/src/EventDispatcher.ts | 19 ++++++ packages/events/src/NitroEventType.ts | 2 + packages/session/src/RoomSessionManager.ts | 48 ++++++++++++- packages/session/src/SessionDataManager.ts | 68 ++++++++++++++++++- 13 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 packages/api/src/nitro/session/IRoomSessionSnapshot.ts create mode 100644 packages/api/src/nitro/session/IUserDataSnapshot.ts diff --git a/package.json b/package.json index ff1499d..ff24624 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@nitrots/nitro-renderer", "description": "Javascript library for rendering Nitro in the browser using PixiJS", - "version": "2.0.0", + "version": "2.1.0", "private": true, "type": "module", "workspaces": [ diff --git a/packages/api/src/common/IEventDispatcher.ts b/packages/api/src/common/IEventDispatcher.ts index 2e4ebdb..9217b0c 100644 --- a/packages/api/src/common/IEventDispatcher.ts +++ b/packages/api/src/common/IEventDispatcher.ts @@ -7,4 +7,5 @@ export interface IEventDispatcher removeEventListener(type: string, callback: Function): void; removeAllListeners(): void; dispatchEvent(event: T): boolean; + subscribe(type: string | string[], callback: (event: T) => void): () => void; } diff --git a/packages/api/src/communication/ICommunicationManager.ts b/packages/api/src/communication/ICommunicationManager.ts index 7c15877..76e17e0 100644 --- a/packages/api/src/communication/ICommunicationManager.ts +++ b/packages/api/src/communication/ICommunicationManager.ts @@ -6,5 +6,6 @@ export interface ICommunicationManager init(): Promise; registerMessageEvent(event: IMessageEvent): IMessageEvent; removeMessageEvent(event: IMessageEvent): void; + subscribeMessage(eventCtor: new (callback: (event: T) => void) => T, handler: (event: T) => void): () => void; connection: IConnection; } diff --git a/packages/api/src/nitro/session/IRoomSessionManager.ts b/packages/api/src/nitro/session/IRoomSessionManager.ts index 974c98a..777345e 100644 --- a/packages/api/src/nitro/session/IRoomSessionManager.ts +++ b/packages/api/src/nitro/session/IRoomSessionManager.ts @@ -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 | null; viewerSession: IRoomSession; } diff --git a/packages/api/src/nitro/session/IRoomSessionSnapshot.ts b/packages/api/src/nitro/session/IRoomSessionSnapshot.ts new file mode 100644 index 0000000..5b1b7e2 --- /dev/null +++ b/packages/api/src/nitro/session/IRoomSessionSnapshot.ts @@ -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; +} diff --git a/packages/api/src/nitro/session/ISessionDataManager.ts b/packages/api/src/nitro/session/ISessionDataManager.ts index 26947e6..59b0d97 100644 --- a/packages/api/src/nitro/session/ISessionDataManager.ts +++ b/packages/api/src/nitro/session/ISessionDataManager.ts @@ -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; } diff --git a/packages/api/src/nitro/session/IUserDataSnapshot.ts b/packages/api/src/nitro/session/IUserDataSnapshot.ts new file mode 100644 index 0000000..84c971f --- /dev/null +++ b/packages/api/src/nitro/session/IUserDataSnapshot.ts @@ -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; +} diff --git a/packages/api/src/nitro/session/index.ts b/packages/api/src/nitro/session/index.ts index 1fe774d..c93302b 100644 --- a/packages/api/src/nitro/session/index.ts +++ b/packages/api/src/nitro/session/index.ts @@ -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'; diff --git a/packages/communication/src/CommunicationManager.ts b/packages/communication/src/CommunicationManager.ts index a9d2cad..c783183 100644 --- a/packages/communication/src/CommunicationManager.ts +++ b/packages/communication/src/CommunicationManager.ts @@ -203,6 +203,17 @@ export class CommunicationManager implements ICommunicationManager this._connection.removeMessageEvent(event); } + public subscribeMessage(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; diff --git a/packages/events/src/EventDispatcher.ts b/packages/events/src/EventDispatcher.ts index f95aa3c..5c21b00 100644 --- a/packages/events/src/EventDispatcher.ts +++ b/packages/events/src/EventDispatcher.ts @@ -101,4 +101,23 @@ export class EventDispatcher implements IEventDispatcher { this._listeners.clear(); } + + public subscribe(type: string | string[], callback: (event: T) => void): () => void + { + if(!type || !callback) return () => {}; + + if(Array.isArray(type)) + { + for(const t of type) this.addEventListener(t, callback); + + return () => + { + for(const t of type) this.removeEventListener(t, callback); + }; + } + + this.addEventListener(type, callback); + + return () => this.removeEventListener(type, callback); + } } diff --git a/packages/events/src/NitroEventType.ts b/packages/events/src/NitroEventType.ts index 9e575ac..6cfd367 100644 --- a/packages/events/src/NitroEventType.ts +++ b/packages/events/src/NitroEventType.ts @@ -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'; } diff --git a/packages/session/src/RoomSessionManager.ts b/packages/session/src/RoomSessionManager.ts index 0ffd0eb..52b206f 100644 --- a/packages/session/src/RoomSessionManager.ts +++ b/packages/session/src/RoomSessionManager.ts @@ -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 = null; private _savedPosX: number = -1; private _savedPosY: number = -1; + private _activeRoomSessionSnapshot: Readonly | null = null; + + private invalidateRoomSessionSnapshot(): void + { + this._activeRoomSessionSnapshot = null; + + GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.ROOM_SESSION_UPDATED)); + } + + public getActiveRoomSessionSnapshot(): Readonly | null + { + const session = this._viewerSession; + + if(!session) return null; + + if(this._activeRoomSessionSnapshot && this._activeRoomSessionSnapshot.session === session) return this._activeRoomSessionSnapshot; + + this._activeRoomSessionSnapshot = Object.freeze({ + 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 { @@ -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 diff --git a/packages/session/src/SessionDataManager.ts b/packages/session/src/SessionDataManager.ts index 9d3c07f..c92c52a 100644 --- a/packages/session/src/SessionDataManager.ts +++ b/packages/session/src/SessionDataManager.ts @@ -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 | null = null; + constructor() { this.resetUserInfo(); } + private invalidateUserDataSnapshot(): void + { + this._userDataSnapshot = null; + + GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SESSION_DATA_UPDATED)); + } + + public getUserDataSnapshot(): Readonly + { + if(this._userDataSnapshot) return this._userDataSnapshot; + + this._userDataSnapshot = Object.freeze({ + 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([...this._tags]) as ReadonlyArray + }); + + return this._userDataSnapshot; + } + public async init(): Promise { 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.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('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