From 28c552f6f82a4d3d689e470a83f9ec28339c3877 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 18 May 2026 20:58:25 +0200 Subject: [PATCH] docs(CLAUDE.md): document new snapshot getters + flat bytesAvailable pattern Add the four 2026-05-18 snapshot additions (IgnoredUsers, GroupInformation, UserDataManager room list, SoundManager volumes) to the snapshot-getter table with their invalidation events, plus the 3-step checklist for adding new ones. Also document the flat bytesAvailable early-return pattern as the canonical shape for optional-trailing-field parsers (replaces the brittle nested if-chain). Note the SoundManager volume-diff bug fix landed alongside. --- CLAUDE.md | 73 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index cfca37b..1a727f1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,26 +58,35 @@ unsubscriber, no need to juggle callback identity. Implemented in Equivalent for packet streams. Implemented in `packages/communication/src/CommunicationManager.ts`. -### Snapshot getters on `SessionDataManager` + `RoomSessionManager` +### Snapshot getters (referentially stable, lazy-frozen, invalidated on mutation) -```ts -getUserDataSnapshot(): Readonly -getActiveRoomSessionSnapshot(): Readonly | null -``` +Pattern: `getXxxSnapshot()` returns a frozen value cached internally; +mutators call `invalidateXxxSnapshot()` which drops the cache AND +dispatches an invalidation event. The React side reads via +`useSyncExternalStore`. -Returns **referentially-stable** values: the same object reference is -returned across reads until invalidated. Invalidation happens via the -new event types `NitroEventType.SESSION_DATA_UPDATED` and -`NitroEventType.ROOM_SESSION_UPDATED`. +| Manager | Getter | Invalidation event | +|---|---|---| +| `SessionDataManager` | `getUserDataSnapshot(): Readonly` | `SESSION_DATA_UPDATED` | +| `RoomSessionManager` | `getActiveRoomSessionSnapshot(): Readonly \| null` | `ROOM_SESSION_UPDATED` | +| `IgnoredUsersManager` | `getIgnoredUsersSnapshot(): ReadonlyArray` | `IGNORED_USERS_UPDATED` | +| `GroupInformationManager` | `getGroupBadgesSnapshot(): ReadonlyMap` | `GROUP_BADGES_UPDATED` (only on real changes — no-op refresh stays quiet) | +| `UserDataManager` | `getRoomUserListSnapshot(): ReadonlyArray` | `ROOM_USER_LIST_UPDATED` (inner IRoomUserData kept mutable — don't deep-clone) | +| `SoundManager` | `getVolumesSnapshot(): Readonly` | `SOUND_VOLUMES_UPDATED` (only when a volume actually changes) | -When you mutate any field that the snapshot exposes, call the private -`invalidateUserDataSnapshot()` / `invalidateRoomSessionSnapshot()` — -that drops the cached snapshot and dispatches the invalidation event. -The React side rebuilds via `useSyncExternalStore`. +Snapshot interface contracts live under `packages/api/src/nitro/session/` +and `packages/api/src/nitro/sound/`. When adding a new snapshot, the +checklist is: +1. Define the `Ixxx Snapshot` interface in `packages/api/src/nitro/...` + and export it from the matching `index.ts`. +2. Add a `XXX_UPDATED` member to `packages/events/src/NitroEventType.ts`. +3. Add `getXxxSnapshot()` to the interface AND impl; cache + invalidate + on every mutation path (don't forget batch operations like queue + truncation — invalidate AFTER the full batch, not mid-way). -The interface contracts live in: -- `packages/api/src/nitro/session/IUserDataSnapshot.ts` -- `packages/api/src/nitro/session/IRoomSessionSnapshot.ts` +Adding snapshots here is the preferred way to unblock new React +widgets — prefer it over exposing raw event-listener APIs on the +client side. ## Recent renderer changes (`feat/react19-event-bus`) @@ -139,6 +148,38 @@ unchanged. `WiredUserVariablesRequestComposer`) annotate the return type `(): []` explicitly so `IMessageComposer<[]>` lines up. +### Optional-trailing-field parsers: flat early-return chain + +Parsers that read "one tier of optional trailing fields per emulator +release" (UserProfileParser, GetGuestRoomResultMessageParser, +RoomSettingsDataParser, ModeratorUserInfoData, UserSubscriptionParser +…) all use a flat chain: + +```ts +if(!wrapper.bytesAvailable) return true; +// block N reads +if(!wrapper.bytesAvailable) return true; +// block N+1 reads +… +``` + +Defaults come from `flush()`. When the next emulator release ships a +new trailing block, append `if(!wrapper.bytesAvailable) return true;` ++ the new reads. Do NOT nest with `if(wrapper.bytesAvailable) { … }` +— the nested form re-indents the whole chain on every new tier and +is the historical source of brittle reads. + +### Bug fix: `SoundManager` volume diff comparison + +`onEvent(SETTINGS_UPDATED)` cached `volumeFurniUpdated` / +`volumeTraxUpdated` by comparing `castedEvent.volumeFurni` (percent, +e.g. 75) against `this._volumeFurni` (fraction, e.g. 0.75) — so the +change check almost always reported "updated" for a real settings push +and only reported "unchanged" if the percent matched the fraction by +coincidence (0 / 100 only). Fixed: divide first, compare divided +values, then write. Also tracks `volumeSystemUpdated` for the new +`SOUND_VOLUMES_UPDATED` snapshot invalidation. + ### Bug fix: `PetBreedingMessageParser.bytesAvailable < 12` `bytesAvailable` is a boolean (the wrapper just answers "is there