feat(session): snapshot getter for UserDataManager room user list

Extends the snapshot pattern to the room's user list. The React client
currently has many widgets each calling `getUserDataByIndex(idx)` in a
loop (chat, doorbell, room player list, infostand …) — every render
walks the underlying Map and rebuilds an array. With
`getRoomUserListSnapshot(): ReadonlyArray<IRoomUserData>` consumers can
memoize on the array reference and only rebuild when something actually
changed.

Invalidation fires on every state-changing path:
- updateUserData (add/replace)
- removeUserData (leave)
- updateFigure / updateName / updateMotto / updateNickIcon /
  updateCustomization / updateBackground / updateAchievementScore /
  updatePetLevel / updatePetBreedingStatus

The inner IRoomUserData objects keep their existing in-place mutation
semantics (deep-clone would be too expensive for 30+ avatars on every
single status push). Consumers should treat each entry as a
snapshot-at-time-of-read and not retain references across an
invalidation.

New event: NitroEventType.ROOM_USER_LIST_UPDATED. Interface and event
additions are backwards-compatible; no existing accessors changed.

Also tidied: `updatePetLevel` now uses the explicit `if(!userData)
return;` guard pattern matching the rest of the methods (was a one-line
inline conditional).
This commit is contained in:
simoleo89
2026-05-18 20:52:33 +02:00
parent a599e0cf89
commit 761d8ffe19
3 changed files with 62 additions and 3 deletions
@@ -23,4 +23,20 @@ export interface IUserDataManager
updatePetLevel(roomIndex: number, level: number): void;
updatePetBreedingStatus(roomIndex: number, canBreed: boolean, canHarvest: boolean, canRevive: boolean, hasBreedingPermission: boolean): void;
requestPetInfo(id: number): void;
/**
* Returns the current room's user list as a referentially-stable
* ReadonlyArray. The same array reference is returned across reads
* until any user is added, removed, or has a tracked field updated
* (figure / name / motto / nick icon / customization / background /
* achievement score / pet level / breeding status). Mutations
* dispatch `NitroEventType.ROOM_USER_LIST_UPDATED` to signal
* invalidation.
*
* The inner IRoomUserData objects keep the existing in-place
* mutation semantics — they are NOT deep-cloned. Treat them as
* snapshots-at-time-of-read; consumers should not retain individual
* entries across invalidations.
*/
getRoomUserListSnapshot(): ReadonlyArray<IRoomUserData>;
}
+1
View File
@@ -21,4 +21,5 @@ export class NitroEventType
public static readonly ROOM_SESSION_UPDATED = 'ROOM_SESSION_UPDATED';
public static readonly IGNORED_USERS_UPDATED = 'IGNORED_USERS_UPDATED';
public static readonly GROUP_BADGES_UPDATED = 'GROUP_BADGES_UPDATED';
public static readonly ROOM_USER_LIST_UPDATED = 'ROOM_USER_LIST_UPDATED';
}
+45 -3
View File
@@ -1,5 +1,6 @@
import { IRoomUserData, IUserDataManager } from '@nitrots/api';
import { GetCommunication, RequestPetInfoComposer, UserCurrentBadgesComposer } from '@nitrots/communication';
import { GetEventDispatcher, NitroEvent, NitroEventType } from '@nitrots/events';
export class UserDataManager implements IUserDataManager
{
@@ -11,6 +12,23 @@ export class UserDataManager implements IUserDataManager
private _userDataByType: Map<number, Map<number, IRoomUserData>> = new Map();
private _userDataByRoomIndex: Map<number, IRoomUserData> = new Map();
private _userBadges: Map<number, string[]> = new Map();
private _roomUserListSnapshot: ReadonlyArray<IRoomUserData> | null = null;
private invalidateRoomUserListSnapshot(): void
{
this._roomUserListSnapshot = null;
GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.ROOM_USER_LIST_UPDATED));
}
public getRoomUserListSnapshot(): ReadonlyArray<IRoomUserData>
{
if(this._roomUserListSnapshot) return this._roomUserListSnapshot;
this._roomUserListSnapshot = Object.freeze<IRoomUserData[]>([ ...this._userDataByRoomIndex.values() ]) as ReadonlyArray<IRoomUserData>;
return this._roomUserListSnapshot;
}
public getUserData(webID: number): IRoomUserData
{
@@ -84,6 +102,8 @@ export class UserDataManager implements IUserDataManager
existingType.set(data.webID, data);
this._userDataByRoomIndex.set(data.roomIndex, data);
this.invalidateRoomUserListSnapshot();
}
public removeUserData(roomIndex: number): void
@@ -97,6 +117,8 @@ export class UserDataManager implements IUserDataManager
const existingType = this._userDataByType.get(existing.type);
if(existingType) existingType.delete(existing.webID);
this.invalidateRoomUserListSnapshot();
}
public getUserBadges(userId: number): string[]
@@ -125,6 +147,8 @@ export class UserDataManager implements IUserDataManager
userData.sex = sex;
userData.hasSaddle = hasSaddle;
userData.isRiding = isRiding;
this.invalidateRoomUserListSnapshot();
}
public updateName(roomIndex: number, name: string): void
@@ -134,6 +158,8 @@ export class UserDataManager implements IUserDataManager
if(!userData) return;
userData.name = name;
this.invalidateRoomUserListSnapshot();
}
public updateMotto(roomIndex: number, custom: string): void
@@ -143,6 +169,8 @@ export class UserDataManager implements IUserDataManager
if(!userData) return;
userData.custom = custom;
this.invalidateRoomUserListSnapshot();
}
public updateNickIcon(roomIndex: number, nickIcon: string): void
@@ -152,6 +180,8 @@ export class UserDataManager implements IUserDataManager
if(!userData) return;
userData.nickIcon = nickIcon;
this.invalidateRoomUserListSnapshot();
}
public updateCustomization(roomIndex: number, nickIcon: string, prefixText: string, prefixColor: string, prefixIcon: string, prefixEffect: string, prefixFont: string, displayOrder: string): void
@@ -167,9 +197,11 @@ export class UserDataManager implements IUserDataManager
userData.prefixEffect = prefixEffect;
userData.prefixFont = prefixFont;
userData.displayOrder = displayOrder;
this.invalidateRoomUserListSnapshot();
}
public updateBackground(roomIndex: number, background: number, stand: number, overlay: number, cardBackground: number = 0): void
public updateBackground(roomIndex: number, background: number, stand: number, overlay: number, cardBackground: number = 0): void
{
const userData = this.getUserDataByIndex(roomIndex);
@@ -179,6 +211,8 @@ export class UserDataManager implements IUserDataManager
userData.stand = stand;
userData.overlay = overlay;
userData.cardBackground = cardBackground;
this.invalidateRoomUserListSnapshot();
}
public updateAchievementScore(roomIndex: number, score: number): void
@@ -188,13 +222,19 @@ export class UserDataManager implements IUserDataManager
if(!userData) return;
userData.activityPoints = score;
this.invalidateRoomUserListSnapshot();
}
public updatePetLevel(roomIndex: number, level: number): void
{
const userData = this.getUserDataByIndex(roomIndex);
if(userData) userData.petLevel = level;
if(!userData) return;
userData.petLevel = level;
this.invalidateRoomUserListSnapshot();
}
public updatePetBreedingStatus(roomIndex: number, canBreed: boolean, canHarvest: boolean, canRevive: boolean, hasBreedingPermission: boolean): void
@@ -207,6 +247,8 @@ export class UserDataManager implements IUserDataManager
userData.canHarvest = canHarvest;
userData.canRevive = canRevive;
userData.hasBreedingPermission = hasBreedingPermission;
this.invalidateRoomUserListSnapshot();
}
public requestPetInfo(id: number): void