diff --git a/CLAUDE.md b/CLAUDE.md index bd25cd7..e88b214 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -127,12 +127,12 @@ canonical pattern. ## Patterns to use -### `useSessionSnapshots` (renderer snapshot pattern, React-side) +### `useSessionSnapshots` (renderer snapshot pattern, React-side — OPT-IN) For state that lives on a renderer Manager and is invalidated through -`NitroEventType.*_UPDATED`, prefer the snapshot consumer hooks in -`src/hooks/session/useSessionSnapshots.ts` over `useState + -useMessageEvent` mirrors: +`NitroEventType.*_UPDATED`, the file +`src/hooks/session/useSessionSnapshots.ts` exposes eight consumer hooks +backed by `useSyncExternalStore`: ```ts const userData = useUserDataSnapshot(); // SessionData @@ -145,12 +145,21 @@ const vols = useVolumesSnapshot(); // sound volumes const users = useRoomUserListSnapshot(); // ReadonlyArray ``` -Each is a thin `useSyncExternalStore` wrapper around the renderer's -matching `getXxxSnapshot()` + subscription to the matching event. -Snapshot references are renderer-guaranteed stable until invalidation -— React bails out cleanly when nothing changed. Pilot adopters: -`useSessionInfo` (userFigure / respects), `AvatarInfoWidgetAvatarView` -(reactive Ignore/Unignore menu entry). +Each hook has defensive `typeof method === 'function'` guards against +a stale renderer bundle and degrades to a frozen default snapshot if +the renderer doesn't expose the matching getter (kept module-level so +React's bailout still works on the degraded path). + +**Adoption status: zero in-tree consumers.** The first three pilot +migrations (`useSessionInfo`, `useChatWidget.ownUserId`, +`AvatarInfoWidgetAvatarView` Ignore/Unignore) were rolled back in +`e142efd` after a persistent runtime error +`(intermediate value)() is undefined` at `ToolbarView.tsx:46` that the +vite-alias fix (`790ad2b`) and the defensive guards (`c35a2d4`) could +not eliminate. The hooks remain available for any future opt-in +consumer, but **do not migrate `useBetween`-shared consumers to them +without isolated testing first** — that combination is the suspected +cause of the bug. ### `useNitroEventState` / `useMessageEventState` @@ -285,7 +294,7 @@ into `configurePreviewServer` so `yarn preview` keeps working. | Adopted | Pilot sites | |---|---| -| Renderer snapshot consumer hooks (`useSessionSnapshots`) | `useSessionInfo` (userFigure / respectsLeft / respectsPetLeft via `useUserDataSnapshot`), `AvatarInfoWidgetAvatarView` (reactive Ignore/Unignore via `useIsUserIgnored`). 8 hooks total available; consumers can read userData / activeRoomSession / ignoredUsers / groupBadges / soundVolumes / roomUserList reactively | +| Renderer snapshot consumer hooks (`useSessionSnapshots`) | **No in-tree consumers** — three pilot migrations rolled back in `e142efd` due to a runtime `(intermediate value)() is undefined` at `ToolbarView.tsx:46` that survived both the vite-alias fix and defensive guards. The 8 hooks (userData / activeRoomSession / ignoredUsers / groupBadges / soundVolumes / roomUserList / isUserIgnored / groupBadge) remain available as opt-in API with frozen-default fallbacks. | | `useNitroEventState` + companions (Reducer, ExternalSnapshot) | `OfferView`, `useAvatarInfoWidget` (figure/badges/group reducer), `useInventoryFurni` (pure reducers + fragments useRef) | | `useNitroQuery` + `useNitroEventInvalidator` | `OfferView`, `CatalogLayoutRoomAdsView`, `ModToolsChatlogView`, `CfhChatlogView`, `useGiftConfiguration`, `useUserGroups`, `useClubOffers(windowId)`, `useSellablePetPalette(breed)`, `useMarketplaceConfiguration`, `useClubGifts` (with invalidator) | | Zustand | `NavigatorRoomCreatorView` (`useRoomCreatorStore`), `WiredCreatorToolsView` (`useWiredCreatorToolsUiStore` — every panel-lifecycle-relevant flag, snapshot, selection, highlight, inline editor, picker chain hoisted; what's left in the component as `useState` is genuinely transient: keepSelected, globalClock, roomEnteredAt, selectedMonitorErrorType, selectedMonitorLogDetails) | @@ -298,9 +307,9 @@ into `configurePreviewServer` so `yarn preview` keeps working. | Not yet | Notes | |---|---| -| Split `useChatWidget` / `useAvatarInfoWidget` (data/actions) | Both state-driven via events with no clean imperative actions to extract — split still skip-motivated, but each got a targeted tidy in 2026-05-18 (useChatWidget: reactive `ownUserId` via `useUserDataSnapshot`; useAvatarInfoWidget: typed `__nitroAvatarClickControl` accessor + module-scope DEBOUNCE const). Further work would have to be feature-driven. | +| Split `useChatWidget` / `useAvatarInfoWidget` (data/actions) | Both state-driven via events with no clean imperative actions to extract — split still skip-motivated, but `useAvatarInfoWidget` got a typed `__nitroAvatarClickControl` accessor + module-scope DEBOUNCE const in 2026-05-18 (commit `05ff7df`). The `useChatWidget` reactive-`ownUserId` migration in the same commit was rolled back in `e142efd`; the hook is back on `GetSessionDataManager()?.userId` (static at mount). | | Split `usePetPackageWidget` / `useWordQuizWidget` / `useChatCommandSelector` (data/actions) | Data/actions split remains a bad fit, but all three got real modernization in 2026-05-18 instead: usePetPackageWidget → useReducer + extracted `getPetPackageNameError` pure helper + 4 tests; useWordQuizWidget → fixed stale-closure bug in `setUserAnswers` updater + `useRef` for the timeout handle; useChatCommandSelector → module-level `let` cache replaced with a Zustand store. | -| Migrate remaining `useSessionInfo`-style mirrors to renderer snapshots | Pilot done on `useSessionInfo` + `AvatarInfoWidgetAvatarView` + `useChatWidget.ownUserId`. Other candidates: any place that reads `GetSessionDataManager().userId/figure/clubLevel/isModerator` etc. directly in render and never re-renders on session changes. Each is a small migration; no need to bundle. | +| Migrate any consumer to renderer snapshot hooks | **Blocked.** Three pilot migrations were rolled back in `e142efd` after the `useSessionInfo` migration triggered a persistent runtime error at `ToolbarView.tsx:46`. The defensive guards in `useSessionSnapshots.ts` and the umbrella vite alias (`790ad2b`) are still in place. Before retrying, isolate the cause: the suspected interaction is `useBetween` + `useSyncExternalStore` + React Compiler. Try a NON-`useBetween` consumer first (e.g. a fresh per-component hook usage in a low-blast-radius widget). | | Widen the component / hook test coverage | Mock layer is in place (`src/nitro-renderer.mock.ts`) and 3+ hook/component pilots pass. Good follow-up targets: `LoginView` Form Actions happy/error paths, `OfferView` with `useNitroQuery`. (Acceptable only as a side-effect of a real change — coverage growth on its own is deprioritized per session feedback.) | ## Known open logic bugs diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index fccb1be..637c273 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -103,7 +103,21 @@ information when forced into a single selector. `useSyncExternalStore` wrapper pairing the renderer's `EventDispatcher.subscribe()` with the `getXxxSnapshot()` getters added in renderer 2.1.0. Use this for readonly views over manager - state (`getUserDataSnapshot`, `getActiveRoomSessionSnapshot`). + state. Eight pre-built consumers live in + `src/hooks/session/useSessionSnapshots.ts` (userData / activeRoomSession + / ignoredUsers / groupBadges / soundVolumes / roomUserList + scalar + derivations `useIsUserIgnored`, `useGroupBadge`), each with defensive + `typeof` guards against a stale renderer bundle. + + **Note (2026-05-18):** the first three pilot migrations (`useSessionInfo`, + `useChatWidget.ownUserId`, `AvatarInfoWidgetAvatarView` Ignore-menu) + were rolled back in `e142efd` after a persistent runtime error + `(intermediate value)() is undefined` at `ToolbarView.tsx:46` that + the vite-alias fix (`790ad2b`) and defensive guards (`c35a2d4`) could + not eliminate. Suspected interaction: `useBetween` + + `useSyncExternalStore` + React Compiler. Before retrying any + migration here, exercise the snapshot hooks from a non-`useBetween` + consumer in a low-blast-radius widget first to isolate the cause. For state owned outside the listener (the `useState` + `setState(prev => applyX(prev, event))` pattern), keep using `useNitroEvent` /