diff --git a/CHANGELOG.md b/CHANGELOG.md index d3e6978..9b62d2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,126 @@ # Changelog +## React 19 Modernization Phase 2 (2026-05-12) + +Long-running work on the `feat/react19-modernization` branch — see +[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the design rationale. +Companion changes shipped on `feat/react19-event-bus` in +[`Nitro_Render_V3`](../Nitro_Render_V3) — see that repo's CLAUDE.md +for the renderer-side notes. + +### Pattern #1: `useNitroEventState` + companions +- New `useNitroEventReducer` / `useMessageEventReducer` for the case + where multiple event types collapse into one owned state slice. +- New `useExternalSnapshot` — typed wrapper of + `useSyncExternalStore` pairing the renderer's + `EventDispatcher.subscribe()` with `getXxxSnapshot()` getters. +- Pilot adoption: `useAvatarInfoWidget` now owns the figure / badges / + group merge (three event listeners moved out of + `InfoStandWidgetUserView`, three `CloneObject` calls dropped). + Reducers extracted to `src/hooks/rooms/widgets/avatarInfo.reducers.ts` + with 14 Vitest cases. +- `useInventoryFurni` refactored to call three pure reducers + (`useInventoryFurni.reducers.ts`) instead of inlining ~250 LOC of + merge logic in the event handlers. Module-level + `furniMsgFragments` becomes a `useRef` — eliminates a latent bug + where two simultaneous client instances would have trampled each + other's fragment buffers. Empty `FurniturePostItPlacedEvent` listener + dropped. + +### Pattern #2: `useNitroQuery` adoption +- New `useNitroEventInvalidator(eventType, queryKey, accept?)` companion + in `src/api/nitro-query/` — invalidates a query slot every time the + renderer pushes the matching parser event. Required when the server + refreshes data outside the request cycle (e.g. ClubGiftInfoEvent + after a gift claim). +- Seven catalog fetches lifted out of `useCatalog` into dedicated + TanStack queries: + - `useGiftConfiguration` (GiftWrappingConfigurationEvent) + - `useUserGroups` — consolidates 5 sites that each dispatched + `CatalogGroupsComposer` independently + - `useClubOffers(windowId)` — per-windowId, with `accept` filter + - `useSellablePetPalette(breed)` — per-breed, with `accept` filter + - `useMarketplaceConfiguration` — lifted out of a self-fetch in + `MarketplacePostOfferView` + - `useClubGifts` — paired with `useNitroEventInvalidator` for the + server-push-after-SelectClubGift case +- `ICatalogOptions` (the "catalogOptions" bag that the various views + stuffed their fetched data into) is now **empty and deleted**. + +### Pattern #4: god-hook splits +Five new splits in this round, two patterns. The doorbell-style +(state + actions + shim, no shared singleton) for hooks whose actions +are pure-dispatch: + +- **chat-input** (334 LOC → 3 files) — `useChatInputState` owns the + 5 state slices + 3 event listeners + 3 lifecycle effects; + `useChatInputActions` owns `sendChat` with the full slash-command + repertoire and the outgoing-translation pipeline. Single consumer + (`ChatInputView`) keeps the original tuple via the shim. + +The `useBetween` singleton-filter style for hooks where actions +mutate shared state: + +- **wired-tools** (618 LOC) — 20 consumers; `useWiredToolsStore` + internal singleton, public `useWiredToolsState` / + `useWiredToolsActions` filter views, `useWiredTools` shim. +- **translation** (600 LOC) — 6 consumers; `useTranslationStore` + inline + filter views. +- **notification** (493 LOC) — ~44 consumers, most of which use a + single action (`simpleAlert` or `showConfirm`); the read-only state + slice exposes the three queue arrays for the renderer view layer. +- **friends** (258 LOC) — 16 consumers; state slice covers the friend + list / settings / derived online-offline split, actions slice covers + `requestFriend` / `requestResponse` / `followFriend` / + `updateRelationship`. + +Documented skip-motivated splits: `useChatWidget`, +`useChatCommandSelector`, `useFurniturePresentWidget`, +`useAvatarInfoWidget`, `useNavigator`, `useMessenger`, +`usePetPackageWidget`, `useWordQuizWidget`. Reasons logged in commit +messages. + +### Typecheck / Pixi v8 / Arcturus alignment +- Repository-wide `tsgo` (TS 7 preview) error count: **134 → 0** client, + **24 → 0** renderer. Notable clusters: framer-motion `Variants` + typing on Toolbar + FriendsBar (-33), `useFurniChooserState` + retyped as `IRoomObject` + dead `getUserData` guard dropped (-10), + React 19 `useRef()` → `useRef(null)` sweep on 15 sites (-15), + `IGetImageListener` single-arg signature migration on 3 sites, + `ColorVariantType` extended with the 5 `outline-*` bootstrap + variants. +- Renderer-side aligned with Pixi v8 (Filter[] narrowing, + WebGLRenderer narrowing, ImageLike cast) and TS 5.7+ ArrayBuffer + drift (BinaryReader / BinaryWriter / WsSessionCrypto / NitroBundle). +- Cross-repo additions on `Nitro_Render_V3`: + `RoomEnterComposer` now accepts optional `spawnX`/`spawnY` matching + Arcturus' `RequestRoomLoadEvent` optional tail; `RoomSettingsData` + surfaces the `allowUnderpass` field that Arcturus already emits. + Dead `sendWhisperGroupMessage` / `ChatWhisperGroupComposer` + reference removed. + +### Vitest coverage +Bumped from 65 → 113 cases across 8 test files. New coverage: +- `dedupeBadges.test.ts` (6) — slot-preserving badge dedup. +- `catalog-favorites.helpers.test.ts` (16) — v2→v3 localStorage + migration + per-catalog-type storage-key routing. +- `avatar-info-reducers.test.ts` (14) — three reducer bail-out + branches + apply paths. +- `friendly-time.test.ts` (12) — `FriendlyTime` with a deterministic + `LocalizeText` mock. + +### Logic bug fixes (in scope) +- `useInventoryFurni`'s module-level `furniMsgFragments` buffer + scoped to `useRef`. +- `RoomChatHandler.dispatchEvent(RoomSessionChatEvent)` arg order + fix in renderer — `chatColours` and `links` slots were swapped. +- `PetBreedingMessageParser.bytesAvailable < 12` was a boolean-vs-number + bug; replaced with the standard guard pattern. +- `useOnClickChat` was passing an extra 8th arg to `showConfirm` + (signature only takes 7). +- `UserContainerView` was passing `userProfile.friendsCount` (number) + to a `LocalizeText` placeholder array (expects string). + ## Badge System Rework (2026-04-04) ### Bug Fixes diff --git a/CLAUDE.md b/CLAUDE.md index 1475060..9e3f830 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -109,8 +109,46 @@ const { data } = useNitroQuery({ ``` Already wired up; `QueryClientProvider` is mounted in `src/index.tsx`. -Adopted on `OfferView`, `CatalogLayoutRoomAdsView`, `ModToolsChatlogView`, -`CfhChatlogView`. + +Companion `useNitroEventInvalidator(eventType, queryKey, accept?)` — +import from `src/api/nitro-query`. Subscribes to the renderer event +and invalidates the query slot on every push, so server-driven +refresh paths work the same as the initial request/response (e.g. +ClubGiftInfoEvent firing again after the user claims a gift). + +### Singleton-filter split for `useBetween`-based hooks + +When a hook backs many consumers but most only need either state OR +actions (not both), split it without breaking the shared-singleton +guarantee: + +```ts +// internal: state + actions in one closure +const useFooStore = () => { + const [ data, setData ] = useState(...); + // listeners, effects, actions ... + return { data, doThing }; +}; + +// public: read-only filter +export const useFooState = () => { + const { data } = useBetween(useFooStore); + return { data }; +}; + +// public: imperative filter +export const useFooActions = () => { + const { doThing } = useBetween(useFooStore); + return { doThing }; +}; + +// deprecated shim — keeps the historical return shape +export const useFoo = () => useBetween(useFooStore); +``` + +`useBetween` ensures all three entry points hit the same store +instance, so listeners/effects register once. Used by `useWiredTools`, +`useTranslation`, `useNotification`, `useFriends`. ### Zustand stores @@ -159,9 +197,9 @@ Login / Register / Forgot in `src/components/login/LoginView.tsx` use | `useNitroQuery` + `useNitroEventInvalidator` | `OfferView`, `CatalogLayoutRoomAdsView`, `ModToolsChatlogView`, `CfhChatlogView`, `useGiftConfiguration`, `useUserGroups`, `useClubOffers(windowId)`, `useSellablePetPalette(breed)`, `useMarketplaceConfiguration`, `useClubGifts` (with invalidator) | | Zustand | `NavigatorRoomCreatorView` (`useRoomCreatorStore`) | | God-hook split (state + actions + shim) | `doorbell`, `poll`, `furni-chooser`, `user-chooser`, `friend-request`, `chat-input` | -| God-hook split (`useBetween` singleton + state filter + actions filter + shim) | `wired-tools`, `translation` | +| God-hook split (`useBetween` singleton + state filter + actions filter + shim) | `wired-tools`, `translation`, `notification`, `friends` | | `WidgetErrorBoundary` | `RoomWidgetsView` umbrella | -| Vitest | 99/99 cases on pure helpers + the Zustand store | +| Vitest | 113/113 cases on pure helpers + the Zustand store | | Not yet | Notes | |---|---| @@ -197,7 +235,7 @@ Fix shapes documented; both are reasonable PRs on their own. - **Skip-motivated god-hook splits are fine** — when a hook's actions mutate internal state, document the reason in the commit message and move on rather than forcing a bad split. -- **`yarn test` must stay green** on every commit. Currently 99/99. +- **`yarn test` must stay green** on every commit. Currently 113/113. - **Lint baseline**: don't regress. Some pre-existing errors (`FC<{}>`, `IMessageEvent | undefined` redundant union in the local sandbox where the renderer SDK isn't installed) are out of scope here. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f1049e4..658bf95 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -427,6 +427,18 @@ The current branch (**`feat/react19-modernization`**, PR #2) has applied: Wired tools — six consumers split across read-only views (settings panel, bootstrap) and dispatch sites (messenger, chat input). + - **notification**: `useNotificationStore` (internal singleton) + + `useNotificationState` (queue arrays for the renderer view) + + `useNotificationActions` (8 entry points: simpleAlert, + showNitroAlert, showTradeAlert, showConfirm, showSingleBubble, + closeAlert, closeBubbleAlert, closeConfirm) + shim. The ~30 + message-event listeners and 5 state slices stay in the singleton. + Used by ~44 consumers, most of which only need one action. + - **friends**: `useFriendsStore` (internal singleton) + + `useFriendsState` (friends arrays, settings, derived + online/offline split, lookup helpers) + `useFriendsActions` + (requestFriend, requestResponse, followFriend, updateRelationship) + + shim. 16 consumers. - **Zustand** (proposal #5) — **enabled**. `zustand` installed; factory at `src/state/createNitroStore.ts`. First adoption: the `let isCreatingRoom` / `createRoomTimeout` module-level pair in `NavigatorRoomCreatorView` @@ -472,7 +484,7 @@ Status after this round of work: - Vitest 3 + jsdom + `@testing-library/react` + `@testing-library/jest-dom` configured. Separate `vitest.config.mts` so the runner doesn't drag in the renderer SDK aliases from `vite.config.mjs`. -- **99 cases passing** across 7 test files: +- **113 cases passing** across 8 test files: - `WiredCreatorTools.helpers.test.ts` (18) — formatters + snapshot factory. - `navigatorRoomCreatorStore.test.ts` (4) — Zustand store invariants @@ -488,10 +500,16 @@ Status after this round of work: (covers the helper used by the InfoStand pilot). - `catalog-favorites.helpers.test.ts` (16) — localStorage parse + v2→v3 migration + per-catalog-type storage-key routing. + - `avatar-info-reducers.test.ts` (14) — InfoStand reducer pilot: + bail-out branches (state-not-AvatarInfoUser, mismatched + user/roomIndex, equal-after-dedup) + the figure / favorite-group + apply paths. - **Pure-module convention**: tests live in `tests/` and import from concrete file paths (e.g. `../src/api/catalog/CatalogType`) rather than the api barrel, so jsdom doesn't transitively load the renderer - SDK's Pixi-bound modules. + SDK's Pixi-bound modules. Renderer event type imports use + `import type { … }` so they're erased at compile time and don't + trigger the runtime module load either. - `yarn test` + `yarn test:watch` scripts added. ### Logic bug fixes