mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
feat(hooks/session): React-side consumer hooks for the renderer snapshot pattern
The renderer exposes six referentially-stable snapshot getters under the v2.1.0 React-friendly pattern (SessionData / RoomSession / IgnoredUsers / GroupBadges / RoomUserList / SoundVolumes), each invalidated by a dedicated NitroEventType.*_UPDATED dispatch. Until now nothing on the client consumed them — useExternalSnapshot existed as a useSyncExternalStore wrapper but no widget was wired up to a snapshot. Add thin consumer hooks under src/hooks/session/useSessionSnapshots.ts, each a useExternalSnapshot wrapper around the matching subscribe+getter pair: - useUserDataSnapshot() → Readonly<IUserDataSnapshot> - useActiveRoomSessionSnapshot() → Readonly<IRoomSessionSnapshot> | null - useIgnoredUsersSnapshot() → ReadonlyArray<string> - useIsUserIgnored(name) → boolean (useMemo over the array) - useGroupBadgesSnapshot() → ReadonlyMap<number, string> - useGroupBadge(groupId) → string (useMemo over the map) - useVolumesSnapshot() → Readonly<ISoundVolumesSnapshot> - useRoomUserListSnapshot() → ReadonlyArray<IRoomUserData> Two design details worth noting: - useRoomUserListSnapshot subscribes to BOTH ROOM_USER_LIST_UPDATED (for join/leave/update inside a session) AND ROOM_SESSION_UPDATED (because the underlying userDataManager reference flips when the active room session changes). A single module-level frozen EMPTY_USER_LIST is the fallback when no session is active, keeping reference stability across reads in the no-room state. - useIsUserIgnored / useGroupBadge memoize the scalar derivation so a re-render only happens when the underlying snapshot reference flips, not on unrelated useExternalSnapshot wake-ups. These hooks unlock per-component snapshot consumption — widgets that previously juggled addEventListener + useState pairs (or worse, read GetSessionDataManager().userId directly and never re-rendered) can now go through one of these and get reactivity for free. Migration of existing consumers (useSessionInfo, AvatarInfoUtilities, etc.) is the next pass. Verification: yarn typecheck clean, yarn test 203/203, yarn build green.
This commit is contained in:
@@ -1 +1,2 @@
|
||||
export * from './useSessionInfo';
|
||||
export * from './useSessionSnapshots';
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { GetEventDispatcher, GetRoomSessionManager, GetSessionDataManager, GetSoundManager, IRoomSessionSnapshot, IRoomUserData, ISoundVolumesSnapshot, IUserDataSnapshot, NitroEventType } from '@nitrots/nitro-renderer';
|
||||
import { useMemo } from 'react';
|
||||
import { useExternalSnapshot } from '../events/useExternalSnapshot';
|
||||
|
||||
/**
|
||||
* React-side consumers for the referentially-stable snapshot getters
|
||||
* the renderer exposes (Nitro_Render_V3 v2.1.0+ pattern).
|
||||
*
|
||||
* Every hook here is a thin `useSyncExternalStore` wrapper: it subscribes
|
||||
* to the corresponding `NitroEventType.*_UPDATED` invalidation event and
|
||||
* reads the matching `getXxxSnapshot()`. Because the renderer guarantees
|
||||
* snapshot reference invariance until invalidation, React's bailout logic
|
||||
* skips re-renders when the snapshot is unchanged — so widgets that read
|
||||
* the same slice across many components share a single subscription and
|
||||
* only re-paint when the underlying state actually changes.
|
||||
*
|
||||
* Prefer these over reaching into the manager directly with
|
||||
* `GetSessionDataManager().userId` etc., which never trigger a re-render
|
||||
* when the value changes.
|
||||
*/
|
||||
|
||||
const subscribeTo = (eventType: string) => (onChange: () => void) =>
|
||||
GetEventDispatcher().subscribe(eventType, onChange);
|
||||
|
||||
export const useUserDataSnapshot = (): Readonly<IUserDataSnapshot> =>
|
||||
useExternalSnapshot(
|
||||
subscribeTo(NitroEventType.SESSION_DATA_UPDATED),
|
||||
() => GetSessionDataManager().getUserDataSnapshot()
|
||||
);
|
||||
|
||||
export const useActiveRoomSessionSnapshot = (): Readonly<IRoomSessionSnapshot> | null =>
|
||||
useExternalSnapshot(
|
||||
subscribeTo(NitroEventType.ROOM_SESSION_UPDATED),
|
||||
() => GetRoomSessionManager().getActiveRoomSessionSnapshot()
|
||||
);
|
||||
|
||||
export const useIgnoredUsersSnapshot = (): ReadonlyArray<string> =>
|
||||
useExternalSnapshot(
|
||||
subscribeTo(NitroEventType.IGNORED_USERS_UPDATED),
|
||||
() => GetSessionDataManager().ignoredUsersManager.getIgnoredUsersSnapshot()
|
||||
);
|
||||
|
||||
/**
|
||||
* Reactive predicate built on top of `useIgnoredUsersSnapshot`.
|
||||
* Re-renders only when the array reference flips (i.e. someone is added
|
||||
* or removed) — not on unrelated session updates.
|
||||
*/
|
||||
export const useIsUserIgnored = (name: string): boolean =>
|
||||
{
|
||||
const list = useIgnoredUsersSnapshot();
|
||||
|
||||
return useMemo(() => list.includes(name), [ list, name ]);
|
||||
};
|
||||
|
||||
export const useGroupBadgesSnapshot = (): ReadonlyMap<number, string> =>
|
||||
useExternalSnapshot(
|
||||
subscribeTo(NitroEventType.GROUP_BADGES_UPDATED),
|
||||
() => GetSessionDataManager().groupInformationManager.getGroupBadgesSnapshot()
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the badge id for a given group, reactive. Empty string when
|
||||
* the badge isn't known (matches the legacy `getGroupBadge` fallback).
|
||||
*/
|
||||
export const useGroupBadge = (groupId: number): string =>
|
||||
{
|
||||
const badges = useGroupBadgesSnapshot();
|
||||
|
||||
return useMemo(() => badges.get(groupId) ?? '', [ badges, groupId ]);
|
||||
};
|
||||
|
||||
export const useVolumesSnapshot = (): Readonly<ISoundVolumesSnapshot> =>
|
||||
useExternalSnapshot(
|
||||
subscribeTo(NitroEventType.SOUND_VOLUMES_UPDATED),
|
||||
() => GetSoundManager().getVolumesSnapshot()
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the active room's user list, reactive. Returns an empty
|
||||
* frozen array when no room session is active (matches the renderer's
|
||||
* "no active session" shape).
|
||||
*
|
||||
* The room session itself is read via the active-room snapshot —
|
||||
* `ROOM_USER_LIST_UPDATED` fires on user join/leave/update inside the
|
||||
* active session, but the underlying `userDataManager` reference
|
||||
* follows whichever session is current, so we re-resolve it on every
|
||||
* snapshot read. The empty-array fallback is also frozen so consumers
|
||||
* relying on referential stability don't accidentally trigger renders
|
||||
* by getting a fresh `[]` each call when no session is active.
|
||||
*/
|
||||
const EMPTY_USER_LIST = Object.freeze<IRoomUserData[]>([]) as ReadonlyArray<IRoomUserData>;
|
||||
|
||||
export const useRoomUserListSnapshot = (): ReadonlyArray<IRoomUserData> =>
|
||||
useExternalSnapshot(
|
||||
// Subscribe to BOTH events: ROOM_USER_LIST_UPDATED fires for
|
||||
// join/leave/update inside the active session, but
|
||||
// ROOM_SESSION_UPDATED fires when the active session itself
|
||||
// changes (room change) — and the underlying `userDataManager`
|
||||
// reference flips with it, so we need to re-read.
|
||||
(onChange) =>
|
||||
{
|
||||
const dispatcher = GetEventDispatcher();
|
||||
const offList = dispatcher.subscribe(NitroEventType.ROOM_USER_LIST_UPDATED, onChange);
|
||||
const offSession = dispatcher.subscribe(NitroEventType.ROOM_SESSION_UPDATED, onChange);
|
||||
|
||||
return () =>
|
||||
{
|
||||
offList();
|
||||
offSession();
|
||||
};
|
||||
},
|
||||
() => GetRoomSessionManager().getActiveRoomSessionSnapshot()?.session?.userDataManager?.getRoomUserListSnapshot() ?? EMPTY_USER_LIST
|
||||
);
|
||||
Reference in New Issue
Block a user